WPF Single Instance Best Practices

2019-01-16 03:10发布

This is the code I implemented so far to create a single instance WPF application:

#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
    }
}

Everything works perfectly... but I have some doubts about it and I would like to receive your suggestions about how my approach could be improved.

1) I was asked by Code Analysis to implement IDisposable interface because I was using IDisposable members (the Mutex). Is my Dispose() implementation good enough? Should I avoid it because it's never going to be called?

2) It's better to use m_Mutex = new Mutex(true, mutexName, out mutexCreated); and check for the result or to use m_Mutex = new Mutex(false, mutexName); and then check for m_Mutex.WaitOne(TimeSpan.Zero, false); ? In case of multithreading I mean...

3) RegisterWindowMessage API call should return UInt32... but HwndSourceHook is only accepting Int32 as message value... should I be worried about unexpected behaviors (like a result bigger than Int32.MaxValue)?

4) In OnStartup override... should I execute base.OnStartup(e); even if another instance is already running and I'm going to shutdown the application?

5) Is there a better way to bring the existing instance to the top that doesn't need to set Topmost value? Maybe Activate()?

6) Can you see any flaw in my approach? Something concerning multithreading, bad exceptions handling and something like that? For example... what happens if my application crashes between OnStartup and OnExit?

9条回答
劳资没心,怎么记你
2楼-- · 2019-01-16 03:58

I've used a simple TCP socket for this (in Java, 10 years ago).

  1. On startup connect to a predefined port, if the connection is accepted, another instance is running, if not, start a TCP Listener
  2. Once someone connects to you, popup the window and disconnect
查看更多
对你真心纯属浪费
3楼-- · 2019-01-16 03:59

Here is example that brings the old instance to foreground aswell:

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

But it will only work if your main window title do not change durig runtime.

Edit:

You can also use Startup event in App.xaml instead of overriding 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

Remember to not call base.OnStartup(e) in this case!

查看更多
ゆ 、 Hurt°
4楼-- · 2019-01-16 04:01

I wanted to have a bit better user experience - if another instance is already running let's activate it rather than showing an error about the second instance. Here is my implementation.

I use named Mutex for making sure that only one instance is running and named EventWaitHandle to pass notification from one instance to another.

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
}

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

And add Startup="AppOnStartup" (thanks 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>

Works for me :)

查看更多
登录 后发表回答