microsoft / microsoft-ui-xaml

Windows UI Library: the latest Windows 10 native controls and Fluent styles for your applications
MIT License
6.28k stars 675 forks source link

Proposal: Single instance WinUI desktop apps #4780

Open JaiganeshKumaran opened 3 years ago

JaiganeshKumaran commented 3 years ago

Proposal: Single instance WinUI desktop apps

Unlike UWP apps which are single instance and for multiple instances need to be enabled manually, Win32 apps are multi instance by default. While this is fine for most apps, some apps particularly need single instancing and today WinUI desktop doesn't provide a way to do that easily.

Summary

Add a property named SingleInstance in the App class. If it's true then new instances of the process will bring the existing window on to the foreground instead of showing a message that existing instance is running like some old desktop apps and not bother to switch and the newly created process should be terminated. One problem here is if the same instance has multiple windows open, you need rightly open the main window and not a secondary window created by the app. A shared mutex could be used to know whether an existing instance is there or not however there's no way to know which window to switch in case of multiple windows unless the developer does it manually themselves as WinUI doesn't know.

Rationale

Scope

Capability Priority
This proposal will allow developers to make their WinUI desktop single instance Must

Important Notes

I added this code under App's constructor in my app to make it single instance, but it probably won't work with apps that have multiple windows in the same instance.

auto existingWindow = FindWindow(L"WinUIDesktopWin32WindowClass", L"Develop");
if (existingWindow != nullptr)
{
    ShowWindow(existingWindow, SW_RESTORE);
    SetForegroundWindow(existingWindow);
    ExitProcess(0);
}
JaiganeshKumaran commented 3 years ago

See also https://github.com/microsoft/ProjectReunion/issues/111

JaiganeshKumaran commented 3 years ago

The problem with this is that the main process doesn't know it's window has been activated through a new process. It should be possible but then you can't use the same OnLaunched method and instead a custom one. You can't use SendMessage for example since WinUI doesn't provide a way to let us handle the window messages.

sjb-sjb commented 9 months ago

For those of you who land here looking for how to single-instance your WinUI 3 app, here is what I put together after cobbling together bits and pieces from other posts.

public static class ApplicationInstancing
{
    #region TryRedirection

    /// <summary>
    /// If there is already an instance registered with the given key, then redirect 
    /// execution to that instance and return true. 
    /// Otherwise, register this instance with the key and return false. 
    /// If a redirection occurs, then <c>Application.Current</c> is searched for an 
    /// method <c>void OnRedirection( AppActivationArguments e)</c>, and if found then 
    /// it will be called. The args used will be the arguments from this instance's activation.
    /// Typically, <c>OnRedirection</c> should activate the window and set it as foreground. 
    /// </summary>
    ///
    /// <remarks>
    /// In order to achieve single-instancing of your WinUI 3 app, 
    /// define DISABLE_XAML_GENERATED_MAIN in your application's 
    /// Project > Properties > Build > General > Conditional 
    /// compilation symbols. Then create a class in your app as follows: 
    /// <code>
    /// static class Program { 
    ///     [STAThread]
    ///     static void Main(string[] args) { 
    ///         string key = ...;
    ///         if (ApplicationInstancing.TryRedirection( key)) { return; }
    ///         ApplicationInstancing.Main&lt;App&gt;();
    ///     }
    /// }
    /// </code>
    /// 
    /// Define the <c>key</c> to be the string that uniquely identifies the 
    /// running instance of your app to redirect to. For example, this could 
    /// simply be the name of the application. Or it could include a file 
    /// name if you want to have one instance per filename, for example. 
    /// 
    /// Here, <c>App</c> is the <c>Application</c>-derived class that defines 
    /// your application.
    /// It must have a constructor that takes no arguments. 
    /// 
    /// Note, one cannot declare Main to be async, because in WinUI this prevents
    /// Narrator from reading XAML elements. 
    /// </remarks>
    /// <param name="key">The string that uniquely identifies the instance.</param>
    /// <returns></returns>
    public static bool TryRedirection( string key)
    {
        AppInstance registeredInstance = AppInstance.FindOrRegisterForKey( key);
        if (registeredInstance.IsCurrent) {
            registeredInstance.Activated += RegisteredInstance_Activated;
            return false;
        } else { 
            AppActivationArguments activationArgs = AppInstance.GetCurrent().GetActivatedEventArgs(); // was: registerdInstance.GetActivatedEventArgs()
            var redirectSemaphore = new Semaphore(0, 1);
            Action redirectAndRelease = () => {
                registeredInstance.RedirectActivationToAsync(activationArgs).AsTask().Wait();
                redirectSemaphore.Release();
            };
            Task.Run(redirectAndRelease); // on thread pool.
            redirectSemaphore.WaitOne();
            return true;
        }
    }

    private static void RegisteredInstance_Activated(object? sender, AppActivationArguments e)
    {
        dynamic app = Application.Current;
        try {
            app.OnRedirection(e);
        } catch (Exception) {
        }
    }

    #endregion

    #region Main

    [global::System.Runtime.InteropServices.DllImport("Microsoft.ui.xaml.dll")]
    private static extern void XamlCheckProcessRequirements();

    /// <summary>
    /// The main function  of a WinUI application. 
    /// 
    /// This does the same thing as the <c>Main( string[] args)</c> method 
    /// generated by Visual Studio for a WinUI 3 application. 
    /// In order to suppress the automatic generation by Visual Studio,
    /// define DISABLE_XAML_GENERATED_MAIN in your application's 
    /// Project > Properties > Build > General > Conditional 
    /// compilation symbols.      
    /// </summary>
    public static void Main<TApp>() where TApp: Application, new()
    {
        XamlCheckProcessRequirements();
        ComWrappersSupport.InitializeComWrappers();
        ApplicationInitializationCallback initializer = (p) => {
            var context = new DispatcherQueueSynchronizationContext( DispatcherQueue.GetForCurrentThread());
            SynchronizationContext.SetSynchronizationContext(context);
            new TApp();
        };
        Application.Start(initializer);
    }
}

#endregion

A related task is raising the window of the currently-running instance of your app so that it is on top of other windows. Just calling Window.Activate will not do it, as discussed in https://github.com/microsoft/microsoft-ui-xaml/issues/7595 . Here's how I did it. This method must be called on the UI thread.

    static internal void ActivateAndSetAsForeground(this Window window)
    {
        [DllImport("user32.dll")]
        [return: MarshalAs(UnmanagedType.Bool)]
        static extern bool SetForegroundWindow(IntPtr hWnd);

        IntPtr hWnd = WinRT.Interop.WindowNative.GetWindowHandle(window);

        window.Activate();
        bool didSet = SetForegroundWindow(hWnd);
        Debug.Assert(didSet);
    }
sjb-sjb commented 9 months ago

Comment, about the question of which window to bring to the top, this could be addressed simply by adding a virtual OnRedirection method to Application which does nothing and which the app developer can override to brong to top whichever window they desire.