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).
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:
First, you must tell MSBuild not to create the default `Program.cs` file.
<!-- This disables the auto-generated Program.cs -->
<DefineConstants>$(DefineConstants);DISABLE_XAML_GENERATED_MAIN</DefineConstants>
Now, you must create your own entry point.
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);
});
}
}
}
The `Program.cs` code needs a static way to access your `MainWindow` to get its `DispatcherQueue`.
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().
Finally, you must tell Windows that your app can handle a specific URI scheme (e.g., `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>
You are now done. To test:
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.