WPF 单实例最佳实践

52

这是我迄今为止实现创建单个实例 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
    }
}

一切都工作得非常完美...但我还有一些疑虑,希望能收到您的建议,提高我的方法。

1)由于我正在使用Mutex,所以Code Analysis要求我实现IDisposable接口。我的Dispose()实现足够好吗?因为它永远不会被调用,所以我应该避免使用它吗?

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

3)RegisterWindowMessage API调用应返回UInt32,但HwndSourceHook仅接受Int32作为消息值。我是否应该担心意外的行为(如大于Int32.MaxValue的结果)?

4)在OnStartup覆盖中,即使另一个实例已经在运行并且我将关闭应用程序,我是否应该执行base.OnStartup(e);

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

6)您是否能看到我的方法中的任何缺陷?例如关于多线程、异常处理等等。例如...如果我的应用程序在OnStartupOnExit之间崩溃会发生什么?


4
好问题。然而,它包含了很多问题,因此http://codereview.stackexchange.com可能更合适。 - Heinzi
好的,我可能会移动它! - Tommaso Belluzzo
3
你有没有检查过微软的实现?http://elegantcode.com/wp-content/uploads/2011/03/SingleInstance_cs.txt - stijn
1
远程IPC管理对我来说似乎有点过于复杂了。这就像用核弹轰击一只苍蝇一样。 - Tommaso Belluzzo
2
http://stackoverflow.com/questions/2849687/maintaining-single-instance-application - kenny
2
从那个链接和其中的其他链接中,我喜欢这个答案http://stackoverflow.com/a/1904772/3225 - kenny
13个回答

66

有几种选择:

  • Mutex
  • 进程管理器
  • 命名信号量
  • 使用监听套接字

Mutex

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();  
    }
}

进程管理器

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(); 
    }
}

使用监听套接字

向另一个应用程序发信号的一种方法是打开一个Tcp连接。创建一个套接字,绑定到一个端口,并在后台线程上监听连接。如果成功,正常运行。如果失败,则连接到该端口,这将向其他实例发出信号,表示已经尝试启动第二个应用程序。原始实例可以在适当的情况下将其主窗口置于前景。

“安全”软件/防火墙可能是一个问题。

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


1
没有网络接口卡/ IP地址的计算机 -> 不是问题,您可以在同一台机器上使用环回适配器。 - Lorenzo Dematté
1
作為額外的補充,您可能想要添加對SetForegroundWindow(IntPtr hWnd)和ShowWindowAsync(IntPtr hWnd,int nCmdShow)的調用,以將當前運行的實例帶到前景,而不是顯示消息框。 您需要使用[DLLImport("user32.dll")]來完成這一操作。 - Shannon Cook
“MyWPFApplication” 是 EXE 的名称吗? - NoWar
2
@Clark - Mutex的名称。可以是任何唯一的名称。因此,该互斥锁仅允许在该唯一名称下存在一个实例。 - C-va

55

我希望提供更好的用户体验 - 如果另一个实例已经在运行,则激活它,而不是显示第二个实例的错误。这是我的实现。

我使用命名的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
}

在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();
    }

同时添加 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>

对我来说运作良好 :)


4
您忘了提到我们需要在App.Xaml文件中添加AppOnStartup: Startup="AppOnStartup" - vhanla
2
这是一个非常棒的答案。我几乎可以逐字地将其插入我的应用程序中,然后轻松地扩展它以支持我的要求。运行得非常好! - thehelix
唯一的缺点是它使用了线程。可能对性能不利。 - Altiano Gerung
1
这也解决了我的问题,非常感谢。我应该补充一下,我的App.xaml文件有Startup="AppOnStartup"和StartupUri="MainWindow.xaml"。我还有一个小问题,即网络部署/发布的应用程序每次用户从桌面启动时都会检查新版本,但我可以接受。 - miriyo
1
@AltianoGerung,性能没有问题,因为当线程在等待事件时,它不会处于活动状态。 - Adrian S
1
我喜欢线程的方法 - 它比旧方法更好,旧方法是搜索与应用程序同名的顶级窗口。只需确保您的事件名称和互斥体名称确实是唯一的 - 我使用应用程序名称后跟使用Create GUID创建的GUID。 - Adrian S

