Supporting a modular Windows application with MSIX and Optional Packages
Published Jan 23 2020 05:48 AM 7,244 Views
Microsoft

MSIX aims to become the best packaging and deployment technology for Windows desktop applications and, on this blog, we have seen multiple times its advantages: the clean install & uninstall experience, the network bandwidth and disk space optimizations, the support to automatic updates even without using the Microsoft Store, etc. However, there is a category of applications for which, at first glance, adopting MSIX could be a challenge: modular applications. We're talking about applications that offers a set of core features, but that can be expanded with additional ones by installing additional components, like plug-ins or modules. There are many applications on the market that behave this way: Paint.NET, Office, etc.

 

The challenge comes from the fact that Win32 applications packaged with MSIX run inside a lightweight container. It isn't a full sandbox like the one leveraged by Universal Windows Platform apps, but still helps to isolate some of the most critical aspects of an application, like the registry or the AppData folder in the file system. If this feature bring many security and reliability benefits, it can be a challenge for applications that need to be extended with additional modules. Since the registry and the file system are isolated, for a module it can be hard to interact with the main application.

 

In this blog post we're going to explore a couple of options to implement this scenario with MSIX. But first, let's introduce the application we're going to use to demonstrate the scenario.

 

An extensible application

The starting point is a Windows Forms application built on top of .NET Framework 4.8, which is available on GitHub. The application, called MyEmployees, is very simple: it displays a list of employees that is stored in a SQLite database which ships with the application itself. The data access layer is implemented using the System.Data.SQLite library, which is an ADO.NET provider for SQLite. The frontend, instead, is made by a form which includes a DataGrid control, which is populated by a BindingSource. When the application starts, we execute a query on the SQLite database to retrieve all the employees. I won't go into the details in this post because it would be out of scope from the purpose of the article. The key feature we're interested to implement is extensions support. The application allows to export the list of employees in a CSV file. However, this feature isn't implemented directly in the main core, but in a separate plugin that can be installed independently. If this plugin is found, the application will enable the export option; otherwise, it will stay hidden.

 

Let's see, step by step, how it has been implemented.

 

Define a common interface

As first step, we need to define a generic interface that describes a plugin. This interface must be implemented by every plugin and the main application must be aware of it. Every plugin, in fact, can offer different features, but it must provide a standard way to be leveraged by the main application. Since the interface must be shared between every plugin and the main application, we're going to create a dedicated Class Library to store it.

 

This is the purpose of the MyEmployees.PluginInterface project you can see in the Visual Studio solution. It contains just a single file, which defines a generic IPlugin interface:

 

public interface IPlugin
{
    string Name { get; }

    string GetDescription();

    bool Execute(IList data, string filePath);

    event EventHandler OnExecute;
}

The Execute() method is the one which we're going to use to implement the export logic. It receives, as parameters from the main application, the list of employees and the path where to save the CSV file.

 

Implement the plugin

Now that we have an interface, we can implement it. However, since the plugin is an extension, we're going to do it in another class library. It's the project called ExportDataLibrary. Let's take a look at the ExportData.cs file included in the project:

 

public class ExportData: IPlugin
{
    public string Name => "Export Data";

    public event EventHandler OnExecute;

    public bool Execute(IList data, string filePath)
    {
        try
        {
            using (TextWriter textWriter = File.CreateText(filePath))
            {
                CsvWriter csvWriter = new CsvWriter(textWriter);
                csvWriter.Configuration.Delimiter = ";";
                csvWriter.WriteRecords(data);
            }

            return true;
        }
        catch (Exception exc)
        {
            return false;
        }
    }

    public string GetDescription()
    {
        return "Export data to CSV";
    }
}

 

We're implementing every property and method exposed by the IPlugin interface. The core of the plugin is the Execute() method, which contains the logic to export the CSV file. The task is achieved using a library called CsvHelper, which makes the whole implementation really easy. We create a TextWriter object, which is linked to the file path passed by the main application. We use it to initialize a CsvWriter object. We setup the delimiter to use (a semi colon) and then we write the list of employees passed by the main application using the WriteRecords() method. That's it!

 

