Unpackaged Windows Apps with Identity using a Sparse Package

In my previous post I made a passing reference to the use of sparse packages to allow unpackaged Windows applications to acquire an identity. In this post we’re going to walk through creating a sparse package and attaching it to an unpackaged Windows application.

Before we get into it I want to link to a few relevant posts and documentation. It’s worth having a read through these as they provide some context and alternative instructions on using a sparse package:

These links mostly focus around the PhotoStoreDemo sample and how to run it. In contrast, in this post we’re going to create a new sparse package from scratch.

Identity

At this point you might be wondering what an unpackage application is, what identity is, why your app needs it and what’s a sparse package? Let’s cover these questions before we get into the walkthrough:

What is an unpackaged application?

When we refer to an unpackaged application we’re talking about an application that hasn’t been distributed using one of the packaging formats supported by Windows (namely APPX and MSIX. Since MSIX effectively supersedes APPX, we’ll mostly just be talking about MSIX). An unpackaged application could be installed using a traditional installer, for example as an MSI, or even using ClickOne. Because the application hasn’t been installed from a package, it won’t have identity and won’t be running in a Windows app container.

Note that by default all UWP applications are packaged, so when we refer to an unpackaged application we’re typically referring to a WinForms or WPF application. Microsoft has indicated that at some point they’ll provide a way for a desktop WinUI app (desktop template) to run unpackaged – currently this is only possible with a lot of hacking.

What is identity?

Tanaka explains identity in his post as “The need for package or application identity to identify the caller, and an identifier to scope data and resources.” In straight forward terms identity is needed in order for an application to take advantage of a number of the WinRT apis. For example, in order to access ApplicationData or to register for Toast notifications.

Why does my app need an identity?

If you’re application is running fine today, it may not need an identity. However, if you want to take advantage of some of the modern features of Windows 10, like notifications, your application will need an identity.

What’s a sparse package?

Usually the approach to acquire an identity for an application is to package the application using the Windows Application Packaging project template. This will package the application as a MSIX ready for distribution. As part of installation, the application will be assigned an identity. However, even though the application may be set to run as full trust, parts of the file system and registry are virtualized to ensure the application can be cleanly uninstalled. Furthermore, packaging as an MSIX may not work well for existing applications that may have a more complex installation process.

This is where a sparse package can provide an alternative. A sparse package is simply a package (i.e. an MSIX) where all, or part, of the application resides outside of the package. The manifest of the package has the AllowExternalContent property set to true, and then the location of the external files is defined by setting the ExternalLocationUri property. Don’t worry if this doesn’t make sense right now, we’ll go through the process step by step.

Sparse WPF App

Ok, so let’s get into building a sample app that makes use of a sparse package – for this, we’re going to add a new sample app to the existing GitHub repository nickrandolph/TrustAppContainer (github.com). We’ll add a new WPF project based on the WPF (.NET) project template, SparseWpfApp. The walkthroughs I’ve linked above uses a .NET 4.6.1 sample app, demonstrating how you can attach identity to a full .NET Framework application. In this case, our application is going to be a .NET 5.0 application. However, since we want to reference the package management APIs of Windows 10, we’ll adjust the TFM to .net5.0-windows10.0.19041.0. The resulting csproj project file looks like:

<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net5.0-windows10.0.19041.0</TargetFramework>
    <UseWPF>true</UseWPF>
  </PropertyGroup>
</Project>

Note: If you’re working with a full .NET Framework application, you’ll need to add a reference to the appropriate Windows.winmd file that ships with the Windows SDK (eg C:\Program Files (x86)\Windows Kits\10\UnionMetadata\<SDK_Version>\Windows.winmd)

We’ll also add some basic layout to our application so that we can see whether the application is running with identity. We’ll add a couple of buttons to allow us to register, unregister and restart the application.

<Window x:Class="SparseWPFApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <StackPanel VerticalAlignment="Center" 
                    HorizontalAlignment="Center">
            <TextBlock x:Name="IdentityText" Text="Running...."/>
            <Button Content="Register" 
                    Click="RegisterClick" Margin="10"/>
            <Button Content="Unregister" 
                    Click="UnregisterClick" Margin="10"/>
            <Button Content="Restart" 
                    Click="RestartClick" Margin="10"/>
        </StackPanel>
    </Grid>
</Window>

Detecting Identity