48

对于 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);
    }          
}

这是最佳答案的代码,简洁明了,没有使用血腥的外部dll和过时的大量代码。 - luka
可以简化这个代码并移除一个任意的字符串:_mutex = new Mutex(true, this.GetType().Namespace.ToString(), out createdNew); - Geordie

12

为了防止第二个实例(并标记现有实例),可以采用以下方法:

  • 使用EventWaitHandle(因为我们正在讨论事件),
  • 使用Task,
  • 不需要Mutex代码,
  • 不需要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
        }
    }
}

第二个版本:在原来的基础上,向另一个实例发信号以显示窗口(对于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="https://dev59.com/KWUq5IYBdhLWcg3wbv5Z#23730146"/>
    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();
    }
}

这个代码可以作为一个独立的类,放在 Selfcontained-C-Sharp-WPF-compatible-utility-classes/Utils.SingleInstance.cs 中使用。


1
很棒的解决方案。我喜欢你使用EventWaitHandle而不是依赖于互斥锁或类似的东西的方式。我所做的唯一更改是特别捕获WaitHandleCannotBeOpenedException,并将等待句柄的创建移动到任务本身内部,因为它永远不需要是全局的(它可以在“try”块中是本地的,并且在任务内部是第二个本地的)。但是,这是最好的解决方案。 - Mark A. Donohoe
@MarqueIV 谢谢 - 只捕获特定的异常是有意义的。我认为在任务之外同步创建处理程序的原因是,在应用程序退出之前,它可以避免异步运行任务 - 这样更加节省资源,也许可以防止任何应用程序内容闪烁或运行(或代码后面的任何消息框,调试等 = 阻塞代码流)。 - BananaAcid
有道理。但是即使您在任务之外创建它,它仍然可以作为该函数的本地变量,因为任务应该捕获它,不是吗? - Mark A. Donohoe
哦,当然 - 我刚意识到你的意思。在那种情况下,它不必在全局范围内。只是把它留在那里,以便在任何其他方法触发该eventWaitHandle对象上的任何内容或对其做出反应时可以访问它... - BananaAcid
4
我会尝试提出两个建议改进。首先,建议您使用TryOpenExisting而不是将异常作为正常流程的一部分;其次,注册任务为长时间运行的任务——TaskCreationOptions.LongRunning——以便您不会在应用程序的整个生命周期中使用任务池中的一个空间。 - Itzalive
好的,EventWaitHandle.OpenExisting确实会在底层创建一个同步对象,所以我不明白为什么它应该比命名互斥体更好,但看到另一种解决方案确实很有趣! - Lorenzo Dematté

8

1) 对我来说,它看起来像是一个标准的Dispose实现。这并不是真正必要的(参见第6点),但它也不会有任何伤害。(在关闭时进行清理有点像在烧毁房子之前清洁房子,但对此问题的看法不同...)

无论如何,为什么不将“Dispose”作为清理方法的名称,即使它不被直接调用?你本可以称其为“Cleanup”,但请记住,你还要为人类编写代码,“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的等效方法。
  • 你可能不必担心崩溃(内核会为内核对象降低引用计数;但还是做一个小测试),但如果我可以提出一点改进,那就是:如果你的第一个应用程序实例没有崩溃,而是挂起了呢?(Firefox就会发生这种情况……我相信你也遇到过!没有窗口、没有ff进程,你无法打开新的窗口)。在这种情况下,可以结合其他技术来解决,比如a)测试应用程序/窗口是否响应;b)找到挂起的实例并终止它。

