WiX Custom .NET Bootstrapper with UI Part 1

Posted on: May 15, 2020 1:35:36 AM
I recently had the task of creating a custom UI for a WiX installer that uses a bootstrapper. This particular UI had to allow the user to customize their installed features along with installing the primary product. As it turns out, there is not many examples out there of how to best do this. I did find one (https://www.wrightfully.com/part-1-of-writing-your-own-net-based-installer-with-wix-overview), but it wasn't a complete project, so there was still a lot to figure out on my own. I did base a lot of my bootstrapper code from the example I found, it provided a great starting place.
There are a few requirements to get the WiX bootstrapper to use a custom UI.
  1. Your UI project must reference BootstrapperCore - installed with WiX
  2. Your project must contain a BootstrapperCore.config - this provides WiX with the .NET Framework requirements to run your UI
  3. Your project must contain a class that inherits from BootstrapperApplication - this is the entry point for WiX
  4. You must inform the bootstrapper about your project
Since setting a project reference should be pretty self explanatory, I'm going to start with BootstrapperCore.config. My first attempt at adding the config, I tried adding it to the App.config file. As it turns out, this doesn't work, the file must be named BootstrapperCore.config for it to be picked up. This is one of those things that isn't documented very well. Here is the contents this file in the example project I made.
BootstrapperCore.config
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <sectionGroup name="wix.bootstrapper" type="Microsoft.Tools.WindowsInstallerXml.Bootstrapper.BootstrapperSectionGroup, BootstrapperCore">
      <section name="host" type="Microsoft.Tools.WindowsInstallerXml.Bootstrapper.HostSection, BootstrapperCore" />
    </sectionGroup>
  </configSections>
  <startup useLegacyV2RuntimeActivationPolicy="true">
    <supportedRuntime version="v4.0" />
  </startup>
  <wix.bootstrapper>
    <host assemblyName="Bootstrapper.UI">
      <supportedFramework version="v4\Full" />
      <supportedFramework version="v4\Client" />
    </host>
  </wix.bootstrapper>
</configuration>
An important thing to point out in this file that is easy to miss is setting the assemblyName. This value must be the name of the assembly that contains your UI, in my case Bootstrapper.UI. I will admit that I played around with the supportedFramework properties, because my project targets .NET Framework 4.6.2. I wasn't able to find a combination that worked, so I left it at 4.0, it seems to work fine but it may cause issues if the .NET Framework 4.0 is not installed.
Next I'd like to talk about informing the bootstrapper about your project. I'm doing this one next because it's one of the smaller pieces needed. When you're telling the bootstrapper about your project, you need to tell it about your library along with all of it's dependencies. You also need to inform it where to find the BootstrapperCore.config file. Below you will find the contents of my example project bootstrapper.
Bundle.wxs
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
    <Bundle Name="Bootstrapper" Version="!(bind.packageVersion.Msi_Installer)" Manufacturer="Justin" UpgradeCode="1b9106f0-5ba8-4857-ac0a-c1becba1fe51">
    <BootstrapperApplicationRef Id="ManagedBootstrapperApplicationHost">
        <Payload SourceFile="$(var.Bootstrapper.UI.TargetPath)" />
        <Payload SourceFile="$(var.Bootstrapper.UI.ProjectDir)BootstrapperCore.config"/>
        <Payload SourceFile="$(var.Bootstrapper.UI.TargetDir)BootstrapperCore.dll"/>
        <Payload SourceFile="$(var.Bootstrapper.UI.TargetDir)Microsoft.Deployment.WindowsInstaller.dll"/>
    </BootstrapperApplicationRef>

        <Chain>
        <PackageGroupRef Id="NetFx462Web"/>
        
        <MsiPackage Id="Msi_Installer" SourceFile="$(var.Installer.TargetPath)" EnableFeatureSelection="yes" Compressed="yes" />
        </Chain>
    </Bundle>
</Wix>
You'll notice that I'm using variables to inform the project about the file paths. I can do this because I added my project as a reference to the bootstrapper project. You can find a list of all available reference variables at https://wixtoolset.org/documentation/manual/v3/votive/votive_project_references.html.
For this next part, I'm going to only go over the key pieces of it because it's quite a bit larger than the others. This next part is going to be going over the entry point for the UI application, the BootstrapperApplication. The entry point into this class is the Run() method. This is where you will want to determine if UI should be shown or execute the command line options. It's important to remember that you can't just ignore the command line arguments because things like upgrade installations will attempt to uninstall the previous installation using command line arguments. If you don't handle these, the uninstall will never happen and the installers will hang indefinitely without informing your user about anything. Here is a code snippet from my example project.
BootstrapperEntry.cs - snippet
    protected override void Run()
    {
        WaitForDebugger();

        InitializePackages();

        _BootstrapDispatcher = Dispatcher.CurrentDispatcher;

        // should UI be displayed
        if (Command.Display == Display.Full || Command.Display == Display.Unknown)
        {
            Engine.Log(LogLevel.Verbose, "Launching custom UX");

            _InstallerWindowViewModel = new InstallerWindowViewModel(this);

            InstallerWindow installerWindow = new InstallerWindow
            {
                DataContext = _InstallerWindowViewModel
            };
            installerWindow.Closed += (s, e) => _BootstrapDispatcher.InvokeShutdown();
            installerWindow.Show();

            Dispatcher.Run();

            Engine.Quit(_ErrorCode);
        }
        else
        {
            DetectComplete += (sender, args) => Plan(Command.Action);
            PlanComplete += (sender, args) => Execute();
            ExecuteComplete += (sender, args) =>
            {
                Engine.Quit(args.Status);
                _BootstrapDispatcher.InvokeShutdown();
            };

            Detect();

            Dispatcher.Run();
        }
    }
First, with an MSI there are certain steps that always have to happen. You first need to detect the current state, then plan the next state, lastly you execute the plan. So when we aren't showing the UI, these steps still need to occur. The Engine property provided by the base class allows you to register to events during the entire installation process. In this case, we want to register to the completion step of each of these steps so we can automatically start the next step. We then tell then want to push the main execution frame on the event queue by calling Dispatcher.Run().
Debugging can be very difficult, that's why the very first method I call is WaitForDebugger(). This method checks for a command line parameter being passed in of DEBUG. If this value exists, the process will enter a loop checking if a debugger has been attached every half second. This gives you time to attach your favorite debugger to help you troubleshoot from the very beginning. Here is my implementation of that method.
BootstrapperEntry.cs - snippet
    
    private void WaitForDebugger()
    {
        if (Command.GetCommandLineArgs().Contains("DEBUG"))
        {
            Engine.Log(LogLevel.Verbose, "Waiting for debugger to be attached...");

            while (!Debugger.IsAttached)
            {
                Thread.Sleep(500);
            }

            Debugger.Break();
        }
    }
We also need to be able to collect and store the packages and features that are going to be installed. This step is important because it is going to help us tell the installer what to do and tell our UI the what state to show based on what the installer needs to do. At this point, we only know what the installer has in it, we won't know the current state until the Detect() method is called. In order to get what is in the installer, we need to parse an XML file that every bootstrapper creates upon launch. This file is created in the working directory of the installer and can be very easily parsed. I've created POCO classes to hold the information we want to gather from this file. I store this data in a property called Packages
BootstrapperEntry.cs - snippet
    private readonly XNamespace ManifestName = "http://schemas.microsoft.com/wix/2010/BootstrapperApplicationData";
…
    private void InitializePackages()
    {
        const string DataFilePathName = "BootstrapperApplicationData.xml";
        const string ApplicationDataNamespace = "BootstrapperApplicationData";
        const string MbaPrereqNamespace = "WixMbaPrereqInformation";
        const string PackageNamespace = "WixPackageProperties";
        const string FeatureNamespace = "WixPackageFeatureInfo";

        var workingDir = Path.GetDirectoryName(GetType().Assembly.Location);
        var dataFilePath = Path.Combine(workingDir, DataFilePathName);
        XElement applicationData = null;

        try
        {
            using (var reader = new StreamReader(dataFilePath))
            {
                var xml = reader.ReadToEnd();
                var xDoc = XDocument.Parse(xml);
                applicationData = xDoc.Element(ManifestName + ApplicationDataNamespace);
            }
        }
        catch (Exception ex)
        {
            Engine.Log(LogLevel.Error, $"Unable to parse {DataFilePathName}.\nReason: {ex.Message}");
        }

        var mbaPrereqs = applicationData.Descendants(ManifestName + MbaPrereqNamespace)
                                        .Select(x => new MbaPrereqPackage(x));
        // exclude prereq packages
        Packages = applicationData.Descendants(ManifestName + PackageNamespace)
                                      .Select(x => new BundlePackage(x))
                                      .Where(pkg => !mbaPrereqs.Any(preReq => preReq.PackageId == pkg.Id))
                                      .ToArray();

        // get features and associate with their package
        var featureNodes = applicationData.Descendants(ManifestName + FeatureNamespace);
        foreach (var featureNode in featureNodes)
        {
            var feature = new PackageFeature(featureNode);
            var parentPkg = Packages.First(pkg => pkg.Id == feature.PackageId);
            parentPkg.Features.Add(feature);
            feature.Package = parentPkg;
        }
    }
This will conclude part 1 of this series. There is a lot to cover and I feel that splitting it up is the best way to handle it. I will post a link to the complete project in the final part of the series.

Challenge: Digit Fifth Powers

Posted on: August 23, 2017 11:28:58 PM

Surprisingly there are only three numbers that can be written as the sum of fourth powers of their digits:

1634 = 14 + 64 + 34 + 44
8208 = 84 + 24 + 04 + 84
9474 = 94 + 44 + 74 + 44

As 1 = 14 is not a sum it is not included.

The sum of these numbers is 1634 + 8208 + 9474 = 19316.

Find the sum of all the numbers that can be written as the sum of fifth powers of their digits.

This is another relatively straight forward challenge where a modified brute force approach is fastest. I cached the 0-9 fifth powers so they wouldn't always need to be calculated and based on those results, it was clear that a good starting point was 35 since there was no possible way it could be anything between 25 and 35. The next step is finding an upper bound. I started by taking 5x95 and found that the result contained 6 digits. So I put the upper bound to 6x96 to support the 6 digits. The rest is just letting it loop and finding if the on going sum is larger than the initial number.
Program.cs
using System;
using System.Diagnostics;

namespace Project_Euler_30
{
    internal class Program
    {
        // 5x9^5 has 6 digits, so the upper bound should be 6x9^6
        private const int UpperBound = 59049 * 6;

        private static readonly int[] powersCache = new int[] { 0, 1, 32, 243, 1024, 3125, 7776, 16807, 32768, 59049 };

        private static void Main(string[] args)
        {
            Stopwatch sw = Stopwatch.StartNew();
            int result = 0;

            // start the loop at 243 since that is the lower bound (3^5) of a possible sum
            for(int i = powersCache[3]; i <= UpperBound; i++)
            {
                int sum = 0;
                int workingNumber = i;

                while(workingNumber > 0)
                {
                    int digit = workingNumber % 10;
                    workingNumber /= 10;

                    sum += powersCache[digit];

                    if(sum > i)
                    {
                        // passed the number, stop checking
                        break;
                    }
                }

                if(sum == i)
                {
                    result += i;
                }
            }
            sw.Stop();

            Console.WriteLine($"Sum of results: {result}");
            Console.WriteLine($"Took {sw.ElapsedMilliseconds}ms");
        }
    }
}

Challenge: Distinct Powers

Posted on: August 23, 2017 10:17:27 PM

Consider all integer combinations of ab for 2 ≤ a ≤ 5 and 2 ≤ b ≤ 5:

22=4, 23=8, 24=16, 25=32
32=9, 33=27, 34=81, 35=243
42=16, 43=64, 44=256, 45=1024
52=25, 53=125, 54=625, 55=3125

If they are then placed in numerical order, with any repeats removed, we get the following sequence of 15 distinct terms:

4, 8, 9, 16, 25, 27, 32, 64, 81, 125, 243, 256, 625, 1024, 3125

How many distinct terms are in the sequence generated by ab for 2 ≤ a ≤ 100 and 2 ≤ b ≤ 100?

This was very straight forward for me. The most simple way to do this is a simple brute force option. There are ways to calculate out most of the duplicates, but those calculations end up taking longer than 2 simple loops and feed the results into a hashset to guarantee uniqueness to get the answer.
Program.cs
using System;
using System.Collections.Generic;
using System.Diagnostics;

namespace Project_Euler_29
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            Stopwatch sw = Stopwatch.StartNew();
            HashSet<double> results = new HashSet<double>();

            for (int a = 2; a <= 100; a++)
            {
                for (int b = 2; b <= 100; b++)
                {
                    results.Add(Math.Pow(a, b));
                }
            }

            sw.Stop();

            Console.WriteLine($"Number of distinct powers: {results.Count}");
            Console.WriteLine($"Took {sw.ElapsedMilliseconds}ms");
        }
    }
}

