Master Guide: WinUI 3 Single-Instance & Protocol Activation

A comprehensive, step-by-step walkthrough for creating a single-instanced WinUI 3 desktop app with redirection and protocol argument handling using the Windows App SDK (late 2025).

Introduction: The Modern Approach

The original 2022 blog posts have been superseded by a more robust model using the Microsoft.Windows.AppLifecycle.AppInstance class. This guide uses that modern API. The entire process hinges on creating a custom entry point for your application (`Program.cs`) where you can check for other running instances *before* any windows are created.

The flow is:

Step 1: Disable the Auto-Generated Entry Point

First, you must tell MSBuild not to create the default `Program.cs` file.

  1. Right-click your WinUI 3 project in Visual Studio and select Edit Project File.
  2. Inside the first <PropertyGroup>, add this line:
<!-- This disables the auto-generated Program.cs -->
<DefineConstants>$(DefineConstants);DISABLE_XAML_GENERATED_MAIN</DefineConstants>

Step 2: Create a Custom Program.cs

Now, you must create your own entry point.

  1. Right-click your project and select Add > New Item.
  2. Choose Class and name it Program.cs.
  3. Replace the contents of `Program.cs` with the code below. This is the core logic that will handle instancing and redirection.
using Microsoft.UI.Xaml;
using Microsoft.Windows.AppLifecycle;
using System;
using System.Linq;
using System.Threading.Tasks;

namespace YourAppNamespace; // <-- CHANGE THIS to your app's namespace

public static class Program
{
    // Define a unique key for your app instance
    public const string AppInstanceKey = "my-unique-app-key-12345";

    [STAThread] // Must be single-threaded
    static async Task Main(string[] args)
    {
        // Initialize WinRT (required)
        WinRT.ComWrappersSupport.InitializeComWrappers();

        // Check if we are being activated by a protocol
        AppActivationArguments activationArgs = AppInstance.GetCurrent().GetActivatedEventArgs();

        // 1. Find or register the main instance
        AppInstance mainInstance = AppInstance.FindOrRegisterForKey(AppInstanceKey);

        // 2. If this is the main instance...
        if (mainInstance.IsCurrent)
        {
            // ...register to handle future activations (from other instances)
            mainInstance.Activated += OnAppActivated;

            // Run the app's OnLaunched code
            // We pass the args here, but OnLaunched will also get them.
            // This is just to ensure the first launch works correctly.
            StartApp(activationArgs);
        }
        // 3. If this is NOT the main instance...
        else
        {
            // ...redirect the activation to the main instance and exit.
            // This sends our activationArgs (e.g., the protocol URI) to the
            // 'OnAppActivated' handler in the main instance.
            await mainInstance.RedirectActivationToAsync(activationArgs);
            Environment.Exit(0);
        }
    }

    private static void StartApp(AppActivationArguments args)
    {
        // This is the standard WinUI 3 startup code
        Application.Start((p) =>
        {
            var context = new DispatcherQueueSynchronizationContext(
                Microsoft.UI.Dispatching.DispatcherQueue.GetForCurrentThread());
            SynchronizationContext.SetSynchronizationContext(context);

            // This is your standard App.xaml.cs class
            new App();
        });
    }

    // This handler runs in the *main instance* when a *new instance* redirects to it
    private static void OnAppActivated(object sender, AppActivationArguments args)
    {
        // This event is NOT on the UI thread.
        // We must use the DispatcherQueue to safely update the UI.

        if (App.MainWindow != null) // App.MainWindow must be set in App.xaml.cs
        {
            App.MainWindow.DispatcherQueue.TryEnqueue(() =>
            {
                // Call a method on your App or MainWindow to handle the args
                (Application.Current as App)?.HandleActivation(args);
            });
        }
    }
}

Step 3: Update App.xaml.cs to Store the Window

The `Program.cs` code needs a static way to access your `MainWindow` to get its `DispatcherQueue`.

  1. Open your App.xaml.cs file.
  2. Add a static `MainWindow` property.
  3. In `OnLaunched`, *set* this static property.
  4. Create the `HandleActivation` method that `Program.cs` will call.
using Microsoft.UI.Xaml;
using Microsoft.Windows.AppLifecycle;
using System;
using Windows.ApplicationModel.Activation; // For ProtocolActivatedEventArgs

namespace YourAppNamespace; // <-- CHANGE THIS