The main application

Let's see now how we can load this plugin into the main application and leverage it. We're going to use reflection to dynamically load the DLL generated by our Class Library and discover the types and methods it implements. This is the required method to load the assembly:

 

private IPlugin LoadAssembly(string assemblyPath)
{
    string assembly = Path.GetFullPath(assemblyPath);
    Assembly ptrAssembly = Assembly.LoadFile(assembly);
    foreach (Type item in ptrAssembly.GetTypes())
    {
        if (!item.IsClass) continue;
        if (item.GetInterfaces().Contains(typeof(IPlugin)))
        {
            return (IPlugin)Activator.CreateInstance(item);
        }
    }
    throw new Exception("Invalid DLL, Interface not found!");
}

The parameter passed to the method is the path of the DLL. Once it has been loaded with the LoadFile() method exposed by the Assembly class, we iterate through the types exposed by the library. Since we have defined a common interface, IPlugin, we look specifically for types which implement it. Once we find it, we create an instance of the class using the Activator.CreateInstance() method. Now we have a reference to the plugin, so we can use it through our application to invoke the Execute() method so that we can perform the CSV export.

The application includes a menu to invoke the export feature. This is how the event handler connected to the menu item looks like:

 

private void exportAsCSVToolStripMenuItem_Click(object sender, EventArgs e)
{
    SaveFileDialog saveFileDialog = new SaveFileDialog();
    saveFileDialog.Filter = "CSV|*.csv";
    saveFileDialog.Title = "Save a CSV file";
    saveFileDialog.ShowDialog();

    bool isFileSaved = plugin.Execute(employeeBindingSource.List, saveFileDialog.FileName);
    if (isFileSaved)
    {
        MessageBox.Show("The CSV file has been exported with success");
    }
    else
    {
        MessageBox.Show("The export operation has failed");
    }
}

The application uses a SaveFileDialog object to let the user choose where he wants to save the generated CSV file. Then we invoke the Execute() method exposed by the plugin, using the reference we have previously retrieved with the LoadAssembly() method. We pass two parameters: the list of employees (stored in the List property of the BindingSource object) and path of the file just selected by the user, which is stored in the FileName property of the SaveFileDialog object.

That's it! We miss only one step: invoke the LoadAssembly() method passing the path of the DLL which includes our plugin. We do this when the application is loaded:

 

public void LoadPlugins() 
{
    try
    {
        string dllPath = $"{Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles)}\\Contoso\\MyEmployees\\Plugins\\ExportDataLibrary.dll";
        plugin = LoadAssembly(dllPath);
        exportAsCsvButton.Visible = true;
    }
    catch (Exception)
    {
        exportMenu.Visible = false;
    }
}

The application expects to find the plugin in the C:\Program Files (x86)\Contoso\MyEmployees\Plugins folder. If it can find the DLL and load it properly, then the menu to export the CSV is made visible; otherwise, it stays hidden.

 

The solution contains also a Windows Application Packaging Project, which is used to package the Windows Forms application with MSIX.

 

Now that we have an idea of the general architecture of the application, let's see how can we deploy the application and the plugin using the MSIX technology.

 

The first option: modification packages

Let's start with the simplest scenario, which doesn't require to change the code of the application. As such, we need to find a way to deploy the plugin in a separate way than the main application but, at the same time, make sure it can find it in the C:\Program Files (x86)\Contoso\MyEmployees\Plugins folder. Let's introduce modification packages, which have already been discussed multiple times on this blog. A modification package is a special MSIX optional package, which doesn't contain a full application, but files and assets that can be leveraged by another application.

 

