Using the Windows App SDK Resource Manager (MRT Core) in Unpackaged Win32 (WinForms/WPF) App.

As a UWP developer I relied heavily on the framework to deal with managing application resources such as images and string literals. You might think this is quite simple as images are just files that are packaged with your app and strings can just be put in a constants file. Where this all gets complicated is where you need to deal with different screen resolutions, or different languages. This is where the resource management capabilities of UWP kicks in, allowing you to define resources in different files and for the appropriate resources to be loaded based on the runtime properties of the application. In this post we’re going to look at how the Windows App SDK makes resource management easy for win32 based applications such as WinForms and WPF.

A quick side note before we get started – the example in this post uses the new “unpackaged” application capability that’s available in the experimental release of the Windows App SDK. An unpackaged application doesn’t require a separate packaging project. If you’re using the stable release at the time of writing you’ll still need to distribute your application using a packaging project.

I’m going to walk through how to create and manage resources in a WPF application using MRT Core. The same technique will work with any Win32 based application such as WinForms, or even a WinUI3 based application. Note that if you’re using WinUI3 the resource management for images, strings etc that are referenced from XAML is all automatic, so you shouldn’t need to write custom logic such as this example.

Let’s get started with a basic WPF application, MRTCoreSample, which is based on the WPF Application template in Visual Studio. The initial csproj contains only the bare bones required for our application, which in this case is running on .NET 6. This process should work with existing applications so long as they’ve been updated to run on .NET 5 or 6.

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net6.0-windows</TargetFramework>
    <Nullable>enable</Nullable>
    <UseWPF>true</UseWPF>
  </PropertyGroup>
</Project>

The first thing we’re going to do is add references to the Windows App SDK and specify the platforms and runtime identifiers for the application (these are required since the Windows App SDK is a native library that targets specific platform architectures, so doesn’t support AnyCPU).

<Project Sdk="Microsoft.NET.Sdk">
	<PropertyGroup>
		<OutputType>WinExe</OutputType>
		<TargetFramework>net6.0-windows10.0.19041.0</TargetFramework>
		<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
		<Platforms>x86;x64;arm64</Platforms>
		<RuntimeIdentifiers>win10-x86;win10-x64</RuntimeIdentifiers>
		<Nullable>enable</Nullable>
		<UseWPF>true</UseWPF>
	</PropertyGroup>

	<ItemGroup Label="Package references">
		<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.0.0-experimental1" />
		<PackageReference Include="Microsoft.WindowsAppSDK.Foundation" Version="1.0.0-experimental1" />
		<PackageReference Include="Microsoft.WindowsAppSDK.WinUI" Version="1.0.0-experimental1" />
		<PackageReference Include="Microsoft.WindowsAppSDK.InteractiveExperiences" Version="1.0.0-experimental1" />
	</ItemGroup>
</Project>

You’ll also notice that we’ve updated the TargetFramework to include the SDK version and set the TargetPlatformMinVersion. Again, these are required in order for the Windows App SDK to be included in the application.

When you save the changes to the csproj, you’ll most likely see a prompt across the top of the Visual Studio editor indicating that you need to reload one or more projects. Make sure you click the Reload projects button in order for the Windows App SDK references and other property changes to take effect.

If you attempt to build the project at this point you may see a slightly weird behaviour from Visual Studio where it refuses to build the updated project. You can right-click on the project and select Rebuild but all you see in the output window is

Rebuild started...
1>------ Skipped Rebuild All: Project: MRTCoreSample ------
1> 
========== Rebuild All: 0 succeeded, 0 failed, 1 skipped ==========

The cause seems to be a misalignment in the build configuration – prior to the update the project was set to build as AnyCPU; after the update, the project doesn’t have an AnyCPU platform, instead it has x86, x64 and arm64. This issue is easily resolved by opening the Configuration Manager and then simply clicking Close to persist the default settings. After doing this, the project will build as you’d expect.

Before we get started creating any resources, we need to add some logic to our application to initialize the Windows App SDK. The documentation for this isn’t great at the moment but there are some good samples, such as the C# WinForms Unpackaged sample that shows how to initialize the SDK. We’re going with a slightly simpler mechanism that imports a single method that is used to initialize the SDK. The following NativeMethods class is added as a private class within the MainWindow class.

