WPF单实例最佳实践

Tom*_*zzo 36 c# wpf singleton mutex

这是我到目前为止实现的代码,用于创建单个实例WPF应用程序:

#region Using Directives
using System;
using System.Globalization;
using System.Reflection;
using System.Threading;
using System.Windows;
using System.Windows.Interop;
#endregion

namespace MyWPF
{
    public partial class MainApplication : Application, IDisposable
    {
        #region Members
        private Int32 m_Message;
        private Mutex m_Mutex;
        #endregion

        #region Methods: Functions
        private IntPtr HandleMessages(IntPtr handle, Int32 message, IntPtr wParameter, IntPtr lParameter, ref Boolean handled)
        {
            if (message == m_Message)
            {
                if (MainWindow.WindowState == WindowState.Minimized)
                    MainWindow.WindowState = WindowState.Normal;

                Boolean topmost = MainWindow.Topmost;

                MainWindow.Topmost = true;
                MainWindow.Topmost = topmost;
            }

            return IntPtr.Zero;
        }

        private void Dispose(Boolean disposing)
        {
            if (disposing && (m_Mutex != null))
            {
                m_Mutex.ReleaseMutex();
                m_Mutex.Close();
                m_Mutex = null;
            }
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
        #endregion

        #region Methods: Overrides
        protected override void OnStartup(StartupEventArgs e)
        {
            Assembly assembly = Assembly.GetExecutingAssembly();
            Boolean mutexCreated;
            String mutexName = String.Format(CultureInfo.InvariantCulture, "Local\\{{{0}}}{{{1}}}", assembly.GetType().GUID, assembly.GetName().Name);

            m_Mutex = new Mutex(true, mutexName, out mutexCreated);
            m_Message = NativeMethods.RegisterWindowMessage(mutexName);

            if (!mutexCreated)
            {
                m_Mutex = null;

                NativeMethods.PostMessage(NativeMethods.HWND_BROADCAST, m_Message, IntPtr.Zero, IntPtr.Zero);

                Current.Shutdown();

                return;
            }

            base.OnStartup(e);

            MainWindow window = new MainWindow();
            MainWindow = window;
            window.Show(); 

            HwndSource.FromHwnd((new WindowInteropHelper(window)).Handle).AddHook(new HwndSourceHook(HandleMessages));
        }

        protected override void OnExit(ExitEventArgs e)
        {
            Dispose();
            base.OnExit(e);
        }
        #endregion
    }
}
Run Code Online (Sandbox Code Playgroud)

一切都很完美......但我对它有一些疑问,我想收到你关于如何改进我的方法的建议.

1)Code Analysis要求我实现IDisposable接口,因为我使用的是IDisposable成员(the Mutex).我的Dispose()实施是否足够好?我应该避免它,因为它永远不会被调用吗?

2)最好使用m_Mutex = new Mutex(true, mutexName, out mutexCreated);并检查结果或使用m_Mutex = new Mutex(false, mutexName);然后检查m_Mutex.WaitOne(TimeSpan.Zero, false);?在多线程的情况下,我的意思是......

3)RegisterWindowMessageAPI调用应该返回UInt32...但是HwndSourceHook只接受Int32消息值...我应该担心意外行为(比如结果大于Int32.MaxValue)?

4)在OnStartup覆盖... base.OnStartup(e);即使另一个实例已在运行我应该执行并且我要关闭应用程序吗?

5)有没有更好的方法将现有实例置于不需要设置Topmost值的顶部?也许Activate()

6)你能看到我的方法有什么缺陷吗?关于多线程,坏的异常处理和类似的东西?例如......如果我的应用程序在OnStartup和之间崩溃会发生什么OnExit

C-v*_*-va 44

有几种选择,

  • 互斥
  • 流程经理
  • 名为Semaphore
  • 使用侦听器套接字

    互斥

    Mutex myMutex ;
    
    private void Application_Startup(object sender, StartupEventArgs e)
    {
        bool aIsNewInstance = false;
        myMutex = new Mutex(true, "MyWPFApplication", out aIsNewInstance);  
        if (!aIsNewInstance)
        {
            MessageBox.Show("Already an instance is running...");
            App.Current.Shutdown();  
        }
    }
    
    Run Code Online (Sandbox Code Playgroud)

    流程经理

    private void Application_Startup(object sender, StartupEventArgs e)
    {
        Process proc = Process.GetCurrentProcess();
        int count = Process.GetProcesses().Where(p=> 
            p.ProcessName == proc.ProcessName).Count();
    
        if (count > 1)
        {
            MessageBox.Show("Already an instance is running...");
            App.Current.Shutdown(); 
        }
    }
    
    Run Code Online (Sandbox Code Playgroud)