The key feature of modification packages is that they share the same identity of the main application. As such, when you leverage MSIX features like the Virtual File System or the Virtual Registry, Windows will treat them like if they belong to the same application. Let's see how this behavior can be leveraged for our scenario. We know that MSIX supports a feature called Virtual File System, which is implemented with a special folder called VFS that can be included inside a package. This folder can contain special folders (the full list is available here, which maps common system folders, like C:\Program Files, C:\Windows, C:\Windows\System32, etc. When the application runs and looks for files in a system folder (like a dependency), Windows will look first in the VFS folder. This approach allows you to create self-contained MSIX packages, which don't require the user to manually install additional dependencies required by the application; additionally, it helps to solve the DLL Hell problem, when multiple versions of the same dependency must coexist on the same machine.

 

When we install a modification package, Windows will treat the VFS folder included inside it like if belongs to the main application. This means that, if we include our plugin inside the folder which maps C:\Program Files (x86) (which is called ProgramFilesX86), the application will be able to find it without needing any code change.

Let's see the real modification package, so that we can fully understand what I've just explained. You can download it from the GitHub repository. The easiest way to edit is to install the MSIX Packaging Tool from the Microsoft Store. This way, you just need to right click on the package and choose Edit with the MSIX Packaging Tool to explore it.

Let's start by taking a look at the manifest file, by clicking on the Open file button under the Manifest file section:

 

<?xml version="1.0" encoding="utf-8"?>
<Package xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10" xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"  xmlns:uap4="http://schemas.microsoft.com/appx/manifest/uap/windows10/4">
  <Identity Name="MyEmployees-ExportPlugin" Publisher="CN=Matteo Pagani" Version="1.0.3.0" ProcessorArchitecture="x64" />
  <Properties>
    <DisplayName>MyEmployees (Export Plugin)</DisplayName>
    <PublisherDisplayName>Contoso</PublisherDisplayName>
    <Description>Reserved</Description>
    <Logo>Assets\StoreLogo.png</Logo>
  </Properties>
  <Resources>
    <Resource Language="en-us" />
  </Resources>
  <Dependencies>
    <TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17701.0" MaxVersionTested="10.0.17763.0" />
    <uap4:MainPackageDependency Name="MyEmployees" Publisher="CN=Matteo Pagani" />
  </Dependencies>
</Package>

As you can see, exactly like a traditional MSIX package, it has an identity, a publisher, a version number, etc. However, there are two major key differences:

  • It doesn't have an Application entry. Since it doesn't contain a full application, this package doesn't have an entry point.
  • Inside the Dependencies section we can find a MainPackageDependency entry, which specifies the name and the publisher of the main application it targets. If the application which identity is specified with the MainPackageDependency entry is not installed, Windows will refuse to install the optional package.

And what about the content? Move to the Package files section to explore the content of the package. We can see that the output of the ExportDataLibrary class library build has been copied inside the VFS/ProgramFilesX86/Contoso/MyEmployees/Plugins folder. This is the folder where the original code of the application expects to find the plugin.

 

VFS.png

Let's see this in action. This is the version of the app to use. Open the solution in Visual Studio, then right click on the MyEmployees.Package project and choose Deploy. Launch it and notice how the export menu is hidden, since the plugin DLL can't be found.

 

ExportMissing.png

 

Now double click on the modification package which you have just downloaded from the repository. The installation will proceed like with a regular MSIX package, excepts that the Windows won't offer you the option to launch the application since, well... we didn't install an application =) For the same reason, if we open the Start menu we won't find the optional package listed in the list of programs. We will continue to find only the My Employees application. Where can we see the optional package? Right click on the My Employees entry in the Start menu and choose More → App Settings. Scroll down the settings page until you see a section titled App add-ons & downloadable content. Here is our plugin!

 

PluginSettings.png

Now open the My Employees application. You can immediately see that the plugin has been recognized, since now the Export option is available:

 

ExportAvailable.png

And if you click on the option, you will start the process to create a CSV and export it on a file on your local PC. Everything is working, without needing us to touch the code of the original application.

 

Option 2: using an Optional Package

The previous scenario is great for legacy applications or IT Pros who don't have access to the code, but it doesn't offer too much flexibility. We are required to leverage the Virtual File System to "fake" to the main application that our plugin is indeed installed in the C:\Program Files (x86) folder, which limits our opportunities to do more.

 

