Mustafa Can Yücel
blog-post-1

WebView2 (WPF) with WiX and MSI

Some Background Information

Windows Installer XML Toolset is a free software toolset that builds Windows Installer packages from XML. It consists of a command-line environment that developers may integrate into their build processes to build MSI and MSM packages. The Windows Installer XML Toolset (WiX) is often regarded as challenging to use due to several reasons. Firstly, its steep learning curve can pose a significant barrier for newcomers. The toolset requires users to have a solid understanding of the underlying Windows Installer technology, including concepts such as components, features, and conditions. Without a grasp of these fundamentals, constructing robust and reliable installation packages can become an arduous task. Secondly, the documentation for WiX can be complex and overwhelming. While there is extensive documentation available, it can be difficult to navigate for beginners and lacks clear, concise explanations for certain concepts and features. This can lead to frustration and wasted time as users attempt to decipher the intricacies of the toolset. Furthermore, the lack of a graphical user interface (GUI) adds to the difficulty level. WiX primarily relies on XML-based authoring, which requires users to manually write and edit XML code to define the installation package's structure, components, and actions. This text-based approach can be cumbersome and prone to errors, especially for those who are more accustomed to visual design tools. The absence of a user-friendly GUI makes it challenging for users to visualize and modify the installation package effectively.

The Microsoft Edge WebView2 control allows you to embed web technologies (HTML, CSS, and JavaScript) in your native apps. The WebView2 control uses Microsoft Edge as the rendering engine to display the web content in native apps. With WebView2, you can embed web code in different parts of your native app, or build all of the native apps within a single WebView2 instance.

Challenges

The main challenges of WiX are

  • creating the correct wxs files by harvesting the source project outputs
  • creating the definitions for directory structure on the installing computer
  • creating the definitions for registry entries
  • creating the definitions for shortcuts
The WiX 4.0 has new additional harvesting tools, and to be fair these are much better than the previous versions. However, the documentation is still quite limited, and if you encounter an error, well, good luck.

The main challenge of WebView2 (WPF) is due to its requirement to put the user files into a directory (User Data Folder, or UDF). If you do not configure any environments, then this folder will be the same as the executable, and this works for portable applications. However, if you install an application properly, it will most likely be under C:\Program Files, which is a protected system directory that apps cannot write into. For this reason, you need to configure the UDF to be under a non-protected location (one common place is %APPDATA%.)

Another common challenge of MSI (Microsoft Software Installer) applications with shortcuts is the relative paths. Let's say you have an assets folder in the same directory as the executable. If you refer to any file under this directory as assets/file.ext in the code, then it will work fine when you run the executable directly. However, if you create a shortcut to the executable, then the executable will not be able to find the assets folder. This is because the executing directory of the shortcut is not the same as the executable and the relative URIs have the base of the executing directory. To solve this problem, you need to use absolute paths and generate these paths based on the executing assembly's location, not the executing directory.

Creating an Installer for an Application That Uses WebView2

Configuring WebView2 Environment

In order to define the UDF for a webview2, we call the configuring method (InitializeAsync()) in the constructor of our window:

public MainWindow()
{
    InitializeComponent();
    InitializeAsync();
}
In this initializer method, we define the UDF as follows:
async void InitializeAsync()
{
    string userDataFolder = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + @"\AppName";
    var environment = await CoreWebView2Environment.CreateAsync(userDataFolder: userDataFolder);
    await webView.EnsureCoreWebView2Async(environment);
    webView.CoreWebView2.Navigate("https://example.com/");
}

Creating the WiX Project

To have the WiX project, you can download the free Heatwave extension for Visual Studio from Fire Giant. This extension will allow you to harvest the outputs of your project and create the wxs files automatically. You can also use the WiX 4.0 tools to harvest the outputs, but it is a bit more complicated. The HeatWave extension will create a WiX project for you, and you can use this project as a template for your own project.

There are different methods for harvesting the outputs of your project. You can read the details from the official documentation. In this post, we will use HarvestDirectory to harvest the outputs of the project.