Challenge: Number Spiral Diagnols

Posted on: June 14, 2017 12:17:51 AM
Another project euler challenge that states:

Starting with the number 1 and moving to the right in a clockwise direction a 5 by 5 spiral is formed as follows:

21 22 23 24 25
20 7 8 9 10
19 6 1 2 11
18 5 4 3 12
17 16 15 14 13

It can be verified that the sum of the numbers on the diagonals is 101.

What is the sum of the numbers on the diagonals in a 1001 by 1001 spiral formed in the same way?

I found this challenge to be very straight forward in how I would solve it. I know that there is an algebraic way to do it, but I didn't want to find/figure that equation out. I found that the top right corner is going to be the square of the size of the square, so 10012. From there, I can use simple subtraction to find the other corners. Then to get the next number to square will be the floor value of the root value of the bottom left corner. From there, it's a simple loop to go through all values.
Program.cs
using System;
using System.Diagnostics;
using System.Linq;

namespace ProjectEuler_28
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            const int squareSize = 1001;

            Stopwatch sw = Stopwatch.StartNew();

            int half = (int)Math.Ceiling(squareSize / 2D) - 1;
            int[] topRight = new int[half];
            int[] topLeft = new int[half];
            int[] bottomLeft = new int[half];
            int[] bottomRight = new int[half];

            int idx = 0;
            for (int i = squareSize; i > 1;)
            {
                topRight[idx] = i * i;
                topLeft[idx] = topRight[idx] - i + 1;
                bottomLeft[idx] = topLeft[idx] - i + 1;
                bottomRight[idx] = bottomLeft[idx] - i + 1;

                i = (int)Math.Floor(Math.Sqrt(bottomRight[idx]));

                idx++;
            }

            sw.Stop();

            Console.WriteLine($"Sum of diagonols: {topRight.Sum() + topLeft.Sum() + bottomLeft.Sum() + bottomRight.Sum() + 1} in {sw.ElapsedTicks} ticks");
        }
    }
}

Challenge: Quadratic Primes

Posted on: June 13, 2017 1:13:31 AM
Here is another project euler challenge that states:

Euler discovered the remarkable quadratic formula:

n2 + n + 41

It turns out that the formula will produce 40 primes for the consecutive integer values 0 ≤ n ≤ 39. However, when n = 40, 402 + 40 + 41 = 40(40 + 1) + 41 is divisible by 41, and certainly when n = 41, 412 + 41 + 41 is clearly divisible by 41.

The incredible formula n2 - 79n + 1601 was discovered, which produces 80 primes for the consecutive values 0 ≤ n ≤ 79. The product of the coefficients, −79 and 1601, is −126479.

Considering quadratics of the form:

n2 + an + b, where |a| < 1000 and |b| ≤ 1000

where |n| is the modulus/absolute value of n
e.g. |11| = 11 and |-4| = 4

Find the product of the coefficients, a and b, for the quadratic expression that produces the maximum number of primes for consecutive values of n, starting with n = 0.

I tried to find a way to do this outside of brute force, but I couldn't find any fancy formulas or theorems. Knowing that n=0 has to equal a prime number, it was pretty easy to severely narrow down the number of values for b needing to be tested though. n=0, 02 + a(0) + b = b which means b absolutely must be prime. Once you know that b is a prime number, you can also narrow down the values for a by setting n=1. n=1, 12 + a(1) + b = 1 + a + b This result means that, in order to get a prime result, a must be an odd number except when b is 2. This is because all primes, aside from 2, are odd. Knowing this, we can now do a smarter brute force attempt to find the answer.
Program.cs
using Common.Math;
using System;
using System.Diagnostics;
using System.Linq;

namespace ProjectEuler_27
{
    internal class Program
    {
        private static int[] primeCache = Eratosthenes.GetPrimes(1000).ToArray();

        private static bool IsPrime(int num)
        {
            if (num > primeCache.Last())
            {
                primeCache = Eratosthenes.GetPrimes(primeCache.Last() * 2);
            }

            // array.contains checks every value, these are in order from smallest to largest
            // so creating our own contains is more efficient.
            int i = 0;
            while (i < primeCache.Length && primeCache[i] < num)
            {
                i++;
            }

            return primeCache[i] == num;
        }

        private static void Main(string[] args)
        {
            Stopwatch sw = Stopwatch.StartNew();
            ResultHelper result = new ResultHelper();

            // 'b' must be prime because when n=0 we get 0^2 + a(0) + b which is just 'b'
            // since 0 is inclusive and all results must be prime, we can narrow down 'b' to just prime numbers
            int[] bPossibles = primeCache;

            // we can also narrow down 'a' possiblities because all primes except for 2 are odd
            // so if n=1 we get 1^2 + a(1) + b = 1 + a + b
            // since an odd added to an odd is always even, we know that for all cases except 2 'a' must be odd
            // in the case of 2 'a' must be even
            for (int a = -999; a < 1000; a += 2)
            {
                for (int bIdx = 0; bIdx < bPossibles.Length; bIdx++)
                {
                    // handle the case of b=2
                    int workingA = bPossibles[bIdx] == 2 ? a - 1 : a;

                    int n = 0;
                    while (IsPrime(Math.Abs(n * n + workingA * n + bPossibles[bIdx])))
                    {
                        n++;
                    }

                    if (result.Count < n)
                    {
                        result = new ResultHelper
                        {
                            A = a,
                            B = bPossibles[bIdx],
                            Count = n
                        };
                    }
                }
            }

            sw.Stop();

            Console.WriteLine($"{result.Count} primes found in {sw.ElapsedMilliseconds}ms when a={result.A} and b={result.B} with a product of {result.A * result.B}.");
        }

        private struct ResultHelper
        {
            public int A;
            public int B;
            public int Count;
        }
    }
}