private class NativeMethods
{
    [System.Runtime.InteropServices.DllImport("Microsoft.WindowsAppSDK.Bootstrap", EntryPoint = "MddBootstrapInitialize", ExactSpelling = true, PreserveSig = true)]
    public static extern uint MddBootstrapInitialize(
        uint majorMinorVersion,
        [System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.LPWStr)]
        string versionTag,
        ulong packageVersion);
}

In the constructor for the MainWindow, we just need to call the MddBootstrapInitialize method

public MainWindow()
{
    var hr = NativeMethods.MddBootstrapInitialize(1 << 16 | 0, "experimental1", 0);
    if (hr != 0)
    {
        System.Runtime.InteropServices.Marshal.ThrowExceptionForHR(unchecked((int)hr));
    }

    InitializeComponent();
}

Now that we’ve initialized the SDK, the next thing we’re going to do is add a couple of resource files to the application. In doing this, make sure you’re adding .resw files so that they’re compatible with MRT Core. The two files we’re going to add are resources.lang-en-US.resw and resources.lang-fr-CA.resw, which as you might have guessed are going to correspond to resources for English (US) and French (CA).

In each of the resource files I’ve added a string resource for WelcomeText (apologies if my French isn’t correct as I just used a translation tool since I don’t speak French).

Let’s write some code to load the resources. In the following code we’re creating an instance of the ResourceManager, passing in the name of a .pri file. The pri file is automatically created during the build process, with the file name matching the name of the assembly name of the project. In this case our project is called MRTCoreSample, so the corresponding pri file is MRTCoreSample.pri.

var resManager = new Microsoft.ApplicationModel.Resources.ResourceManager("MRTCoreSample.pri");

var ctx = resManager.CreateResourceContext();
var candidate = resManager.MainResourceMap.GetValue("Resources/WelcomeText", ctx);
var enString = candidate.ValueAsString;

ctx.QualifierValues["Language"] = "fr-CA";
candidate = resManager.MainResourceMap.GetValue("Resources/WelcomeText", ctx);
var frString = candidate.ValueAsString;

The next step is to create a resource context, which will automatically include some default qualifiers (see image below). For example, on my computer where I have English (AU + US) and French (CA) languages installed, the Language qualifier includes all three languages. These qualifiers are used to determine which resources to load.

As you can see from the following screenshot, the output of the above code shows that the en-US resource is returned by default and that the fr-CA resource is returned when the Language qualifier is set to just fr-CA. Note that “lang” is a shortcut for Language, that is used in the naming of files. Hence setting the Language qualifier to fr-CA will look for resources with lang-fr-ca in the file name.

The resource manager looks for resources that match the Language qualifier in the order that the languages are specified (ie en-AU, en-US and then fr-CA in the case of my default Language qualifier), which is why it returns the en-US resource by default. If I were to specify en-AU resources, they would be given priority.

The default languages, and their order, are not random. They are set based on the language settings of the host computer. As you can see, the languages for my computer align with the default Language qualifier.

One of the interesting things that you can do with MRT Core is to use your own resource qualifier using the Custom qualifier. Let’s add a couple more resources to our application, this time with filenames resources.custom-noauth.resw and resources.custom-auth.resw. In this case, these resources may be used for different states of the application, corresponding to whether the user has been authenticated or not. Again, the code for loading these resources is similar to using the Language qualifier.

    ctx.QualifierValues["custom"] = "noauth";
    candidate = resManager.MainResourceMap.GetValue("Resources/WelcomeText", ctx);
    var noauthString = candidate.ValueAsString;


    ctx.QualifierValues["custom"] = "auth";
    candidate = resManager.MainResourceMap.GetValue("Resources/WelcomeText", ctx);
    var authString = candidate.ValueAsString;

Where this may get more complex if if you want to use multiple qualifiers together. For example, you might want to define language and custom qualifiers. Rather than creating longer file names, you can use folder names to represent the qualifiers. For example, in the following image there is an en-us folder and a fr-CA folder. Within each folder there are resources for both auth and no auth.

When we access these from code, we can set both Language and Custom qualifiers in order to retrieve the corresponding resource.

In this post we’ve walked through creating and loading resources based on different qualifiers. This can be extended to include images and use other qualifiers such as Scale or Theme. As you move forward with either a new or existing application, think about how you can leverage the Windows App SDK to help you build better applications for Windows.

Update: 26th September 2021

With the release of v1 preview of the Windows App SDK there are a couple of changes that break this code. Firstly, there is a change to the namespace for the ResourceManager.

You’ll also need to update the code for bootstrapping the Windows App Sdk. I suggest copying the MddBootstrap class from this sample and updating your code accordingly