Modern UI for WPF Autofac based contentloader
I'm not much of a UI guy, but sometimes I have to take part in projects that are front-end heavy. And sometimes this means desktop front-end (there's still a surprisingly heavy demand for these kinds of application).
The application I've been working on lately is riddled with pages and menus and uses navigation features heavily. We wanted the whole thing to be as loosely coupled as possible and we designed our pages and viewmodels to support this requirement.
Mainly for the navigation, we decided to use the Modern UI for WPF library. It is an older component and not maintained anymore, but if one uses WPF, one has to come to terms with using technologies that are not actively maintained anymore :)
Modern UI for WPF makes our job a lot easier and a lot more pleasant. It has a whole navigation subcomponent built-in: you just have to specify a page url (i.e. the relative path of the xaml file) and navigation is done for you.
To support loading the specified links, Modern UI exposes an interface called IContentLoader, which basically calls Application.Load() to load the xaml file. And that's pretty neat for every basic scenario.
Controlling page lifetime
By default, Modern UI caches the page instances and if you navigate to a page more than once, the same instance is used. This feature can be disabled (it's quite poorly documented, though), but it took us quite some time to find it :)
During my search for the mysterious option to disable page caching, it occured to me that we already have a component in the project responsible for object lifecycle management: the Autofac container. So it would be logical to use that for page "caching", if needed.
Luckily, the IContentLoader interface is designed for this specific purpose: hook into the page loading mechanism. The default implementation looks like this:
public class DefaultContentLoader : IContentLoader
{
public Task<object> LoadContentAsync(Uri uri, CancellationToken cancellationToken)
{
if (!Application.Current.Dispatcher.CheckAccess())
{
throw new InvalidOperationException(Resources.UIThreadRequired);
}
var scheduler = TaskScheduler.FromCurrentSynchronizationContext();
return Task.Factory.StartNew(() => LoadContent(uri), cancellationToken, TaskCreationOptions.None, scheduler);
}
protected virtual object LoadContent(Uri uri)
{
if (ModernUIHelper.IsInDesignMode)
{
return null;
}
return Application.LoadComponent(uri);
}
Again luckily, this default implementation provides a way to override just the loading functionality itself, reusing the additional WPF-magic. This is the point where we can hook into the page loading mechanism and use Autofac to do the work.
public class AutofacContentLoader : DefaultContentLoader
{
private readonly ILifetimeScope resolver;
public AutofacContentLoader(ILifetimeScope resolve)
{
this.resolver = resolver;
}
protected override object LoadContent(Uri uri)
{
// resolve from the lifetimescope here
}
}
Using this method we get all the Autofac-goodies in our page-loading mechanism, including lifetime management. Provided we can find a way to resolve the page using only the uri, since that's all we have in the LoadContent method.
To resolve the page from the lifetimescope by uri, we can use the keyed registration feature of Autofac, using the uri as the key. Then resolving can happen using this key like this (PageBase is just my base class for the pages; the fragment part of the uri is removed to do the resolving):
protected override object LoadContent(Uri uri)
{
return resolver.ResolveKeyed<PageBase>(NavigationHelper.RemoveFragment(uri))
}
And finally, it is just the question of registering the pages into the container (and passing the container to the AutofacContentLoader constructor — or any lifetimescope, for that matter):
var builder = new ContainerBuilder();
Uri u = new Uri("/Pages/MainPage.xaml",UriKind.Relative);
builder.Register(ctx => (PageBase)Application.LoadComponent(u)).Keyed<PageBase>(u);
var contentLoader = new AutofacContentLoader(builder.Build());
You can even create a nice little extension method called RegisterPage to register pages:
public static Autofac.Builder.IRegistrationBuilder<PageBase, Autofac.Builder.SimpleActivatorData, Autofac.Builder.SingleRegistrationStyle> RegisterPage(this ContainerBuilder builder, string uri)
{
var u = new Uri(uri, UriKind.Relative);
return builder.Register(ctx => (PageBase)Application.LoadComponent(u)).Keyed<PageBase>(u);
}
Again, every piece of code you need is in the post, so no Github repo this time.