|
| 1 | +--- |
| 2 | +description: Describes how to use app instancing features with the app lifecycle API in WinUI with C# and the Windows App SDK. |
| 3 | +title: How to create a single-instanced WinUI app with C# |
| 4 | +ms.topic: how-to |
| 5 | +ms.date: 09/20/2024 |
| 6 | +keywords: AppLifecycle, Windows, ApplicationModel, instancing, single instance, multi instance, winui, windows app sdk, c# |
| 7 | +#customer intent: As a Windows developer, I want to learn how to create a single-instanced WinUI 3 app so that I can ensure only one instance of my app is running at a time. |
| 8 | +--- |
| 9 | + |
| 10 | +# Create a single-instanced WinUI app with C# |
| 11 | + |
| 12 | +This how-to demonstrates how to create a single-instanced WinUI 3 app with C# and the Windows App SDK. Single-instanced apps only allow one instance of the app running at a time. WinUI apps are multi-instanced by default. They allow you to launch multiple instances of the same app simultaneously. That's referred to a multiple instances. However, you may want to implement single-instancing based on the use case of your app. Attempting to launch a second instance of a single-instanced app will only result in the first instance’s main window being activated instead. This tutorial demonstrates how to implement single-instancing in a WinUI app. |
| 13 | + |
| 14 | +In this article, you will learn how to: |
| 15 | + |
| 16 | +> [!div class="checklist"] |
| 17 | +> - Turn off XAML's generated `Program` code |
| 18 | +> - Define customized `Main` method for redirection |
| 19 | +> - Test single-instancing after app deployment |
| 20 | +
|
| 21 | +## Pre-requisites |
| 22 | + |
| 23 | +This tutorial uses Visual Studio and builds on the WinUI blank app template. If you're new to WinUI development, you can get set up by following the instructions in [Get started with WinUI](../../get-started/start-here.md). There you'll install Visual Studio, configure it for developing apps with WinUI while ensuring you have the latest version of WinUI and the Windows App SDK, and create a Hello World project. |
| 24 | + |
| 25 | +When you've done that, come back here to learn how to turn your "Hello World" project into a single-instanced app. |
| 26 | + |
| 27 | +> [!NOTE] |
| 28 | +> This how-to is based on the [Making the app single-instanced (Part 3)](https://blogs.windows.com/windowsdeveloper/2022/01/28/making-the-app-single-instanced-part-3/) blog post from a Windows blog series on WinUI 3. The code for those articles is available on [GitHub](https://github.com/jingwei-a-zhang/WinAppSDK-DrumPad). |
| 29 | +
|
| 30 | +## Disable auto-generated Program code |
| 31 | + |
| 32 | +We need to check for redirection as early as possible, before creating any windows. To do this, we must define the symbol “DISABLE_XAML_GENERATED_MAIN” in the project file. Follow these steps to disable the auto-generated Program code: |
| 33 | + |
| 34 | +1. Right-click on the project name in Solution Explorer and select **Edit Project File**. |
| 35 | +1. Define the **DISABLE_XAML_GENERATED_MAIN** symbol for each configuration and platform. Add the following XML to the project file: |
| 36 | + |
| 37 | + ```xml |
| 38 | + <propertygroup condition="'$(Configuration)|$(Platform)'=='Debug|x64'"> |
| 39 | + <defineconstants>DISABLE_XAML_GENERATED_MAIN</defineconstants> |
| 40 | + </propertygroup> |
| 41 | + <propertygroup condition="'$(Configuration)|$(Platform)'=='Debug|x86'"> |
| 42 | + <defineconstants>DISABLE_XAML_GENERATED_MAIN</defineconstants> |
| 43 | + </propertygroup> |
| 44 | + <propertygroup condition="'$(Configuration)|$(Platform)'=='Release|x86'"> |
| 45 | + <defineconstants>DISABLE_XAML_GENERATED_MAIN</defineconstants> |
| 46 | + </propertygroup> |
| 47 | + <propertygroup condition="'$(Configuration)|$(Platform)'=='Release|x64'"> |
| 48 | + <defineconstants>DISABLE_XAML_GENERATED_MAIN</defineconstants> |
| 49 | + </propertygroup> |
| 50 | + <propertygroup condition="'$(Configuration)|$(Platform)'=='Debug|arm64'"> |
| 51 | + <defineconstants>DISABLE_XAML_GENERATED_MAIN</defineconstants> |
| 52 | + </propertygroup> |
| 53 | + <propertygroup condition="'$(Configuration)|$(Platform)'=='Release|arm64'"> |
| 54 | + <defineconstants>DISABLE_XAML_GENERATED_MAIN</defineconstants> |
| 55 | + </propertygroup> |
| 56 | + ``` |
| 57 | + |
| 58 | +Adding the **DISABLE_XAML_GENERATED_MAIN** symbol will disable the auto-generated Program code for your project. |
| 59 | + |
| 60 | +## Define a Program class with a Main method |
| 61 | + |
| 62 | +A customized Program.cs file must be created instead of running the default Main method. The code added to the **Program** class enables the app to check for redirection, which isn't the default behavior of WinUI apps. |
| 63 | + |
| 64 | +1. Navigate to Solution Explorer, right-click on the project name, and select **Add | Class**. |
| 65 | +1. Name the new class `Program.cs` and select **Add**. |
| 66 | +1. Add the following namespaces to the Program class, replacing any existing namespaces: |
| 67 | + |
| 68 | + ```csharp |
| 69 | + using System; |
| 70 | + using System.Diagnostics; |
| 71 | + using System.Runtime.InteropServices; |
| 72 | + using System.Threading; |
| 73 | + using System.Threading.Tasks; |
| 74 | + using Microsoft.UI.Dispatching; |
| 75 | + using Microsoft.UI.Xaml; |
| 76 | + using Microsoft.Windows.AppLifecycle; |
| 77 | + ``` |
| 78 | + |
| 79 | +1. Replace the empty **Program** class with the following: |
| 80 | + |
| 81 | + ```csharp |
| 82 | + public class Program |
| 83 | + { |
| 84 | + [STAThread] |
| 85 | + static int Main(string[] args) |
| 86 | + { |
| 87 | + WinRT.ComWrappersSupport.InitializeComWrappers(); |
| 88 | + bool isRedirect = DecideRedirection(); |
| 89 | + |
| 90 | + if (!isRedirect) |
| 91 | + { |
| 92 | + Application.Start((p) => |
| 93 | + { |
| 94 | + var context = new DispatcherQueueSynchronizationContext( |
| 95 | + DispatcherQueue.GetForCurrentThread()); |
| 96 | + SynchronizationContext.SetSynchronizationContext(context); |
| 97 | + _ = new App(); |
| 98 | + }); |
| 99 | + } |
| 100 | + |
| 101 | + return 0; |
| 102 | + } |
| 103 | + } |
| 104 | + ``` |
| 105 | + |
| 106 | + The **Main** method determines whether the app should redirect to the first instance or start a new instance after calling **DecideRedirection**, which we will define next. |
| 107 | + |
| 108 | +1. Define the **DecideRedirection** method below the **Main** method: |
| 109 | + |
| 110 | + ```csharp |
| 111 | + private static bool DecideRedirection() |
| 112 | + { |
| 113 | + bool isRedirect = false; |
| 114 | + AppActivationArguments args = AppInstance.GetCurrent().GetActivatedEventArgs(); |
| 115 | + ExtendedActivationKind kind = args.Kind; |
| 116 | + AppInstance keyInstance = AppInstance.FindOrRegisterForKey("MySingleInstanceApp"); |
| 117 | + |
| 118 | + if (keyInstance.IsCurrent) |
| 119 | + { |
| 120 | + keyInstance.Activated += OnActivated; |
| 121 | + } |
| 122 | + else |
| 123 | + { |
| 124 | + isRedirect = true; |
| 125 | + RedirectActivationTo(args, keyInstance); |
| 126 | + } |
| 127 | + |
| 128 | + return isRedirect; |
| 129 | + } |
| 130 | + ``` |
| 131 | + |
| 132 | + **DecideRedirection** determines if the app has been registered by registering a unique key that represents your app instance. Based on the result of key registration, it can determine if there's a current instance of the app running. After making the determination, the method knows whether to redirect or allow the app to continue launching the new instance. The **RedirectActivationTo** method is called if redirection is necessary. |
| 133 | + |
| 134 | +1. Next, let's create the RedirectActivationTo method below the DecideRedirection method, along with the required DllImport statements. Add the following code to the Program class: |
| 135 | + |
| 136 | + ```csharp |
| 137 | + [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] |
| 138 | + private static extern IntPtr CreateEvent( |
| 139 | + IntPtr lpEventAttributes, bool bManualReset, |
| 140 | + bool bInitialState, string lpName); |
| 141 | + |
| 142 | + [DllImport("kernel32.dll")] |
| 143 | + private static extern bool SetEvent(IntPtr hEvent); |
| 144 | + |
| 145 | + [DllImport("ole32.dll")] |
| 146 | + private static extern uint CoWaitForMultipleObjects( |
| 147 | + uint dwFlags, uint dwMilliseconds, ulong nHandles, |
| 148 | + IntPtr[] pHandles, out uint dwIndex); |
| 149 | + |
| 150 | + [DllImport("user32.dll")] |
| 151 | + static extern bool SetForegroundWindow(IntPtr hWnd); |
| 152 | + |
| 153 | + private static IntPtr redirectEventHandle = IntPtr.Zero; |
| 154 | + |
| 155 | + // Do the redirection on another thread, and use a non-blocking |
| 156 | + // wait method to wait for the redirection to complete. |
| 157 | + public static void RedirectActivationTo(AppActivationArguments args, |
| 158 | + AppInstance keyInstance) |
| 159 | + { |
| 160 | + redirectEventHandle = CreateEvent(IntPtr.Zero, true, false, null); |
| 161 | + Task.Run(() => |
| 162 | + { |
| 163 | + keyInstance.RedirectActivationToAsync(args).AsTask().Wait(); |
| 164 | + SetEvent(redirectEventHandle); |
| 165 | + }); |
| 166 | + |
| 167 | + uint CWMO_DEFAULT = 0; |
| 168 | + uint INFINITE = 0xFFFFFFFF; |
| 169 | + _ = CoWaitForMultipleObjects( |
| 170 | + CWMO_DEFAULT, INFINITE, 1, |
| 171 | + [redirectEventHandle], out uint handleIndex); |
| 172 | + |
| 173 | + // Bring the window to the foreground |
| 174 | + Process process = Process.GetProcessById((int)keyInstance.ProcessId); |
| 175 | + SetForegroundWindow(process.MainWindowHandle); |
| 176 | + } |
| 177 | + ``` |
| 178 | + |
| 179 | + The **RedirectActivationTo** method is responsible for redirecting the activation to the first instance of the app. It creates an event handle, starts a new thread to redirect the activation, and waits for the redirection to complete. After the redirection is complete, the method brings the window to the foreground. |
| 180 | + |
| 181 | +1. Finally, define the helper method **OnActivated** below the **DecideRedirection** method: |
| 182 | + |
| 183 | + ```csharp |
| 184 | + private static void OnActivated(object sender, AppActivationArguments args) |
| 185 | + { |
| 186 | + ExtendedActivationKind kind = args.Kind; |
| 187 | + } |
| 188 | + ``` |
| 189 | + |
| 190 | +## Test single-instancing via app deployment |
| 191 | + |
| 192 | +Until this point, we've been testing the app by debugging within Visual Studio. However, we can only have one debugger running at once. This limitation prevents us from knowing whether the app is single-instanced because we can’t debug the same project twice at the same time. For an accurate test, we'll deploy the application to our local Windows client. After deploying, we can launch the app from the desktop like you would with any app installed on Windows. |
| 193 | + |
| 194 | +1. Navigate to Solution Explorer, right-click on the project name, and select **Deploy**. |
| 195 | +1. Open the Start menu and click into the search field. |
| 196 | +1. Type your app's name in the search field. |
| 197 | +1. Click the app icon from the search result to launch your app. |
| 198 | + |
| 199 | + > [!NOTE] |
| 200 | + > If you experience app crashes in release mode, there are some known issues with trimmed apps in the Windows App SDK. You can disable trimming in the project by setting the **PublishTrimmed** property to **false** for all build configurations in your project's `.pubxml` files. For more information, see [this issue](https://github.com/microsoft/microsoft-ui-xaml/issues/9914#issuecomment-2303010651) on GitHub. |
| 201 | +
|
| 202 | +1. Repeat steps 2 to 4 to launch the same app again and see if another instance opens. If the app is single-instanced, the first instance will be activated instead of a new instance opening. |
| 203 | + |
| 204 | + > [!TIP] |
| 205 | + > You can optionally add some logging code to the **OnActivated** method to verify that the existing instance has been activated. |
| 206 | +
|
| 207 | +## Summary |
| 208 | + |
| 209 | +All the code covered here is on [GitHub](https://github.com/jingwei-a-zhang/WinAppSDK-DrumPad), with branches for the different steps in the original [Windows blog series](https://blogs.windows.com/windowsdeveloper/2022/01/28/making-the-app-single-instanced-part-3/). See the [single-instancing](https://github.com/jingwei-a-zhang/WinAppSDK-DrumPad/tree/single-instancing) branch for code specific to this how-to. The `main` branch is the most comprehensive. The other branches are intended to show you how the app architecture evolved. |
| 210 | + |
| 211 | +## Related content |
| 212 | + |
| 213 | +[App instancing with the app lifecycle API](applifecycle-instancing.md) |
| 214 | + |
| 215 | +[Making the app single-instanced (Part 3)](https://blogs.windows.com/windowsdeveloper/2022/01/28/making-the-app-single-instanced-part-3/) |
| 216 | + |
| 217 | +[WinAppSDK-DrumPad sample on GitHub](https://github.com/jingwei-a-zhang/WinAppSDK-DrumPad) |
0 commit comments