例如,你可以使用自己的技术(尝试向窗口发送/发布消息-如果没有回复,就是卡住了),再加上MSK技术,来查找和终止旧的进程。然后正常启动。


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();
        }
    }

}

9
嗯... OP 代码有什么区别吗?他使用了一个命名互斥量,为什么信号量会更好? - Lorenzo Dematté
@LorenzoDematté 我知道这是你的一个老问题,但对于其他人来说,请阅读此答案以获得重要差异的良好分析。 - Geoff
2
@Geoff 虽然这个答案很有趣,但它是针对不同的操作系统。在NT内核中,互斥锁和信号量都是内核调度对象,并且它们的工作方式非常相似。毫无疑问,在这种情况下,使用计数为1的信号量而不是互斥锁对OP的目的没有任何影响。 - Lorenzo Dematté

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();
    }
}

4

我曾经使用过一个简单的TCP套接字来实现这个功能(在Java中,大约10年前)。

  1. 启动时连接到预定义端口,如果连接被接受,则表示另一个实例正在运行,否则启动TCP监听器
  2. 一旦有人连接到您,弹出窗口并断开连接

1
请使用锁文件而不是端口。 - invalidusername
8
我个人不喜欢锁定文件:与其他资源(句柄、套接字)不同,如果进程崩溃,操作系统不会自动“删除”它们。一些程序有这个问题(Firefox的早期版本?),它们非常令人讨厌。 - Lorenzo Dematté
2
是的,但打开另一个端口似乎也是解决这个问题的一种奇怪方式。 - invalidusername
1
这可能很奇怪,但它是有效的,并且在几乎任何系统上都可以工作。我唯一关心的是,有时即使应用程序崩溃后一段时间,监听端口仍然保持“打开”状态。 - toster-cx

2

这是一个简单的解决方案,

打开启动文件(从应用程序开始查看),在这种情况下是MainWindow.xaml。

打开MainWindow.xaml.cs文件。进入构造函数,在intializecomponent()之后添加以下代码:

Process Currentproc = Process.GetCurrentProcess();

Process[] procByName=Process.GetProcessesByName("notepad");  //Write the name of your exe file in inverted commas
if(procByName.Length>1)
{
  MessageBox.Show("Application is already running");
  App.Current.Shutdown();
 }

不要忘记添加 System.Diagnostics


1
这是一个将旧实例带到前台的示例:
public partial class App : Application
{
    [DllImport("user32", CharSet = CharSet.Unicode)]
    static extern IntPtr FindWindow(string cls, string win);
    [DllImport("user32")]
    static extern IntPtr SetForegroundWindow(IntPtr hWnd);
    [DllImport("user32")]
    static extern bool IsIconic(IntPtr hWnd);
    [DllImport("user32")]
    static extern bool OpenIcon(IntPtr hWnd);

    private static Mutex _mutex = null;

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

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

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

        base.OnStartup(e);
    }

    private static void ActivateOtherWindow()
    {
        var other = FindWindow(null, "!YOUR MAIN WINDOW TITLE HERE!");
        if (other != IntPtr.Zero)
        {
            SetForegroundWindow(other);
            if (IsIconic(other))
                OpenIcon(other);
        }
    }
}

但它只在运行时主窗口标题不改变的情况下才能工作。
编辑:
你也可以在 App.xaml 中使用 Startup 事件,而不是重写 OnStartup。
// App.xaml.cs
private void Application_Startup(object sender, StartupEventArgs e)
{
    const string appName = "LinkManager";
    bool createdNew;

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

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

// App.xaml
<Application x:Class="MyApp.App"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:local="clr-namespace:MyApp"
         StartupUri="MainWindow.xaml" Startup="Application_Startup"> //<- startup event

记住,在这种情况下不要调用base.OnStartup(e)


针对.NET 2和Win XP的解决方案...但我对它的问题是在标题后面进行操作。我看到了很多潜力,可以使其失效,遇到安全限制,在未来的Windows环境中无法工作... - BananaAcid

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接