Part 1 of Writing Your Own .Net-based Installer with WiX - Overview

Header Photo Credit: Lorenzo Cafaro (Creative Commons Zero License)

With the release of the Windows Installer XML Toolkit (WiX) v3.5, a new concept of chained installer bundles was added, and refined in v3.6. This brought two key features to the toolset:

  • The ability to group several individual Microsoft Installer (.msi) files together into a single installer executable and (optionally) have them show as only one product in the Add/Remove Programs list
  • The option to develop your own custom UI for the installer, including one written in .NET

While there are several places to find documentation on the more mature features of WiX, I found that there was almost no good information available around what it takes to write a custom .NET UI for my bundle, so voi la... a blog post (series) is born.

This article is one in my series focusing on what I learned creating a Windows Installer using the WiX toolkit’s "Managed Bootstrapper Application" (Custom .NET UI) framework. Each post builds upon the previous posts, so I would suggest you at least skim through the earlier articles in the series.

Here’s an index into the articles in this series:

In this series I will focus on what I learned creating a "Managed Bootstrapper Application" (Custom .NET UI) using the WiX v3.6 SDK.  I assume you already have a basic understanding of WiX and will touch on some of the other concepts in WiX, but if you're looking for information on how to build an individual MSI, or write custom actions for an MSI in .Net, here are some better references:

Quick concept overview

The general idea is this:  You have one or more "package" installer files (.msi or .exe) that you created or are from third parties (make sure they have redistribution licenses). You want to improve the installation experience for the end user, so you bind all of these installers into a single .exe which will install them in a predetermined order, optionally allowing the user to select which packages and sub-features should be installed. Note that the individual component MSIs are installed via "silent" mode, so their UI will not be shown (assuming they properly implement silent mode).

For example: let's take Microsoft Office as an example. Office has several individual products (Word, Outlook, Excel, etc) which, for this example, we'll assume are each individual MSI files.  But as an end user, I want to run a single installer, choose which products to install, agree to a single EULA, and have just one Add/Remove Programs entry. This can be achieved by creating a bundled installer that includes all of the individual product MSIs.  Also note that Office has its own UI experience, which differs from the typical MSI battleship-gray UI.

Office 2010 Installer with more user-friendly UI MySql installer using more typical installer UI

While I don't know if the Office installer uses WiX (It doesn't, per Rob's comment on this post), I do know that the Visual Studio 2012 installer does, and it has a completely unique installer user experience, built in WPF and .Net on top of the WiX managed bootstrapper framework.

What happens at Runtime

When the user executes your bundled installer, a WiX-provided native (ie: C++) application is initially loaded. This is the "Engine". This portion of the installer is what actually interacts with the Windows Installer APIs.  The Engine does some initial checks to make sure the version of .NET required by our code has been installed, and if we've registered the .NET installer as part of our package chain, it will go ahead and run that installer.  Once .NET is ready, the WiX Engine loads the class we've registered via the [assembly: BootstrapperApplication] assembly attribute.

All communication with the Engine from that point forward is done via the events available through the BootstrapperApplication base class. There's a list of those events at the end of this post.

Once our managed code is loaded, we need to walk through some MSI-specific steps to make the installer work correctly:

First, we'll use Engine.Detect to determine the current state of the machine.  We'll use a set of events to get notifications about the bundle, packages and features that are a part of our installer.  Before we do that, however, we can create some objects to store details about our packages and features using an xml config file WiX encloses in our bundle's files.

Next, we'll use Engine.Plan to set the requested state (the state we want the component to be in when we're done) for each of our packages and features. Again, we'll use a register a set of events for the packages and features, and when they fire, we'll set the requested state values.

Then, we'll use Engine.Apply to tell the engine to apply the requested changes (ie: install or remove the packages and features). During this phase, there will be a set of events we'll use to get progress updates and error information.

Finally, we'll use Engine.Exit to notify the engine we're done, and if the operation was successful, failed or if the user cancelled, etc.

I'll dig into each of these in more detail in the remaining posts in this series.

And much, much more...

The content of this posting grew quite large, so I’ve split it into multiple, more focused postings. But there's so much more than I have put into these posts that I’d like to write about, so I'll likely write some follow-up posts dealing with passing variables to your MSIs and into CustomActions, signing your installer so your UAC prompts show them as coming from a trusted source, etc.  And, hopefully, the WiX site will get some better documentation around the managed bootstrappers soon.

Here’s an index into the articles in this series:

Engine Events

