You are developing a new application and want it to be scalable: instead of having a single application with all the interactions, you want to develop it in a modular way, adding new modules as they are developed and make them available without the need of recompiling the app.
For that, you can use something like the Micro frontend architecture. With that kind of architecture, you will develop a host container and add plugin controls to it, as they are being developed.
These plugins are loaded at runtime and added to the container when the app is executed. That way, you can have a scalable application that can be developed in parallel by multiple teams, each one at its own pace. When a module is ready, it's made available to the main application and loaded at runtime.
Some time ago, I wrote this article about loading views and viewmodels dynamically, and you can use this approach to load your plugins in a dynamic way: you can leave the views and viewmodels in some server and load them at runtime.
This approach is nice, but you may not want to use simple views and viewmodels in your app. You may want to develop full controls and add them directly to the UI. In this article, I will show how to develop a modular WinAppSDK app that loads different pages at runtime.
We will create a host app with a NavigationView, which will load its pages at runtime from a folder. To add a new page, we only need to create a new class library, add the page to it and put the compiled files in a special folder.
Creating the dashboard
In Visual Studio, create a new Blank Packaged WinUI3 app. In MainWindow.xaml, we will add a NavigationView that will host our plugins:
<NavigationView x:Name="MainNav" SelectionChanged="MainNav_SelectionChanged"
IsBackButtonVisible="Collapsed" IsPaneOpen="True">
<Frame x:Name="MainFrame" />
</NavigationView>
As you can see, the NavigationView is empty, it will host the plugins that will be loaded dynamically. To do that, we need to create the code that will load the plugins in MainWindow.xaml.cs:
private void LoadDynamicPages()
{
string dllFolder = Path.GetFullPath(AppDomain.CurrentDomain.BaseDirectory +
"..\\..\\..\\..\\..\\..\\..\\DynamicModules");
if (!Directory.Exists(dllFolder))
{
Directory.CreateDirectory(dllFolder);
}
var dlls = Directory.GetFiles(dllFolder, "*.dll");
foreach (var dll in dlls)
{
try
{
Assembly assembly = Assembly.LoadFrom(dll);
var types = assembly.GetTypes().Where(t => t.IsSubclassOf(typeof(Page)));
foreach (var type in types)
{
var title = type.GetProperty("Title")?.GetValue(null) as string ?? "";
var icon = type.GetProperty("Icon")?.GetValue(null) as string ?? "";
var description = type.GetProperty("Description")?.GetValue(null) as string ?? "";
var item = new NavigationViewItem
{
Content = title ?? type.Name,
Icon = new FontIcon() { Glyph = icon },
Tag = type,
};
ToolTipService.SetToolTip(item, description);
MainNav.MenuItems.Add(item);
}
}
catch (Exception ex)
{
Console.WriteLine($"Error loading {dll}: {ex.Message}");
}
}
if (MainNav.MenuItems.Count > 0 && MainNav.MenuItems[0] is NavigationViewItem it && it.Tag is Type tp)
{
NavigateToPage(tp);
}
}
This code will search all dlls in the DynamicModules folder and, for each dll, will look for all pages in it. For every page in the dll, it will search the static properties Title and Icon and will add a new NavigationViewItem to the NavigationView. If there is any item, it will load the page. The NavigateToPage method is:
private void NavigateToPage(Type type)
{
MainFrame.Navigate(type);
}
We also need to handle the SelectionChanged event for the NavigationView:
private void MainNav_SelectionChanged(NavigationView sender, NavigationViewSelectionChangedEventArgs args)
{
if (args.SelectedItem is NavigationViewItem navItem && navItem.Tag is Type type)
{
NavigateToPage(type);
}
}
It will check the item selected and get the Tag property, which should have the type of the desired control to be loaded and will call NavigateToPage to load the page.
In the constructor, we must call LoadDynamicPages, so the dynamic pages are loaded at startup:
public MainWindow()
{
this.InitializeComponent();
LoadDynamicPages();
}
Creating the plugin
The plugins are WinUI3 class libraries that have one or more Page in them. Each Page should have three static properties:
- Title - the title of the page, which will be used in the NavigationViewItem header
- Icon - the icon unicode hex character, pointing to the Segoe Fluent Icons font
- Description - the description of the page, used in the tooltip
Let's create our first plugin. In the solution, create a new class library (WinUI3). Remove the Class1 class and add a new Page. Name it BluePage. We are adding the plugins in the same solution as the host, but that is not needed. You can create independent solutions for the host and the plugins.
In BluePage.xaml, add a blue grid:
<Grid Background="Blue" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<TextBlock Text="Blue" HorizontalAlignment="Center" VerticalAlignment="Center" Foreground="White"/>
</Grid>
In BluePage.xaml.cs, add the three properties:
public static string Title => "Blue Page";
public static string Description => "This is the blue page";
public static string Icon => "\xe13d";
The plugins must be in the DynamicModules folder. For that, we need to copy the compiled files to that folder. That is done by adding a Post Build Event. In the project properties, go to the Build tab and select Events and fill the Post Build Event with this command:
xcopy /y /s $(ProjectDir)$(OutDir)*.* $(ProjectDir)..\DynamicModules\
This will copy all the compiled files to the DynamicModules folder, in the parent folder for the current project.
Now, we can run the project and see if the plugin is loaded. We must rebuild the project in order for every project to be rebuilt and made available. When we run it, we get an error:
This error seems strange, an Access Violation when loading the page into the frame. As we are doing something completely non-standard, I tried to make a change: instead of using the frame and letting the framework create and load the page instance, I changed the main part to a content control, created the instance using reflection, and loaded it. For that, I changed the xaml in MainPage.xaml to:
<NavigationView x:Name="MainNav" SelectionChanged="MainNav_SelectionChanged"
IsBackButtonVisible="Collapsed" IsPaneOpen="True">
<!--<Frame x:Name="MainFrame" />-->
<ContentControl x:Name="MainContent" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch"/>
</NavigationView>
and then the code to create and load the page instance is:
private void NavigateToPage(Type type)
{
//MainFrame.Navigate(type);
var page = Activator.CreateInstance(type) as Page;
MainContent.Content = page;
}
With this code in place, we can run the project again. This time, we get a different error:
After some research, I found this bug. The issue is that the XAML compiler is generating code to load the resource in the dll resources, which is not valid anymore when you are loading the page from the main application.
The solution, in this case, is to replace the InitializeComponent method with a custom one, which will be called from the constructor:
public BluePage()
{
this.InitializeComponentCustom();
}
public void InitializeComponentCustom()
{
if (_contentLoaded)
return;
_contentLoaded = true;
var codeBase = Assembly.GetExecutingAssembly().Location;
UriBuilder uri = new UriBuilder(codeBase);
string path = Uri.UnescapeDataString(uri.Path);
string resourcepath = $"ms-appx:///{Path.GetDirectoryName(path).Replace("\\", "/")}/{this.ToString().Replace(".", "/")}.xaml";
var resourceLocator = new Uri(resourcepath);
Microsoft.UI.Xaml.Application.LoadComponent(this, resourceLocator, Microsoft.UI.Xaml.Controls.Primitives.ComponentResourceLocation.Application);
}
This code will change the location of the resource, pointing to the folder where it was copied. Once you make this change and rerun the project, it will run fine:
You can add other plugins using the same structure. If you need to use assets from the project, don't use the ms-appx schema, as the resources won't be found in the main app (that's where the program will search for the data). You can use the folder directly:
<Image Source="MSLogo.png" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" Stretch="Uniform" Margin="50"/>
Conclusions
As you can see, we can have dynamic loading plugins in WinAppSdk. You must tweak the initialization of the page, and you will lose the navigation features that are added by the Frame component (history, back and forward navigation, etc.) but, in the end, you will have an app that is modular and can be developed by multiple independent teams. If you want to streamline the process of creation of the base Page, you can create a Visual Studio template based on the BluePage project. Just go to Project/Export Template and select to export as a project template:
When you save it, you will have a new template available, which you can customize and distribute to all your team members as a vsix extension.
The source code for this project is at https://github.com/bsonnino/DynamicLoading