使用侦听器套接字

向另一个应用程序发出信号的一种方法是打开一个Tcp连接.创建套接字,绑定到端口,然后在后台线程上侦听连接.如果成功,请正常运行.如果没有,则建立与该端口的连接,该端口向另一个实例发出第二次应用程序启动尝试的信号.如果合适,原始实例可以将其主窗口置于前面.

"安全"软件/防火墙可能是一个问题.

单实例应用程序C#.Net和Win32

  • @Clark - 互斥体的名称。可以是任何独特的名称。因此,互斥体只允许该唯一名称下的一个实例。 (2认同)

Zak*_*iMa 35

我希望有一个更好的用户体验 - 如果另一个实例已经在运行,让我们激活它,而不是显示有关第二个实例的错误.这是我的实施.

我使用命名的Mutex来确保只有一个实例正在运行并命名为EventWaitHandle以将通知从一个实例传递到另一个实例.

App.xaml.cs:

/// <summary>Interaction logic for App.xaml</summary>
public partial class App
{
    #region Constants and Fields

    /// <summary>The event mutex name.</summary>
    private const string UniqueEventName = "{GUID}";

    /// <summary>The unique mutex name.</summary>
    private const string UniqueMutexName = "{GUID}";

    /// <summary>The event wait handle.</summary>
    private EventWaitHandle eventWaitHandle;

    /// <summary>The mutex.</summary>
    private Mutex mutex;

    #endregion

    #region Methods

    /// <summary>The app on startup.</summary>
    /// <param name="sender">The sender.</param>
    /// <param name="e">The e.</param>
    private void AppOnStartup(object sender, StartupEventArgs e)
    {
        bool isOwned;
        this.mutex = new Mutex(true, UniqueMutexName, out isOwned);
        this.eventWaitHandle = new EventWaitHandle(false, EventResetMode.AutoReset, UniqueEventName);

        // So, R# would not give a warning that this variable is not used.
        GC.KeepAlive(this.mutex);

        if (isOwned)
        {
            // Spawn a thread which will be waiting for our event
            var thread = new Thread(
                () =>
                {
                    while (this.eventWaitHandle.WaitOne())
                    {
                        Current.Dispatcher.BeginInvoke(
                            (Action)(() => ((MainWindow)Current.MainWindow).BringToForeground()));
                    }
                });

            // It is important mark it as background otherwise it will prevent app from exiting.
            thread.IsBackground = true;

            thread.Start();
            return;
        }

        // Notify other instance so it could bring itself to foreground.
        this.eventWaitHandle.Set();

        // Terminate this instance.
        this.Shutdown();
    }

    #endregion
}
Run Code Online (Sandbox Code Playgroud)

和MainWindow.cs中的BringToForeground:

    /// <summary>Brings main window to foreground.</summary>
    public void BringToForeground()
    {
        if (this.WindowState == WindowState.Minimized || this.Visibility == Visibility.Hidden)
        {
            this.Show();
            this.WindowState = WindowState.Normal;
        }

        // According to some sources these steps gurantee that an app will be brought to foreground.
        this.Activate();
        this.Topmost = true;
        this.Topmost = false;
        this.Focus();
    }
Run Code Online (Sandbox Code Playgroud)

并添加Startup ="AppOnStartup"(谢谢vhanla!):

<Application x:Class="MyClass.App"  
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"   
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             Startup="AppOnStartup">
    <Application.Resources>
    </Application.Resources>
</Application>
Run Code Online (Sandbox Code Playgroud)

适合我:)

  • 你忘了提到我们需要将AppOnStartup添加到App.Xaml文件中:`Startup ="AppOnStartup"` (2认同)
  • 这是一个很棒的答案。我几乎能够将其逐字插入到我的应用程序中,然后轻松扩展它以支持我的需求。奇迹般有效! (2认同)

sme*_*asn 30

对于WPF,只需使用:

public partial class App : Application
{
    private static Mutex _mutex = null;

    protected override void OnStartup(StartupEventArgs e)
    {
        const string appName = "MyAppName";
        bool createdNew;

        _mutex = new Mutex(true, appName, out createdNew);

        if (!createdNew)
        {
            //app is already running! Exiting the application  
            Application.Current.Shutdown();
        }

        base.OnStartup(e);
    }          
}
Run Code Online (Sandbox Code Playgroud)

  • 轻松,精益,完美的工作 (2认同)