If we build and run the application at this point it will run without identity. Let’s add a small bit of code to detect whether the application has an identity and update the IdentityText TextBlock accordingly. Here are several ways to check:

  • You can attempt to access ApplicationData – this will fail with an exception if the application doesn’t have an identity
// Option 1: Attempt to access ApplicationData
try
{
    var storage = ApplicationData.Current.LocalFolder;
    IdentityText.Text = "Has Identity" ;
}
catch
{
    IdentityText.Text = "No Identity";
}
// Option 2: Use the DesktopBridge.Helpers library
IdentityText.Text = new Helpers().IsRunningAsUwp() ? "Has Identity" : "No Identity";
  • You can import the GetCurrentPackageFullName method which can be used to both determine if the application has an identity and retrieve the full package name. The error code, APPMODEL_ERROR_NO_PACKAGE is a well defined system error code with a note that says the meaning is “The process has no package identity.”
// Option 3: Invoke GetCurrentPackageFullName to get the package name
IdentityText.Text = ApplicationIdentity is string id ? $"Identity {id}" : "No identity";

const long APPMODEL_ERROR_NO_PACKAGE = 15700L;

[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
static extern int GetCurrentPackageFullName(ref int packageFullNameLength, StringBuilder packageFullName);

public string ApplicationIdentity
{
    get
    {
        int length = 0;
        StringBuilder sb = new StringBuilder(0);
        int result = GetCurrentPackageFullName(ref length, sb);

        sb = new StringBuilder(length);
        result = GetCurrentPackageFullName(ref length, sb);

        return result != APPMODEL_ERROR_NO_PACKAGE ? sb.ToString() : null;
    }
}

Registering for Identity

The code for registering for identity is relatively straight forward. However, before we can write this code, we first need to create the sparse package. The sparse package will be registered by the application in order to assign an identity to the application. If you’ve worked with the DesktopBridge, or the Windows Application Packaging project, the next steps will seem familiar.

Creating the Sparse Package

We’re going to use an instance of the Windows Application Packaging (WAP) project, called SparseWPFAppPackaged, to give us the designer support for configuring the manifest file, which will form the foundation of our sparse package. When creating the project, make sure you set both the Target and Minimum windows version to Windows 10, version 2004 (10.0; Build 19041).

Before we can use the visual designer there are a number of tweaks we need to make to the package.appxmanifest file that belongs to the WAP project:

  • Add the uap10 namespace )
  • Add the AllowExternalContent property with a value of true
  • Remove the TargetDeviceFamily entry for Windows.Universal
  • Update the TargetDeviceFamily for Windows.Desktop to specify a MinVersion and MaxVersionTested to 10.0.19041.0
  • Update the Resource language. In this case we’re setting it to en-us but you can set it to whatever language you want, it just can’t be x-generate which is what it was previously.
  • Update the Application attributes. The Id can be whatever value you want but it will need to match what you put in the application manifest file (more on this later). The Executable should be the exact name of the exe file that gets created by your application project, SparseWPFApp.exe in this case. The TrustLevel and RuntimeBehavior values should be set to mediumIL and win32App respectively.
  • Remove the SplashScreen element – for some reason the application fails to register with this value set.
  • Add the unvirtualizedResources capability.
<?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:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
  xmlns:uap10="http://schemas.microsoft.com/appx/manifest/uap/windows10/10"
  IgnorableNamespaces="uap rescap">

  <Identity
    Name="040065f8-4e78-40bd-92b2-3e1f880940e1"
    Publisher="CN=NickRandolph"
    Version="1.0.0.0" />

  <Properties>
    <DisplayName>SparseWPFAppPackaged</DisplayName>
    <PublisherDisplayName>NickRandolph</PublisherDisplayName>
    <Logo>Images\StoreLogo.png</Logo>
    <uap10:AllowExternalContent>true</uap10:AllowExternalContent>
  </Properties>

  <Dependencies>
    <!--<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.0.0" MaxVersionTested="10.0.0.0" />-->
    <TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.19041.0" MaxVersionTested="10.0.19041.0" />
  </Dependencies>

  <Resources>
    <Resource Language="en-us"/>
  </Resources>

  <Applications>
    <!--<Application Id="App"
      Executable="$targetnametoken$.exe"
      EntryPoint="$targetentrypoint$">-->
    <Application Id="SparseWPFApp" 
                 Executable="SparseWPFApp.exe" 
                 uap10:TrustLevel="mediumIL" 
                 uap10:RuntimeBehavior="win32App">

      <uap:VisualElements
        DisplayName="SparseWPFAppPackaged"
        Description="SparseWPFAppPackaged"
        BackgroundColor="transparent"
        Square150x150Logo="Images\Square150x150Logo.png"
        Square44x44Logo="Images\Square44x44Logo.png">
        <uap:DefaultTile Wide310x150Logo="Images\Wide310x150Logo.png" />
        <!--<uap:SplashScreen Image="Images\SplashScreen.png" />-->
      </uap:VisualElements>
    </Application>
  </Applications>

  <Capabilities>
    <!--<Capability Name="internetClient" />-->
    <rescap:Capability Name="runFullTrust" />
    <rescap:Capability Name="unvirtualizedResources"/>
  </Capabilities>