If we are developers there's a better way to use the optional package which contains our plugin. The Universal Windows Platform, in fact, offers a set of APIs to query all the optional packages that are installed for an application. For each of them, we can get the identity, we can access to all the included files, etc.

 

Let's start with analyzing the optional package. From an architectural point of view, there's no difference with the modification package we have seen before. A modification package, in fact, is just an optional package which, by leveraging the Virtual File System and the Virtual Registry, can make its content available to the main application without changing the code. A traditional optional package, in fact, doesn't need to leverage these features: the application is aware of the existence of optional packages and, as such, can freely load the files which are included. Other than that, they behave in the exact same way. They are both installed in the same way; they both appear in the App add-ons & downloadable content section of the App Settings; they can be deployed independently from the main application.

Also in this case you can find the optional package in the the repository. If you edit it with the MSIX Packaging Tool, you will notice that it has the same exact manifest of the modification package. The only difference is the package content:

 

FilesOptionalPackage.png

 

Since we don't need to leverage anymore the VFS, we can just include the files which belong to our plugin in the root of the package. Let's see now how we can use this optional package in the Windows Forms application. As first step, we need to use some Windows 10 APIs, so we need to install the Microsoft.Windows.Sdk.Contracts package from NuGet, which allows our .NET application to access to the API surface of the Universal Windows Platform. Once you have installed it, we can leverage the Package class included in the Windows.ApplicationModel namespace to query our optional packages.

 

This is the code we're going to use to access to our plugin:

 

private async Task LoadOptionalPackagesAsync()
{
    if (Package.Current.Dependencies.Count > 0)
    {
        foreach (var package in Package.Current.Dependencies)
        {
            if (package.Id.Name.Contains("ExportPlugin"))
            {
                var file = await package.InstalledLocation.GetFileAsync("ExportDataLibrary.dll");
                plugin = LoadAssembly(file.Path);
                exportAsCsvButton.Visible = true;
            }
        }
    }
    else
    {
        exportAsCsvButton.Visible = false;
    }
}

All the optional packages are available through a collection called Dependencies, exposed by the singleton Package.Current. In our case we iterate through all the packages and, if we find one which name (stored in the Id.Name property) contains the ExportPlugin string, it means we can enable the export feature. All the files included in the optional package are available through the InstalledLocation property, which type is StorageFolder. This means that we can use all the Windows 10 Storage APIs to query, open and work with the files included in the optional package. In our case, we use the GetFileAsync() method to get a reference to the ExportDataLibrary.dll file. Then, we use the Path property as parameter to invoke the LoadAssembly() method and we save a reference to the plugin, so that we can use it later when the we need to use the CSV export function.

 

As you can see, these APIs offer more flexibility compared to the modification package scenario. The MyEmployees application is very simple and we have developed only one plugin, but think to an application which can have hundreds of plugins. Thanks to these APIs we could provide a UI that displays all the available optional packages, allowing to the user to enable, disable or manage them.

 

If you want to try also this scenario on your machine, this is the folder which contains the MyEmployees version which uses the Optional Packages APIs.

 

Wrapping up

In this post we have seen two different options to support extensible applications with MSIX:

  • Using modification packages, which leverage the Virtual File System and the Virtual Registry provided by the platform. This allows to leverage MSIX for plugins without asking the developer to change the code of the application. However, this approach is limited by the way the developer has created the plugin support. We are forced to follow his constraints and it's not granted that the way he has implemented plugin support will be fully compliant with modification packages.
  • Using optional packages, which require the developer to change the code of the application but they give you lot of flexibility. You can query for the installed optional packages; you can access to the included file; etc.

Before closing the post, there's an important caveat to keep in mind. You can't publish a modification package or an optional package on the Microsoft Store. As such, everything we have seen in this post applies only for enterprise distribution or sideloading.

 

As usual, you can find the sample used in this post on GitHub.

 

Happy coding!

13 Comments
Version history
Last update:
‎Jan 24 2020 12:56 AM
Updated by: