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 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 theStartMenuDir
id we defined in theFolders.wxs
file. -
Component
tag defines the component that will be created. We are using theStartMenuComponent
id. TheGuid
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 theStartMenuShortcut
id. TheName
attribute defines the name of the shortcut. We are using theProductName
property we will define in theProduct.wxs
file. TheDescription
attribute defines the description of the shortcut. We are using theProductName
property again. TheIcon
attribute defines the icon of the shortcut. We are using theicon
id we defined in theIcon
tag. TheTarget
attribute defines the target of the shortcut. We are using theINSTALLFOLDER
id we defined in theFolders.wxs
file. TheRemoveFolder
tag defines the folder that will be removed when the application is uninstalled. We are using theRemoveStartMenuShortcut
id. TheRegistryValue
tag defines a registry value that will be created. We are using theProductName
property again. The reason to add a registry entry is you cannot have aKeyPath
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 namedPackage.xx-XX.wxl
, and automatically selected by the linker based on the culture of thePackage
. We have not specified any culture, so it will selectPackage.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.