Skip to content
Bruno Sonnino
Menu
  • Home
  • About
Menu

Dynamic control loading in WinAppSDK apps

Posted on 19 August 2024

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>
XML

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);
    }
}
C#

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);
}
C#

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);
    }
}
C#

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();
}
C#

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>
XML

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";
C#

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\
Shell

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:

Dynamic Loading1

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>
XML

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;
}
C#

With this code in place, we can run the project again. This time, we get a different error:

Dynamic Loading2

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);
}
C#

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"/>
XML

Dynamic Loading3

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:

Dynamic Loading4

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

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

  • May 2025
  • December 2024
  • October 2024
  • August 2024
  • July 2024
  • June 2024
  • November 2023
  • October 2023
  • August 2023
  • July 2023
  • June 2023
  • May 2023
  • November 2022
  • October 2022
  • September 2022
  • August 2022
  • June 2022
  • April 2022
  • March 2022
  • February 2022
  • January 2022
  • July 2021
  • June 2021
  • May 2021
  • April 2021
  • March 2021
  • February 2021
  • January 2021
  • December 2020
  • October 2020
  • September 2020
  • April 2020
  • March 2020
  • January 2020
  • November 2019
  • September 2019
  • August 2019
  • July 2019
  • June 2019
  • April 2019
  • March 2019
  • February 2019
  • January 2019
  • December 2018
  • November 2018
  • October 2018
  • September 2018
  • August 2018
  • July 2018
  • June 2018
  • May 2018
  • November 2017
  • October 2017
  • September 2017
  • August 2017
  • June 2017
  • May 2017
  • March 2017
  • February 2017
  • January 2017
  • December 2016
  • November 2016
  • October 2016
  • September 2016
  • August 2016
  • July 2016
  • June 2016
  • May 2016
  • April 2016
  • March 2016
  • February 2016
  • October 2015
  • August 2013
  • May 2013
  • February 2012
  • January 2012
  • April 2011
  • March 2011
  • December 2010
  • November 2009
  • June 2009
  • April 2009
  • March 2009
  • February 2009
  • January 2009
  • December 2008
  • November 2008
  • October 2008
  • July 2008
  • March 2008
  • February 2008
  • January 2008
  • December 2007
  • November 2007
  • October 2007
  • September 2007
  • August 2007
  • July 2007
  • Development
  • English
  • Português
  • Uncategorized
  • Windows

.NET AI Algorithms asp.NET Backup C# Debugging Delphi Dependency Injection Desktop Bridge Desktop icons Entity Framework JSON Linq Mef Minimal API MVVM NTFS Open Source OpenXML OzCode PowerShell Sensors Silverlight Source Code Generators sql server Surface Dial Testing Tools TypeScript UI Unit Testing UWP Visual Studio VS Code WCF WebView2 WinAppSDK Windows Windows 10 Windows Forms Windows Phone WPF XAML Zip

  • Entries RSS
  • Comments RSS
©2025 Bruno Sonnino | Design: Newspaperly WordPress Theme
Menu
  • Home
  • About