</Package>

Once you’ve updated the package.appxmanifest and saved changes, you can double-click on the package.appxmanifest file in Solution Explorer to open the visual designer where you can update various attributes of the application, and the visual assets for the application. One thing to note is that the visual designer will highlight an error alongside the Entry point textbox – this is completely fine, it’s just that the designer hasn’t been updated to understand that the Entry point isn’t required for a sparse package.

Building the Sparse Package

Normally with a WAP project you’d add the application you want to package to the Applications node and then you’d build the WAP project, Visual Studio is clever enough to build the application first and then include it in the package that’s created by the WAP project. However, a sparse package almost works in reverse – you need to build the sparse package (using the MakeAppx tool that ships with the Windows SDK), sign the generated package (using signtool that again comes with the Windows SDK) and then distribute the package alongside your application.

Rather than have a bunch of command line steps that have to be manually followed, I figured it would be easiest to setup the building of the sparse package as steps that could be run in the pre-build event of the application. There are going to be three steps we need to perform.

  • Copy the package.appxmanifest file to appxmanifest.xml. The MakeAppx tool is expecting appxmanifest.xml file and there’s no easy way to provide the name of the manifest file, without having to manually list every file that needs to be included in the package. Simply copying the manifest file is the easiest solution to this problem.
  • Invoke MakeAppx with the pack argument in order to package all the files in the WAP project (i.e. the manifest file and the associated visual assets). This command needs to specify the /nv argument to disable validation. Validation will fail as it doesn’t understand how to deal with sparse packages.
  • Invoke Signtool with the sign argument in order to sign the generated package. This command needs to reference a pfx file that can be used for signing the package.

Before we list the actual commands that we’ll specify in the pre-build event, we need to generate the signing certificate (i.e. the pfx file). Andrew Leader, in his post, uses the makecert and pvk2pfx commands to generate the pfx. I prefer to use the support in Visual Studio to generate the certificate. Double-click on the package.appxmanifest file in the Solution Explorer and navigate to the Packaging tab. Click on the Choose Certificate button.

In the Choose a Certificate dialog, click Create. The Publisher Common Name (i.e. the CN value in the certificate) should already be set to match the Publisher attribute in the Identity element in the package.appxmanifest. However, if you want to change the Publisher Common Name as part of creating the certificate, Visual Studio will automatically update the Publisher attribute (for example if I were to change the Publisher Common Name to “NickBtr”, the Publisher attribute would be updated to “CN=NickBtr”).

Once the certificate has been created, you’ll be returned to the Choose a Certificate dialog with the details of the certificate shown.

At this point I would recommend installing the certificate so that it’s trusted on your computer – this is required in order for the sparse package to be successfully registered. It’s worth noting that you either need to sign your sparse package with a certificate that’s been issued by a well known CA, or you need to install/provision the certificate on the computer you plan to run the application on.

To install the certificate you’ve just created, click the View Full Certificate button from the Choose a Certificate dialog. Click Install Certificate from the General tab. Select Local Machine (you’ll need to approve the UAC security request); select the Place all certificates in the following store option and then click Browse to find the Trusted People certificate store. Click Next and Finish to complete the installation process.

The final step in creating the signing certificate is to move it. We need to move it out of the WAP project folder where it’s created, up a folder level into the solution folder. This is important because otherwise the signing certificate, which includes the private key, will be included in the package that gets created. As part of moving the signing certificate, I also rename it from the generated name that visual studio gives it (eg SparseWPFAppPackaged_TemporaryKey.pfx) to just signingkey.pfx.

