This post is based on Using .NET build-in dependency injection with WinUI apps and represents work on the mistakes made in the design of the approach described there.
Too complicated. I realized this after using this approach in several applications. This approach also leads to some interesting (and hard-to-find) bugs. So let’s start from scratch.
You still need to control the initialization and shutdown of the application, so you need to add the following lines to the project file:
<PropertyGroup>
<!-- Use our own Main entry point so we can control the initialization and shutdown of the application -->
<DefineConstants>DISABLE_XAML_GENERATED_MAIN</DefineConstants>
<!-- We use App class to place Main method -->
<StartupObject>DependencyInjection.App</StartupObject>
</PropertyGroup>
This control is necessary for us to properly shut down the services in the host. When the application starts, a host object is created. In this host, all the services required for the application to work are registered. After the application ends (even in case of an error), the services are properly shut down after calling the Dispose
method of the IHost
interface.
// Note that the Main method has the [STAThread] attribute.
[STAThread]
public static void Main(string[] args)
{
WinRT.ComWrappersSupport.InitializeComWrappers();
try
{
Start(_ =>
{
var context = new DispatcherQueueSynchronizationContext(DispatcherQueue.GetForCurrentThread());
SynchronizationContext.SetSynchronizationContext(context);
var app = new App();
app.UnhandledException += (_, _) => StopHost();
host = CreateHost(app);
});
}
finally
{
StopHost();
}
}
private static IHost CreateHost(IApp app)
{
var builder = Host.CreateApplicationBuilder();
builder.Services.AddSingleton<IApp>(app);
builder.Services.AddSingleton<MainWindow>();
return builder.Build();
}
private static void StopHost()
{
host?.Dispose();
}
private static IHost? host;
In our example, the application class itself and the main window are registered as services. The IApp
interface consists of just one method - Exit
:
public interface IApp
{
void Exit();
}
Now we can add a parameterized constructor to MainWindow
to receive an instance of the App
class. Since MainWindow
is also registered as a service, the instance of the App class will be automatically provided.
private readonly IApp app;
public MainWindow(IApp app)
{
this.app = app;
this.InitializeComponent();
}
After starting the application, the OnLaunched
method will be called as usual. There, you can initialize the MainWindow class and open the application window as usual:
protected override void OnLaunched(LaunchActivatedEventArgs args)
{
// The host should already be created by this time.
if (host == null)
{
throw new InvalidOperationException();
}
base.OnLaunched(args);
mainWindow = host.Services.GetRequiredService<MainWindow>();
mainWindow.Closed += OnMainWindowClosed;
mainWindow.AppWindow.Show(true);
}
That’s all you need for a basic implementation of DI (Dependency Injection) in a WinUI application. Much simpler than last time.
Now let’s look at some implementation details.
It’s important to understand that the current implementation does not support IHostedService
(since the host is not started). To fix this, you need to:
- Add the startup code
- Add the shutdown code
You can start the host in the OnLaunched
method. Although the method is called RunAsync
, it will run as long as the host is active. For this reason, I simply ignore it (hopefully, this isn’t a bad practice):
mainWindow.AppWindow.Show(true);
_ = host.RunAsync();
It’s important to note that here we ignore the moment when the host shuts down. In theory, we should wait for the task to complete and then call Exit
on the application. Here, however, we assume that only the application can shut down the host.
It’s best to stop the host right before destroying it. In this case, we need to wait for StopAsync to complete before destroying the host object:
private static void StopHost()
{
host?.StopAsync().Wait();
host?.Dispose();
}
One downside of this approach is that we don’t know exactly when all services will be started. For background tasks, this doesn’t seem very critical (as far as I can tell). In other cases, this needs closer attention.
And finally, I’ll mention a small nuance about unpackaged apps.
In theory, if a user wants to open a file using the application (via the “Open With” menu), the path will be passed through the LaunchActivatedEventArgs
parameter of the OnLaunched
method. However, this doesn’t work for unpackaged apps.
And here, the static Main
method comes to the rescue! All we need to do is pass the args
array to the App
class so it can process it correctly. For example, we pass this array through the constructor and handle it in the OnLaunched
method.
The source code available here.