Lor*_*tté 7

1)它看起来像我的标准Dispose实现.这不是必要的(见第6点),但它没有任何伤害.(清理关闭它有点像清理房子,然后烧掉它,恕我直言,但对此事的意见不同......)

无论如何,为什么不使用"Dispose"作为清理方法的名称,即使它没有被直接调用?您可以将其称为"清理",但请记住您也为人类编写代码,Dispose看起来很熟悉,.NET上的任何人都了解它的用途.所以,去"Dispose".

2)我一直认为,m_Mutex = new Mutex(false, mutexName);我认为这更像是一项技术优势的惯例.

3)来自MSDN:

如果消息成功注册,则返回值是0xC000到0xFFFF范围内的消息标识符.

所以我不担心.通常,对于这类函数,UInt不用于"它不适合Int,让我们使用UInt所以我们有更多东西"但澄清合同"函数永远不会返回负值".

4)如果你关机,我会避免打电话,原因与#1相同

5)有几种方法可以做到这一点.Win32中最简单的方法就是让第二个实例调用SetForegroundWindow(在这里查看:http://blogs.msdn.com/b/oldnewthing/archive/2009/02/20/9435239.aspx); 但是,我不知道是否有等效的WPF功能,或者你是否需要PInvoke它.

6)

例如......如果我的应用程序在OnStartup和OnExit之间崩溃会发生什么?

没关系:当进程终止时,进程拥有的所有句柄都被释放; 互斥体也被释放了.

总之,我的建议:

  • 我会使用一种基于命名同步对象的方法:它是在Windows平台上建立的.(在考虑多用户系统时要小心,比如终端服务器!将同步对象命名为,可能是用户名/ SID和应用程序名称的组合)
  • 使用Windows API引发上一个实例(请参阅第5点的链接)或WPF等效实例.
  • 您可能不必担心崩溃(内核会为您减少内核对象的ref计数器;无论如何都要进行一些测试),但是如果我可以建议改进:如果您的第一个应用程序实例没有崩溃但挂起怎么办?(发生在Firefox上......我确定它也发生在你身上!没有窗口,ff进程,你无法打开一个新的).在这种情况下,将另一种技术或两种技术结合起来可能是好的,a)测试应用程序/窗口是否响应; b)找到挂起的实例并终止它

例如,您可以使用您的技术(尝试向窗口发送/发送消息 - 如果没有回复它被卡住),加上MSK技术,以查找和终止旧进程.然后正常开始.


Ban*_*cid 7

防止第二个实例(并发出现有信号),

  • 使用 EventWaitHandle(因为我们在谈论一个事件),
  • 使用任务,
  • 不需要互斥代码,
  • 没有TCP,
  • 没有 Pinvokes,
  • 没有垃圾收集的东西,
  • 线程保存
  • 简单的

可以这样做(这适用于 WPF 应用程序(参见 App() 的参考),但也适用于 WinForms):

public partial class App : Application
{
    public App()
    {
        // initiate it. Call it first.
        preventSecond();
    }

    private const string UniqueEventName = "{GENERATE-YOUR-OWN-GUID}";

    private void preventSecond()
    {
        try
        {
            EventWaitHandle.OpenExisting(UniqueEventName); // check if it exists
            this.Shutdown();
        }
        catch (WaitHandleCannotBeOpenedException)
        {
            new EventWaitHandle(false, EventResetMode.AutoReset, UniqueEventName); // register
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

第二个版本:上面加上信号另一个实例来显示窗口(更改 WinForms 的 MainWindow 部分):

public partial class App : Application
{
    public App()
    {
        // initiate it. Call it first.
        //preventSecond();
        SingleInstanceWatcher();
    }

    private const string UniqueEventName = "{GENERATE-YOUR-OWN-GUID}";
    private EventWaitHandle eventWaitHandle;

    /// <summary>prevent a second instance and signal it to bring its mainwindow to foreground</summary>
    /// <seealso cref="/sf/answers/1661110251/"/>
    private void SingleInstanceWatcher()
    {
        // check if it is already open.
        try
        {
            // try to open it - if another instance is running, it will exist , if not it will throw
            this.eventWaitHandle = EventWaitHandle.OpenExisting(UniqueEventName);

            // Notify other instance so it could bring itself to foreground.
            this.eventWaitHandle.Set();

            // Terminate this instance.
            this.Shutdown();
        }
        catch (WaitHandleCannotBeOpenedException)
        {
            // listen to a new event (this app instance will be the new "master")
            this.eventWaitHandle = new EventWaitHandle(false, EventResetMode.AutoReset, UniqueEventName);
        }

        // if this instance gets the signal to show the main window
        new Task(() =>
        {
            while (this.eventWaitHandle.WaitOne())
            {
                Current.Dispatcher.BeginInvoke((Action)(() =>
                {
                    // could be set or removed anytime
                    if (!Current.MainWindow.Equals(null))
                    {
                        var mw = Current.MainWindow;

                        if (mw.WindowState == WindowState.Minimized || mw.Visibility != Visibility.Visible)
                        {
                            mw.Show();
                            mw.WindowState = WindowState.Normal;
                        }

                        // According to some sources these steps are required to be sure it went to foreground.
                        mw.Activate();
                        mw.Topmost = true;
                        mw.Topmost = false;
                        mw.Focus();
                    }
                }));
            }
        })
        .Start();
    }
}
Run Code Online (Sandbox Code Playgroud)

此代码作为类中的下降,将是@ Selfcontained-C-Sharp-WPF-compatible-utility-classes / Utils.SingleInstance.cs

  • 我会建议两个改进。首先,您使用 TryOpenExisting 而不是将异常用作正常流程的一部分,其次,您将任务注册为长时间运行的任务 - `TaskCreationOptions.LongRunning` - 因此您没有使用整个任务池空间之一应用程序的生命周期。 (3认同)

小智 5

处理它的最直接的方法是使用命名信号量.尝试这样的事......

public partial class App : Application
{
    Semaphore sema;
    bool shouldRelease = false;

    protected override void OnStartup(StartupEventArgs e)
    {

        bool result = Semaphore.TryOpenExisting("SingleInstanceWPFApp", out sema);

        if (result) // we have another instance running
        {
            App.Current.Shutdown();
        }
        else
        {
            try
            {
                sema = new Semaphore(1, 1, "SingleInstanceWPFApp");
            }
            catch
            {
                App.Current.Shutdown(); //
            }
        }

        if (!sema.WaitOne(0))
        {
            App.Current.Shutdown();
        }
        else
        {
            shouldRelease = true;
        }


        base.OnStartup(e);
    }

    protected override void OnExit(ExitEventArgs e)
    {
        if (sema != null && shouldRelease)
        {
            sema.Release();
        }
    }

}
Run Code Online (Sandbox Code Playgroud)

  • 嗯......与OP代码的区别是什么?他使用一个命名的互斥体,为什么信号量应该更好? (7认同)
  • @Geoff 虽然答案很有趣,但它适用于不同的操作系统。在 NT 内核中,互斥体和信号量都是内核调度程序对象,并且它们的行为方式非常相似。可以肯定的是,出于OP目的,使用计数为1的信号量而不是互斥体没有任何效果。 (2认同)

ffe*_*ech 5

我的 .Net Core 3 Wpf 单实例应用程序解决方案:

[STAThread]
public static void Main()
{
    StartSingleInstanceApplication<CntApplication>();
}

public static void StartSingleInstanceApplication<T>()
    where T : RichApplication
{
    DebuggerOutput.GetInstance();

    Assembly assembly = typeof(T).Assembly;
    string mutexName = $"SingleInstanceApplication/{assembly.GetName().Name}/{assembly.GetType().GUID}";

    Mutex mutex = new Mutex(true, mutexName, out bool mutexCreated);

    if (!mutexCreated)
    {
        mutex = null;

        var client = new NamedPipeClientStream(mutexName);
        client.Connect();

        using (StreamWriter writer = new StreamWriter(client))
            writer.Write(string.Join("\t", Environment.GetCommandLineArgs()));

        return;
    }
    else
    {
        T application = Activator.CreateInstance<T>();

        application.Exit += (object sender, ExitEventArgs e) =>
        {
            mutex.ReleaseMutex();
            mutex.Close();
            mutex = null;
        };

        Task.Factory.StartNew(() =>
        {
            while (mutex != null)
            {
                using (var server = new NamedPipeServerStream(mutexName))
                {
                    server.WaitForConnection();

                    using (StreamReader reader = new StreamReader(server))
                    {
                        string[] args = reader.ReadToEnd().Split("\t", StringSplitOptions.RemoveEmptyEntries).ToArray();
                        UIDispatcher.GetInstance().Invoke(() => application.ExecuteCommandLineArgs(args));
                    }
                }
            }
        }, TaskCreationOptions.LongRunning);

        typeof(T).GetMethod("InitializeComponent").Invoke(application, new object[] { });
        application.Run();
    }
}
Run Code Online (Sandbox Code Playgroud)