We’re now all set to add the three commands to the pre-build event of the application. Right-click on the application project and select Properties, and then navigate to the Build Events tab. Add the following lines to the Pre-build event command line textbox.

copy "$(SolutionDir)\SparseWPFAppPackaged\package.appxmanifest" "$(SolutionDir)\SparseWPFAppPackaged\appxmanifest.xml" /y
"C:\Program Files (x86)\Windows Kits\10\bin\10.0.19041.0\x64\makeappx" pack /d "$(SolutionDir)\SparseWPFAppPackaged" /p "$(ProjectDir)\package.msix" /nv /o
"C:\Program Files (x86)\Windows Kits\10\bin\10.0.19041.0\x64\signtool" sign /fd SHA256 /a /f "$(SolutionDir)\signingkey.pfx" /p "password" "$(ProjectDir)\package.msix"

Note: Make sure you update the WAP project name (i.e. SparseWPFAppPackaged) to match the name of your WAP project. Also, make sure the path to makeappx and signtool is correct as it may vary based on your installation of Visual Studio.

If you now force a rebuild of the application you’ll see the three commands are invoked prior to the application being built. If the build process appears to hang at this point, or fails with no output, it may be that the pre-event commands are either failing, or requiring user input. I suggest changing the build output verbosity (set via Tools, Options, Projects and Solutions, Build and Run) to either Detailed or Diagnostic in order to see the output of the commands.

Once you’ve run the pre-event commands once, you should see the generated package file, in this case package.msix, appear in the application folder. By default there is no Build Action assigned to this file, which means it won’t be included in the application when it’s built. Click on the package.msix file in the Solution Explorer and set the Build Action to Content, and the Copy to Output Directory to Copy always, in the Properties window.

Note: You may also want to double check that the package.msix has been correctly signed. From File Explorer, right-click the package.msix file and select Properties. Check that the Digital Signatures tab exists and that the digital certificate is correct (look for the Issue attribute and make sure it’s set the same as the Publisher value in your package manifest eg CN=NickRandolph)

The last thing we need to do to include the sparse package is to create an application manifest file for the application (assuming the application doesn’t already have one). If your application doesn’t already have a manifest file, you can add one by using the Application Manifest File (Windows Only) file template. If you do this in Visual Studio, the newly created manifest file will automatically be set as the manifest file for your application. If you create the manifest file manually, make sure you set the ApplicationManifest property in the csproj to be the relative path of the manifest file. In the case of a newly created manifest file, I’m going to replace the generated content with the following.

<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
  <assemblyIdentity version="1.0.0.0" name="MyApplication.app"/>
  <msix xmlns="urn:schemas-microsoft-com:msix.v1"
          publisher="CN=NickRandolph"
          packageName="040065f8-4e78-40bd-92b2-3e1f880940e1"
          applicationId="SparseWPFApp"
        />
</assembly>

If you have an existing manifest file, the important element to add is the msix element.

Here’s the important part. Make sure the entries in the msix element of the application manifest file match the corresponding values in the package.appxmanifest.

  • The publisher attribute in msix element the app manifest should match the Publisher in the Identity element in the package manifest
  • The packageName attribute in msix element the app manifest should match the Name in the Identity element in the package manifest
  • The applicationId attribute in msix element the app manifest should match the Id in the Application element in the package manifest

Note: If you don’t get the values correct in the application manifest, you will be able to successfully register your sparse package but when you attempt to run your application it won’t have an identity. Most likely your application will then attempt to register the sparse package again, leading to an endless loop of your application restarting.

Registering the Sparse Package

Now that we’ve successfully created, built, signed and included the sparse package, the last thing left to do is to register the sparse package. The following code fills in the code for the register, unregister and restart button event handlers. The registerSparsePackage and removeSparsePackage come from the PhotoStoreDemo code. It’s important that the externalLocation and sparsePkgPath values are correct – in this case we use the location of the application executable to determine the externalLocation (which needs to be a folder, not a file). We then assume that the package.msix (i.e. the sparse package created earlier) has been deployed alongside the application, so is in the same folder as the application. If your package is deployed to a different location, you need to update this code accordingly.

private void RegisterClick(object sender, RoutedEventArgs e)
{
    string externalLocation = Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName);
    string sparsePkgPath = Path.Combine(externalLocation, "package.msix");

    //Attempt registration
    var result = registerSparsePackage(externalLocation, sparsePkgPath);
    if (result)
    {
        IdentityText.Text = "Registered for Identity - Click Restart";
    }
    else
    {
        IdentityText.Text = "Unable to register";
    }
}

