WPF 单实例最佳做法

本文关键字:最佳 实例 单实例 WPF | 更新日期: 2023-09-27 17:56:05

这是我到目前为止实现的用于创建单个实例 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)代码分析要求我实现IDisposable接口,因为我使用的是IDisposable成员(Mutex)。我的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 之间崩溃,会发生什么情况?

WPF 单实例最佳做法

有几种选择,

  • 互斥体
  • 流程管理器
  • 命名信号量
  • 使用侦听器套接字

互斥体

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

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

我使用名为 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:

    /// <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>

为我工作:)

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

以防止第二个实例(并指示现有实例),

  • 使用EventWaitHandle(因为我们谈论的是事件),
  • 使用任务,
  • 无需互斥代码,
  • 没有TCP,
  • 没有平唤,
  • 没有垃圾收集的东西,
  • 线程保存
  • 简单

它可以像这样完成(这对于 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的主窗口部分):

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://stackoverflow.com/a/23730146/1644202"/>
    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)对我来说,它看起来像一个标准的Dispose实现。这不是真正必要的(见第6点),但它不会造成任何伤害。(恕我直言,关闭时的清理有点像在烧毁房屋之前清理房屋,但对此事的看法不同。

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

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 功能,或者您是否需要调用它。

6)

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

没关系:当进程终止时,将释放该进程拥有的所有句柄;也会释放互斥锁。

简而言之,我的建议:

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

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

处理此问题的最直接方法是使用命名信号量。试试这样的事情...

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

我的 .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();
    }
}
<</div> div class="answers">

我为此使用了一个简单的TCP套接字(在Java中,10年前)。

    启动
  1. 时连接到预定义端口,如果接受连接,则另一个实例正在运行,如果没有,则启动 TCP 侦听器
  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();
 }

不要忘记添加系统诊断

下面是将旧实例带到前台的示例:

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

但仅当您的主窗口标题不更改 durig 运行时时,它才会起作用。

编辑:

您还可以在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)

只是把我的帽子扔进这里的戒指。 我所做的是创建常规Application类的ApplicationBase子类,该子类保存在所有 WPF 应用程序中使用的公共库中。然后,我更改基类(从 XAML 及其代码隐藏中)以使用我的基类。最后,我使用 EntryPoint.Main 作为我的应用程序的启动对象,然后检查单个实例状态,如果我不是第一个,则只需返回。

注意:我还展示了如何支持一个标志,如果你想启动另一个实例,你可以覆盖它。但是,请注意此类选项。仅在实际有意义的地方使用它。

代码如下:

应用程序

库(应用程序子类)

public abstract class ApplicationBase : Application {
    public static string? SingleInstanceId { get; private set; }
    public static bool InitializeAsFirstInstance(string singleInstanceId){
        if(SingleInstanceId != null)
            throw new AlreadyInitializedException(singleInstanceId);
        SingleInstanceId = singleInstanceId;
        var waitHandleName = $"SingleInstanceWaitHandle:{singleInstanceId}";
        if(EventWaitHandle.TryOpenExisting(waitHandleName, out var waitHandle)){
            // An existing WaitHandle was successfuly opened which means we aren't the first so signal the other
            waitHandle.Set();
            // Then indicate we aren't the first instance by returning false
            return false;
        }
        // Welp, there was no existing WaitHandle with this name, so we're the first!
        // Now we have to set up the EventWaitHandle in a task to listen for other attempts to launch
        void taskBody(){
            var singleInstanceWaitHandle = new EventWaitHandle(false, EventResetMode.AutoReset, waitHandleName);
            while (singleInstanceWaitHandle.WaitOne()) {
                if(Current is ApplicationBase applicationBase)
                    Current.Dispatcher.BeginInvoke(applicationBase.OtherInstanceLaunched);
            }
        }
        new Task(taskBody, TaskCreationOptions.LongRunning).Start();
        return true;
    }
    public static bool IsSingleInstance
        => SingleInstanceId != null;
    protected virtual void OtherInstanceLaunched()
        => Current.MainWindow?.BringToFront();
}

通过将OtherInstanceLaunched标记为虚拟,我可以通过简单地覆盖它来基于每个应用程序对其进行自定义,或者只是让默认实现执行它的事情,这里,这是我添加的Window上的扩展方法。 (本质上,它确保它是可见的,恢复的,然后聚焦它。

入口点.主

public static class EntryPoint {
    public static class CommandLineArgs{
        public const string AllowMulti = "/AllowMulti";
        public const string NoSplash   = "/NoSplash";
    }
    [STAThread]
    public static int Main(string[] args) {
        var showSplashScreen = true;
        var allowMulti       = false;
        foreach (var arg in args) {
            if (arg.Equals(CommandLineArgs.AllowMulti, StringComparison.CurrentCultureIgnoreCase))
                allowMulti = true;
            if (arg.Equals(CommandLineArgs.NoSplash, StringComparison.CurrentCultureIgnoreCase))
                showSplashScreen = false;
        }
        // Try and initialize myself as the first instance. If I'm not and 'allowMulti' is false, exit with a return code of 1
        if (!ApplicationBase.InitializeAsFirstInstance(ApplicationInfo.ProductName) && !allowMulti)
            return 1;
        if (showSplashScreen) {
            var splashScreen = new SplashScreen("resources/images/splashscreen.png");
            splashScreen.Show(true, false);
        }
        _ = new App();
        return 0;
    }
}

此方法的优点是,它甚至在实例化应用程序本身之前以及显示初始屏幕之前就移交执行。 换句话说,它会尽早救助。

注意:如果您甚至不想要多支持,则可以删除该参数检查和测试。这只是为了说明目的而添加

尽管 GetProcessesByName()。长度方法有效,互斥是计算机范围所需的C#锁。由于 WPF 不会像 WinForms 那样在程序集信息中自动生成 GUID,因此应用本身必须自行生成唯一标识符。另请注意,互斥体必须在整个应用程序生命周期中保持可见;否则,它将在释放时自动释放。调用 Mutex.WaitOne() 方法进行锁定,调用 Mutex.ReleaseMutex() 方法进行解锁。参考:Mutex,C#线程由Joe Albahari...www.albahari.com/threading/

private Mutex mutex = new Mutex(false, <Author> + <AppName>);
private void Application_Startup(object sender, StartupEventArgs e)
{
    if (!mutex.WaitOne()) { App.Current.Shutdown(<ExitCode>); }
    else                  { new MainWindow(e.Args); }
}

最好使用进程名称,

App.xaml 的交互逻辑

'

    [DllImport("user32.dll")]
    public static extern bool ShowWindowAsync(HandleRef hWnd, int nCmdShow);
    [DllImport("user32.dll")]
    public static extern bool SetForegroundWindow(IntPtr WindowHandle);
    public const int SW_RESTORE = 9;
    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)
        {                
            Process process = Process.GetProcessesByName(proc.ProcessName).FirstOrDefault();
            IntPtr hWnd = IntPtr.Zero;
            hWnd = process.MainWindowHandle;
            ShowWindowAsync(new HandleRef(null, hWnd), SW_RESTORE);
            SetForegroundWindow(process.MainWindowHandle);
            App.Current.Shutdown();
        }
    }

'