For reference, below is a list of the events exposed on the Bootstrapper base class which are used for async communication with the Engine.

One important note: The events will run on a non-UI thread, so if you're manipulating UI-bound values in your event handlers, make sure to use the Dispatcher.Invoke() command to run that on your UI thread to avoid Exceptions.

        //Events related to the Apply method
        event EventHandler<ApplyBeginEventArgs > ApplyBegin;
        event EventHandler<ApplyCompleteEventArgs > ApplyComplete;

        event EventHandler<RegisterBeginEventArgs > RegisterBegin;
        event EventHandler<RegisterCompleteEventArgs > RegisterComplete;
        event EventHandler<UnregisterBeginEventArgs > UnregisterBegin;
        event EventHandler<UnregisterCompleteEventArgs > UnregisterComplete;

        //Events related to package acquisition. Really only needed if you're building a web installer
        event EventHandler<ResolveSourceEventArgs > ResolveSource;

        event EventHandler<CacheBeginEventArgs > CacheBegin;
        event EventHandler<CachePackageBeginEventArgs > CachePackageBegin;
        event EventHandler<CacheAcquireBeginEventArgs > CacheAcquireBegin;
        event EventHandler<CacheAcquireProgressEventArgs > CacheAcquireProgress;
        event EventHandler<CacheAcquireCompleteEventArgs > CacheAcquireComplete;
        event EventHandler<CacheVerifyBeginEventArgs > CacheVerifyBegin;
        event EventHandler<CacheVerifyCompleteEventArgs > CacheVerifyComplete;
        event EventHandler<CachePackageCompleteEventArgs > CachePackageComplete;
        event EventHandler<CacheCompleteEventArgs > CacheComplete;

        //Events related to the Plan method
        event EventHandler<PlanBeginEventArgs > PlanBegin;
        event EventHandler<PlanRelatedBundleEventArgs > PlanRelatedBundle;
        event EventHandler<PlanPackageBeginEventArgs > PlanPackageBegin;
        event EventHandler<PlanTargetMsiPackageEventArgs > PlanTargetMsiPackage;
        event EventHandler<PlanMsiFeatureEventArgs > PlanMsiFeature;
        event EventHandler<PlanPackageCompleteEventArgs > PlanPackageComplete;
        event EventHandler<PlanCompleteEventArgs > PlanComplete;

        //Events related to the Execute method
        event EventHandler<ExecuteBeginEventArgs > ExecuteBegin;
        event EventHandler<ExecutePackageBeginEventArgs > ExecutePackageBegin;
        event EventHandler<ExecutePackageCompleteEventArgs > ExecutePackageComplete;
        event EventHandler<ExecuteCompleteEventArgs > ExecuteComplete;
        event EventHandler<ProgressEventArgs > Progress;
        event EventHandler<ExecuteProgressEventArgs > ExecuteProgress;
        event EventHandler<ExecutePatchTargetEventArgs > ExecutePatchTarget;
        event EventHandler<ExecuteMsiMessageEventArgs > ExecuteMsiMessage;

        //Events related to error scenarios
        event EventHandler<ErrorEventArgs > Error;
        event EventHandler<ExecuteFilesInUseEventArgs > ExecuteFilesInUse;

        //Events related to start/stops (in the event a reboot is required)
        event EventHandler<StartupEventArgs > Startup;
        event EventHandler<ShutdownEventArgs > Shutdown;
        event EventHandler<SystemShutdownEventArgs > SystemShutdown;
        event EventHandler <RestartRequiredEventArgs > RestartRequired;

        //Events related to the Detect method
        event EventHandler<DetectBeginEventArgs > DetectBegin;
        event EventHandler<DetectPriorBundleEventArgs > DetectPriorBundle;
        event EventHandler<DetectRelatedBundleEventArgs > DetectRelatedBundle;
        event EventHandler<DetectPackageBeginEventArgs > DetectPackageBegin;
        event EventHandler<DetectRelatedMsiPackageEventArgs > DetectRelatedMsiPackage;
        event EventHandler<DetectTargetMsiPackageEventArgs > DetectTargetMsiPackage;
        event EventHandler<DetectMsiFeatureEventArgs > DetectMsiFeature;
        event EventHandler<DetectPackageCompleteEventArgs > DetectPackageComplete;
        event EventHandler<DetectCompleteEventArgs > DetectComplete;

        /// <summary>
        /// Fires when an MSI requests elevated permissions. Not really anything you can do with this.
        /// </summary>
        event EventHandler<ElevateEventArgs > Elevate;