private void UnregisterClick(object sender, RoutedEventArgs e)
{
    removeSparsePackage();
    IdentityText.Text = "Unregistered - Click Restart";
}

private void RestartClick(object sender, RoutedEventArgs e)
{
    System.Diagnostics.Process.Start(Process.GetCurrentProcess().MainModule.FileName);
    Application.Current.Shutdown();

}


private static bool registerSparsePackage(string externalLocation, string sparsePkgPath)
{
    bool registration = false;
    try
    {
        Uri externalUri = new Uri(externalLocation);
        Uri packageUri = new Uri(sparsePkgPath);

        Console.WriteLine("exe Location {0}", externalLocation);
        Console.WriteLine("msix Address {0}", sparsePkgPath);

        Console.WriteLine("  exe Uri {0}", externalUri);
        Console.WriteLine("  msix Uri {0}", packageUri);

        PackageManager packageManager = new PackageManager();

        //Declare use of an external location
        var options = new AddPackageOptions();
        options.ExternalLocationUri = externalUri;

        Windows.Foundation.IAsyncOperationWithProgress<DeploymentResult, DeploymentProgress> deploymentOperation = packageManager.AddPackageByUriAsync(packageUri, options);

        ManualResetEvent opCompletedEvent = new ManualResetEvent(false); // this event will be signaled when the deployment operation has completed.

        deploymentOperation.Completed = (depProgress, status) => { opCompletedEvent.Set(); };

        Console.WriteLine("Installing package {0}", sparsePkgPath);

        Debug.WriteLine("Waiting for package registration to complete...");

        opCompletedEvent.WaitOne();

        if (deploymentOperation.Status == Windows.Foundation.AsyncStatus.Error)
        {
            Windows.Management.Deployment.DeploymentResult deploymentResult = deploymentOperation.GetResults();
            Debug.WriteLine("Installation Error: {0}", deploymentOperation.ErrorCode);
            Debug.WriteLine("Detailed Error Text: {0}", deploymentResult.ErrorText);

        }
        else if (deploymentOperation.Status == Windows.Foundation.AsyncStatus.Canceled)
        {
            Debug.WriteLine("Package Registration Canceled");
        }
        else if (deploymentOperation.Status == Windows.Foundation.AsyncStatus.Completed)
        {
            registration = true;
            Debug.WriteLine("Package Registration succeeded!");
        }
        else
        {
            Debug.WriteLine("Installation status unknown");
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine("AddPackageSample failed, error message: {0}", ex.Message);
        Console.WriteLine("Full Stacktrace: {0}", ex.ToString());

        return registration;
    }

    return registration;
}

private void removeSparsePackage()
{
    PackageManager packageManager = new PackageManager();
    Windows.Foundation.IAsyncOperationWithProgress<DeploymentResult, DeploymentProgress> deploymentOperation = packageManager.RemovePackageAsync(ApplicationIdentity);
    ManualResetEvent opCompletedEvent = new ManualResetEvent(false); // this event will be signaled when the deployment operation has completed.

    deploymentOperation.Completed = (depProgress, status) => { opCompletedEvent.Set(); };

    Debug.WriteLine("Uninstalling package..");
    opCompletedEvent.WaitOne();
}

Running the Application

Now you should be able to go ahead and run the application. Click the Register button to register the sparse package – when you restart, you should see that your application now has an identity.

Click the Unregister button to return to running without identity – in testing I noticed that removing the sparse package causes the application to immediately be terminated.

Summary

Hopefully in this post you’ve seen how you can create and use a sparse package to provide your application with an identity.

One last point before I sign off – if you run into difficulty and you need to manually unregister your sparse package, you can do this via powershell using the remove-appxpackage command. This command requires knowing the full package name, which can be retrieved using the get-appxpackage command to search based on the Name of the package (this is the Name attribute of the Identity element in the package.appxmanifest file).

3 thoughts on “Unpackaged Windows Apps with Identity using a Sparse Package”

  1. Hi Nick
    Thanks for the post, I have the code working. However I found when registering the identity, it cause an extra entry in the Start Menu, almost like a phantom entry, it launches the app but doesn’t have an icon associated with it. When unregistering the identity the extra entry disappears. Have you seen anything like this?
    Thanks

    Reply

Leave a comment