The outputs of our source project can be the Release build of the project. However, what I prefer is to create a publishing profile with FolderProfile type, and have the following configuration:

  • Configuration: Release|Any CPU (can be x64)
  • Target framework: (project's TF)
  • Deployment mode: Framework-dependent
  • Target runtime: win-x64
  • Target location: (the default location)

To add a WiX project, right-click the solution, then select Add > New Project > WiX > WiX Installer Project. The location of the WiX project can be the same as the source project, but you should select this manually. Once we added the WiX project, we add our source project as a dependency so that it is built before the WiX project. WiX 4.0 works with NuGet packages, for every feature a separate package is available. We will have a default UI, and a .NET version checker, so we will need the following packages that can be installed via NuGet Package Manager:

  • WixNetFxExtension
  • WixUIExtension

Once we have the WiX project, we can start harvesting the outputs. To do this, we need to edit the installer project file itself. To do this, we double-click the project file, and add the following lines (update the publish directory with your own):

  <ItemGroup>
    <HarvestDirectory Include="..\bin\Release\net7.0-windows10.0.17763.0\publish\win-x64">
        <ComponentGroupName>MainComponents</ComponentGroupName>
        <DirectoryRefId>INSTALLFOLDER</DirectoryRefId>
        <SuppressRootDirectory>true</SuppressRootDirectory>
        <SuppressCom>true</SuppressCom>
        <SuppressRegistry>true</SuppressRegistry>
        <AutogenerateGuids>true</AutogenerateGuids>
    </HarvestDirectory>
    <BindPath Include="..\bin\Release\net7.0-windows10.0.17763.0\publish\win-x64" />
    </ItemGroup>
Note that this will harvest everything in your publish directory, including .pdb files. So you should delete anything that you do not want to be harvested before building the installer project.

We will also add an icon file for our shortcut, a banner for installing UI, and a license.rtf file that will be shown during the install wizard. If you add these files to the project, they will automatically be added to the project file. In the end, the project file should look like this:

<Project Sdk="WixToolset.Sdk/4.0.0">
<ItemGroup>
    <HarvestDirectory Include="..\bin\Release\net7.0-windows10.0.17763.0\publish\win-x64">
    <ComponentGroupName>MainComponents</ComponentGroupName>
    <DirectoryRefId>INSTALLFOLDER</DirectoryRefId>
    <SuppressRootDirectory>true</SuppressRootDirectory>
    <SuppressCom>true</SuppressCom>
    <SuppressRegistry>true</SuppressRegistry>
    <AutogenerateGuids>true</AutogenerateGuids>
    </HarvestDirectory>
    <BindPath Include="..\bin\Release\net7.0-windows10.0.17763.0\publish\win-x64" />
</ItemGroup>
<ItemGroup>
    <Content Include="banner.png" />
    <Content Include="icon.ico" />
</ItemGroup>
<ItemGroup>
    <None Include="license.rtf" />
</ItemGroup>
<ItemGroup>
    <PackageReference Include="WixToolset.Heat" Version="4.0.1" />
    <PackageReference Include="WixToolset.Netfx.wixext" Version="4.0.1" />
    <PackageReference Include="WixToolset.UI.wixext" Version="4.0.1" />
</ItemGroup>
<ItemGroup>
    <ProjectReference Include="..\source.csproj" />
</ItemGroup>
</Project>

Once the harvesting is configured, we need to define how the target directory structure will be. For this, we add a Folders.wxs file and add the following lines:

<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
<Fragment>
    <StandardDirectory Id="ProgramFiles6432Folder">
        <Directory Id="INSTALLFOLDER" Name="!(bind.Property.ProductName)" />
    </StandardDirectory>
    <StandardDirectory Id="ProgramMenuFolder">
        <Directory Id="StartMenuDir" Name="!(bind.Property.ProductName)" />
    </StandardDirectory>
</Fragment>
</Wix>
This will create two directories, once under Program Files and one under Start Menu. The INSTALLFOLDER is an id that refers to the installation directory the user selects. Note that we are using !(bind.Property.ProductName) to get the product name. This is a property that is defined in the Product.wxs file, which we will create later. We can refer to these directories with their respective ids; INSTALLFOLDER and StartMenuDir.

Next, we need to configure program shortcuts. You can have multiple shortcuts (one on desktop, one in Start Menu...etc.), but we will create only a single shortcut in the Start Menu. For this, we add a Shortcuts.wxs file and add the following lines:

<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
<Fragment Id="fragmentShortcuts">
    <Icon Id="icon" SourceFile="icon.ico"/>
    <DirectoryRef Id="StartMenuDir">
        <Component Id="StartMenuComponent" Guid="*">
            <Shortcut Id="StartMenuShortcut"
                        Name="!(bind.Property.ProductName)"
                        Description="!(bind.Property.ProductName)"
                        Icon="icon"
                        Target="[INSTALLFOLDER]/program.exe" />
            <RemoveFolder Id="RemoveStartMenuShortcut" On="uninstall" />
            <RegistryValue Root="HKCU" Key="Software\!(bind.Property.ProductName)" Name="installed" Type="integer" Value="1" KeyPath="yes" />
        </Component>
    </DirectoryRef>
</Fragment>
</Wix>
The explanation of the code above is as follows:
  • Icon tag defines the icon that will be used for the shortcut. We are using the icon file we added to the project.
  • DirectoryRef tag defines the directory where the shortcut will be created. We are using the StartMenuDir id we defined in the Folders.wxs file.
  • Component tag defines the component that will be created. We are using the StartMenuComponent id. The Guid attribute is used to uniquely identify the component. We are using * to generate a new GUID for each build.
  • Shortcut tag defines the shortcut that will be created. We are using the StartMenuShortcut id. The Name attribute defines the name of the shortcut. We are using the ProductName property we will define in the Product.wxs file. The Description attribute defines the description of the shortcut. We are using the ProductName property again. The Icon attribute defines the icon of the shortcut. We are using the icon id we defined in the Icon tag. The Target attribute defines the target of the shortcut. We are using the INSTALLFOLDER id we defined in the Folders.wxs file. The RemoveFolder tag defines the folder that will be removed when the application is uninstalled. We are using the RemoveStartMenuShortcut id. The RegistryValue tag defines a registry value that will be created. We are using the ProductName property again. The reason to add a registry entry is you cannot have a KeyPath component without either a file or a registry entry.

Finally, we can combine all the fragments we created into a single file. Note that in order to include a fragment in any other fragment or product, you only need to refer to a component id; the inclusion will automatically be done by the linker. We will add a Product.wxs file and add the following lines:

<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs"
    xmlns:ui="http://wixtoolset.org/schemas/v4/wxs/ui">
    <Package Name="App Name" Manufacturer="Producer" Version="1.0.5.4" UpgradeCode="0d5b56c2-a581-4834-878f-c46268cdcb19" Codepage="Windows-1254">
        <MajorUpgrade DowngradeErrorMessage="!(loc.DowngradeError)" />
        <MediaTemplate EmbedCab="yes" />
        <ui:WixUI
                Id="WixUI_InstallDir"
                InstallDirectory="INSTALLFOLDER" />
        <WixVariable
                Id="WixUILicenseRtf"
                Value="license.rtf" />
        <WixVariable
            Id="WixUIDialogBmp"
            Value="banner.png"  />
        <PropertyRef Id="WIX_IS_NETFRAMEWORK_472_OR_LATER_INSTALLED" />
        <Launch
                Message="!(loc.DotNetError)"
                Condition="Installed OR WIX_IS_NETFRAMEWORK_472_OR_LATER_INSTALLED" />
        <Feature Id="Main">
            <ComponentGroupRef Id="MainComponents" />
            <ComponentRef Id="StartMenuComponent" />
        </Feature>
    </Package>
</Wix>
Let's discuss the code above:
  • We add the wxs ui namespace with the name of UI.
  • We define the codepage if we are going to use any other language than English. WiX has an archaic background, it still has to work with Windows codepages from the 1990s.
  • We add an upgrade code guid manually; the updates are based on this code, so it should not change between builds.
  • We add a major upgrade tag to allow the installer to upgrade the application. We also add a downgrade message that is displayed if the user tries to install a lower version than he has. Note that the message is referred as !(loc.DowngradeError). This allows us to create installers in different languages, and use different messages for each culture. The culture files are named Package.xx-XX.wxl, and automatically selected by the linker based on the culture of the Package. We have not specified any culture, so it will select Package.en-US.wxl
  • We add a UI that allows the user to accept a license, and select an installation location. This UI is named WixUI_InstallDir. For more details, see the official documentation.
  • We point the compiler to our license and banner file. Note that the license file has to be .rtf. The customizable images and their sizes are available in the official documentation.
  • We add a launch condition that checks if the .NET Framework 4.7.2 is installed. If it is not installed, the installer will display an error message and exit.
  • We add a feature that includes the components we created.

Now we can compile our project. We set the configuration of the installer project to Release, and build it. We will get an MSI file in the bin/Release folder.

Installing Prerequisites and Bundles

If you want to install a bunch of software together or want to install prerequisites before your application, you can use bundles. Bundles are a collection of packages that can be installed together. You can also add prerequisites to your bundle. They are quite more complex than regular installers and will be covered in a separate article.

Updates and Patches

If you want to update your application, you can create a patch. Patches are smaller than the original installer and only contain the changed files. They are also more complex than regular installers; for this reason, even the documentation recommends to use major upgrades instead of patches:

If you don’t absolutely need to ship patches, you can avoid the costs of minor upgrades by simply using major upgrades. You can remove files without worrying about component-rule violations if you use an “early” scheduling of the RemoveExistingProducts standard action – before or immediately after the InstallInitialize action.
For this reason, if you want to update your application, just create a new installer with an updated version value. The installer will automatically upgrade the application.