public partial class App : Application
{
    // 1. Static property to hold the main window
    public static WindowEx MainWindow { get; private set; } // Use WindowEx or your MainWindow type

    private Window m_window;

    public App()
    {
        this.InitializeComponent();
    }

    protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
    {
        m_window = new MainWindow(); // Or your main window class

        // 2. Set the static property
        MainWindow = m_window as WindowEx; // Cast to your window type if needed

        m_window.Activate();

        // 3. Handle the *first* activation (optional, but good practice)
        // This handles the case where the app is launched *for the first time*
        // via a protocol, before the `Activated` event is registered.
        var activationArgs = AppInstance.GetCurrent().GetActivatedEventArgs();
        HandleActivation(activationArgs);
    }

    // 4. This is the method our Program.cs will call!
    public void HandleActivation(AppActivationArguments args)
    {
        // Bring the window to the foreground
        if (MainWindow != null)
        {
            MainWindow.Activate();
            // You can also use other methods to bring it to focus, e.g.:
            // WindowEx.BringToFront();
        }

        // Check what kind of activation it is
        if (args.Kind == ExtendedActivationKind.Protocol)
        {
            // This is the magic!
            // Cast the Data to the correct *Windows* (not Microsoft.UI) type
            var protocolArgs = args.Data as Windows.ApplicationModel.Activation.IProtocolActivatedEventArgs;

            if (protocolArgs != null)
            {
                // We now have the URI!
                Uri uri = protocolArgs.Uri;
                string rawUri = uri.AbsoluteUri;

                // TODO: Process the URI!
                // e.g., pass this to a ViewModel or log it
                System.Diagnostics.Debug.WriteLine($"[ACTIVATION] Protocol URI received: {rawUri}");

                // Example: Navigate your app or show the args
                if (MainWindow.Content is MainPage mainPage)
                {
                    mainPage.ShowActivationMessage($"Activated with URI: {rawUri}");
                }
            }
        }
        else if (args.Kind == ExtendedActivationKind.Launch)
        {
            // Standard launch
            System.Diagnostics.Debug.WriteLine("[ACTIVATION] Standard launch.");
        }
        // You can add more handlers here for File activation, etc.
    }
}

// NOTE: You'll need a simple WindowEx class or similar if you want BringToFront
// You can get this from the Windows App SDK samples or community toolkits.
// For simplicity, this guide just uses m_window.Activate().
Bonus Points: The DispatcherQueue
Notice the `OnAppActivated` in `Program.cs`. This event fires on a background thread. Trying to access `MainWindow` or update any UI from there will crash your app.

The solution is `App.MainWindow.DispatcherQueue.TryEnqueue(...)`. This is the "bonus point" scenario you mentioned. It safely marshals the call (and the `args`) back to the main UI thread, where it's safe to call `MainWindow.Activate()` and pass the URI to your UI.

Step 4: Register the Protocol URI Scheme

Finally, you must tell Windows that your app can handle a specific URI scheme (e.g., `my-app-scheme://`).

  1. This is easiest if your app is Packaged (e.g., in an MSIX).
  2. Open your Package.appxmanifest file.
  3. Go to the Declarations tab.
  4. From the "Available Declarations" dropdown, select Protocol and click Add.
  5. In the "Properties" pane, set the Name. This is your scheme. For example: `my-app-scheme`.

This adds the following to your manifest XML:

<Applications>
  <Application ...>
    <Extensions>
      <uap:Extension Category="windows.protocol">
        <uap:Protocol Name="my-app-scheme" />
      <uap:Extension>
    </Extensions>
    ...
  </Application>
</Applications>
For Unpackaged Apps: If your app is unpackaged, you must write this information directly to the Windows Registry during your app's installation process. This is more complex and typically handled by an installer like Inno Setup or WiX. The registry keys are created under `HKEY_CLASSES_ROOT\my-app-scheme`.

Conclusion & Testing

You are now done. To test:

  1. Run your app from Visual Studio. This is now the "main instance."
  2. Press Win + R to open the Run dialog.
  3. Type your protocol URI, including some test arguments: `my-app-scheme://open/document?id=123`
  4. Press Enter.

You should see a *new process* for your app briefly appear in Task Manager, then immediately disappear (this is the redirection). Your *original* app window will then pop to the foreground, and your Debug Output window in Visual Studio should log the message:

[ACTIVATION] Protocol URI received: my-app-scheme://open/document?id=123

You have successfully created a single-instance WinUI 3 app that can handle redirected URI activations with arguments.