TLDR; Issues addressed
- Make WinUI 3 single instanced
- Activate main window on additional launches of a single instance application
Current state of single instance support in WinUI 3
The SwiftUI application of our time tracker was single instance by default: if you launch it the second time, it brings its window to the foreground, but does not open another instance of the application.
In WinUI 3 the default behavior is that it opens a second instance. This is not ideal as the database file should not be accessed from multiple applications at the same time. To prevent corrupting the data, we can set the WinUI 3 application to single instanced in a few steps.
There is an official tutorial on the Windows Blogs which explains the steps, and also a Program.cs on the WindowsAppSDK-Samples GitHub repository which shows an example implementation. On the other hand, I needed to adjust a few things to have it running, so I will describe the steps that I collected from the different sources.
It’s also worth mentioning that there is an open ticket on the WindowsAppSDK repository and one on the .NET MAUI repository to make this setup much easier, for example it could be just one property in the project file. Before moving forward, you can check if those tickets were already resolved.
Make WinUI 3 single instanced
By default, WinUI 3 generates a Main function in the App class (to be precise, it’s in the generated App.g.i.cs file). This can be deactivated by setting the DISABLE_XAML_GENERATED_MAIN macro. This can be done by adding the following lines in your project file:
<Project Sdk="Microsoft.NET.Sdk">
<!-- ...Rest of your project file... -->
<!-- Setting the application single instanced by creating a custom entry point. -->
<PropertyGroup>
<DefineConstants>DISABLE_XAML_GENERATED_MAIN</DefineConstants>
</PropertyGroup>
</Project>
Note that you can also define this constant per configuration level, see the official tutorial. As for our application we only have 64-bit dependencies, we can only use the x64 configuration, so it doesn’t make a difference.
Then we have to create our custom Main function that will check if our application is already running, and in this case redirect instead of creating a new instance. In the tutorial you could see that they create an async function for this, but according to breenbob who created one of the earlier mentioned tickets this can lead to instance crashes, so better to go with a synchronized approach. We can delegate the asynchronization to the redirection part, like it is done in the Program.cs example.
public class Program
{
[STAThread]
static int Main(string[] args)
{
ComWrappersSupport.InitializeComWrappers();
bool isRedirect = DecideRedirection();
if (!isRedirect)
{
Microsoft.UI.Xaml.Application.Start((p) =>
{
var context = new DispatcherQueueSynchronizationContext(
DispatcherQueue.GetForCurrentThread());
SynchronizationContext.SetSynchronizationContext(context);
new App();
});
}
return 0;
}
private static bool DecideRedirection()
{
bool isRedirect = false;
AppActivationArguments args = AppInstance.GetCurrent().GetActivatedEventArgs();
ExtendedActivationKind kind = args.Kind;
AppInstance keyInstance = AppInstance.FindOrRegisterForKey("ARBITRARY-KEY-THAT-IDENTIFIES-YOUR-APP");
if (keyInstance.IsCurrent)
{
keyInstance.Activated += OnActivated;
}
else
{
isRedirect = true;
RedirectActivationTo(args, keyInstance);
}
return isRedirect;
}
// Do the redirection on another thread, and use a non-blocking
// wait method to wait for the redirection to complete.
public static void RedirectActivationTo(
AppActivationArguments args, AppInstance keyInstance)
{
var redirectSemaphore = new Semaphore(0, 1);
Task.Run(() =>
{
keyInstance.RedirectActivationToAsync(args).AsTask().Wait();
redirectSemaphore.Release();
});
redirectSemaphore.WaitOne();
}
private static void OnActivated(object sender, AppActivationArguments args)
{
// TODO Handle when application is reactivated
}
}
Activate main window on additional launches of a single instance application
In the previous section we have added the OnActivated function, which is called when the instance is launched on a second time. As mentioned in the first section, we would like to have the same behavior as in the SwiftUI implementation: bring the main window to the foreground.
For this I had to slightly modify the OnActivated function of the time tracker window: it used to only focus on the input field, now we ensure that the window is open.
public sealed partial class MainWindow : Window
{
// ... rest of the class
private void OnActivated(object sender, WindowActivatedEventArgs eventArgs)
{
if (eventArgs.WindowActivationState == WindowActivationState.Deactivated)
{
HideWindow();
}
else
{
ShowWindow();
input.Focus(FocusState.Programmatic);
}
}
private void HideWindow()
{
PInvoke.ShowWindow(Hwnd, Windows.Win32.UI.WindowsAndMessaging.SHOW_WINDOW_CMD.SW_HIDE);
}
private void ShowWindow()
{
PInvoke.ShowWindow(Hwnd, Windows.Win32.UI.WindowsAndMessaging.SHOW_WINDOW_CMD.SW_SHOW);
PInvoke.SetForegroundWindow(Hwnd);
PInvoke.SetActiveWindow(Hwnd);
}
// ... rest of the class
}
Now we have to forward the Activate event to the MainWindow when our Program gets activated. For this my first idea was to expose the reference from the App class and just call Activate(), but this ended up with the following exception (note that if your application crashes, you can open the Windows Event Log to look up the exception):
System.Runtime.InteropServices.COMException(0x8001010E): The application called an interface that was marshalled for a different thread. (Exception from HRESULT: 0x8001010E (RPC_E_WRONG_THREAD))
This means that I can’t activate the window from the thread that handles the OnActivated event for the Program class. Luckily there is a DispatcherQueue where we can queue tasks to be executed on the UI thread:
DispatcherQueue.GetForCurrentThread();
On the other hand, this returns null on our event handler thread. The solution is to delegate the activation to the App class which is able to store the instance of the DispatcherQueue.
public partial class App : Application
{
private Window m_window;
private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread();
// ... rest of the class, e.g. storing reference for m_window
public void ActivateWindow()
{
_dispatcherQueue.TryEnqueue(() => { m_window.Activate(); });
}
}
Then we can call the ActivateWindow function in the OnActivated event handler of the Program class:
public class Program
{
// ... rest of the class
private static void OnActivated(object sender, AppActivationArguments args)
{
if (App.Current is App thisApp)
{
thisApp.ActivateWindow();
}
}
}
Now if the application is relaunched, it will explicitly activate our main window.