Posts Tagged With WiX - Musing, Rants & Jumbled Thoughts

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

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:

During an installation, it's often expected that the end user can change the installation folder from the default value. Obviously, you'll need some UI controls to allow the user to select the destination folder they want to use, but what do you do with it once you have it?

Changing the install folder location for your installation has two main tasks:

  • Overriding the folder path at install
  • Determining the override used at modify/uninstall/upgrade time

I'll walk through both in this article.

Overriding the Install Folder Path

Let's look at how the default install folder is set. In your individual MSI WiX projects, you'll have a Directory fragment defined that will look something like this:

  <Fragment>
    <Directory Id="TARGETDIR" Name="SourceDir">
      <Directory Id="ProgramFilesFolder">
        <Directory Id="MYINSTALLFOLDER" Name="Wrightfully Blog Examples">

          <!-- ... subfolders -->   
          <Directory Id="dirLicenses" Name="Licenses" />
          <Directory Id="dirdoc" Name="doc" />
        </Directory>
      </Directory>
    </Directory>
  </Fragment>

Notice that some of the Ids are in ALLCAPS. This means they are exposed as variables that can be set by the WiX Engine's Engine.StringVariables array. The override will replace that node in the structure and all child folders will branch off the new path. Overrides will not affect any Directory nodes defined above or beside it in the xml structure -- only it and it's children.

So, if I want to override the install folder defined in my Directory fragment, I'd do something like this:

var pathOverride = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles),
                           "My Alternate Location");

Engine.StringVariables["MYINSTALLFOLDER"] = pathOverride;

Then, when the actual MSIs install, they will substitute the value for MYINSTALLFOLDER to use my new value at install time. But first, you have to tell WiX to pass the MYINSTALLFOLDER along to the MSI, which you do in the <MsiPackage> declaration of your <Bundle>, like this:

  <MsiPackage Compressed="yes" SourceFile="$(var.MyMis.TargetPath)">
      <MsiProperty Name="MYINSTALLFOLDER" Value="[MYINSTALLFOLDER]"/>
  </MsiPackage>

Determining Override Used in Later Runs

Unfortunately, if you override the folder at install time, that's not persisted by the installation system, so you don't know what the value was set to during later runs -- such as when the user clicks Modify/Uninstall in Add/Remove Programs, or when the user goes to upgrade with a newer version of the installer.

So, in order to correctly re-override the value during future runs, we need to store it somewhere and then go grab that value when we run. And we do this with a registry key. You could do this directly in the Bootstrapper Application (managed code), but I prefer to use the WiX functionality to manage the registry key and ensure it's removed when the software is uninstalled, etc. So in an MSI Feature that will always be installed with my software, I put this in my product definition:

<Feature Id="feat_Documentation" Title="Documentation" Level="1" Display="hidden">
    <ComponentGroupRef Id="compgroup_Docs" />

    <Component Directory='MYINSTALLFOLDER'>
        <RegistryValue Root='HKLM' Key='SOFTWARE\\Wrightfully\\BlogInstaller'
                   Name='MyInstallFolder' Value='[MYINSTALLFOLDER]'
                   Type='string'></RegistryValue>
    </Component>
</Feature>

This will create the registry key and populate it with the value of MYINSTALLFOLDER. Then, in my bootstrapper app, I can read the values using this code:

var existingInstallLocation =
    Registry.GetValue(@"HKEY_LOCAL_MACHINE\\SOFTWARE\\Wrightfully\\BlogInstaller",
                    "MyInstallFolder", "").ToString();
if (!string.IsNullOrEmpty(existingInstallLocation))
{
     Engine.StringVariables["MYINSTALLFOLDER"] = existingInstallLocation;
}

If the registry value exists (is not an empty string), then I use that value for [MYINSTALLFOLDER]. Otherwise, I use the default value.

TIP: Since the default value for MYINSTALLFOLDER isn't known by the Bootstrapper App (it's defined in each MSI's Directory fragment), I just always set it to a default value at startup of my bootstrapper (or to the override found in the registry) -- which means the value in the Directory fragments is never actually used.

ANOTHER TIP: Make sure you do some general validation on the user-entered values to make sure it's a valid path. Otherwise, the MSI installs will blow up, and you definitely don't want to deal with that.



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:

You can extract an embedded .msi from your bundle while it's running if you're using a custom bootstrapper application, then extract the contents of that .msi by using the WiX SDK.

Below is a walkthrough of just extracting the files and laying them down on the filesystem. It does not run any of the CustomActions (I'll post how to do that later). I do this all through a class I call MsiFileExtractor - the full source is at the end of this posting, so I'll walk through the methods a little at a time.

First, you need to know a couple of things (which I've put into the cxtr):

  • The path to the executing bundle file, in which the msi files are embedded. You can get this through the WixBundleOriginalSource engine variable
  • This list of features to be installed for each MSI. I'm assuming you don't just want to install the defaults and that you want to select which specific features to install.

Using my MsiFileExtractor class, you'll need to pass into the cxtr the target folder where you want the files extracted into, the list of features to be installed for each MSI, and a reference to the MBA object itself. Then RunInstall() and watch the magic happen.

Here's a high-level overview of what happens:

Step 1: extract MSIs from bundle

The ExtractComponentInstallersFromBundle method is used to extract the .msi files from inside the bundle .exe and place them into a temporary working folder. This is actually pretty straightforward using the WiX Unbinder class. This throws the MSI files into a AttachedContainer subfolder of our temp working folder.

step 2: explode MSIs

Now, we loop through each of the MSIs you want to install and use the InstallPackage method to actually extract the files from them into your target folder.

To do this, I used the WiX InstallPackage class, which wraps our MSI package. Keep in mind, the MSI package is basically just a database with the files embedded. So we'll interact with the database using SQL statements in order to change the behavior of the installer.

step 2a: set the install location in the MSI database

The first thing we need to do is update the install folder. In your .wix files, you should have defined a bunch of Directory targets, like this:

  <Fragment>
    <Directory Id="TARGETDIR" Name="SourceDir">
      <Directory Id="ProgramFilesFolder">
        <Directory Id="APPLICATIONFOLDER" Name="MyCompany">
          <Directory Id="ProductFolder" Name="Demo Product Installer">
            <Directory Id="Cmp1Folder" Name="Component1" />
            <Directory Id="Cmp2Folder" Name="Component2">
                <Directory Id="Cmp3Folder" Name="Component3" />
            </Directory>
          </Directory>
        </Directory>
      </Directory>
    </Directory>
  </Fragment>

Note that I have a top-level folder APPLICATIONFOLDER defined just under the ProgramFilesFolder. All of my files get installed at or below this level. So I need to change the location of this folder in the MSI database to allow me to install somewhere other than Program Files. The way I achieve this is by setting it to "." (the current folder), which will override everything above it in the folder hierarchy as well. I do this in the UpdateMSIDirectoryTableToUseNewApplicationFolder method by changing the DefaultDir value in the Directory table.

step 2b: set the installable features in the MSI database

As I mentioned above, the Level value for each feature determines if it's installed by default of not. Anything that's a Level 1 will be installed. Anything higher will not. So if all you want to install is the Level 1 features, you can skip this step. Otherwise, we need to modify the level values in the MSI's database to change it to 1 for those features we want and to something higher than 1 for the features we don't. I do this in the UpdateMSIFeatureTableToSetInstallableFeatures method by changing the Level value in the Feature table.

step 2c: create a Session

Since we'll basically be using MSI/WiX to "install" these files, and they require a Session to run, we need to create our own session object. This is done using the OpenPackage method -- but we need to tell it to ignoreMachineState, otherwise it will look for this MSI in the system registry and get all confused. I also set the UI level to Silent to prevent the MSI's UI from showing. Make sure to store the old UI level so you can restore it when you're done.

You also need to set the Properties for the session. I do this in SetEngineVarsInSession by copying the properties I want from the Bootstrapper's StringVariable. Modify this as you need.

step 3c: the real work

Now we're ready to actually extract the files, which I do in ExtractPackageFiles. I do this by searching for all of the features in the Feature table with a Level value of 1 (see above about setting that value), then finding all of the entries in the FeatureComponents table for those features, then finding all of the entries in the File table for those components and keeping a list of everything that needs to be installed.

Then, I call InstallPackage.ExtractFiles with that list and BAM I have installed files!

step 3: cleanup

We nuke the temp working folder to keep things tidy.

And here's the full source code:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.Deployment.WindowsInstaller;
using Microsoft.Deployment.WindowsInstaller.Package;
using Microsoft.Tools.WindowsInstallerXml;
using Microsoft.Tools.WindowsInstallerXml.Bootstrapper;
using FileAttributes = System.IO.FileAttributes;

/// <summary>
/// Action will extract the files in the provided MSIs into the provided folder.
/// </summary>    
internal class MsiFileExtractor 
{
    private readonly string _bundlePath;
    private readonly string _targetFolder;
    private readonly Dictionary<string, List<string>> _msiAndFeaturesToInstall;
    private readonly BootstrapperApplication _bootstrapperApplication;

    /// <summary>
    /// </summary>
    /// <param name="targetFolder">File path to the parent folder where files should be installed</param>
    /// <param name="msiAndFeaturesToInstall">collection of msi names and the features in each MSI to be installed</param>
    /// <param name="bootstrapperApplication">instance of the MBA</param>
    public MsiFileExtractor(string targetFolder, Dictionary<string, List<string>> msiAndFeaturesToInstall, BootstrapperApplication bootstrapperApplication)           
    {
        _bootstrapperApplication = bootstrapperApplication; 
        _bundlePath = _bootstrapperApplication.Engine.StringVariables["WixBundleOriginalSource"]; 
        _targetFolder = targetFolder;
        _msiAndFeaturesToInstall = msiAndFeaturesToInstall;        
    }

    /// <summary>
    /// Extract the bundle's embedded MSIs, extract the files from each and run 
    /// any custom actions
    /// </summary>        
    public void RunInstall()
    {
        var workingDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());

        //step 1: extract MSIs from bundle
        ExtractComponentInstallersFromBundle(_bundlePath, workingDir);

        //step 2: explode MSIs
        foreach (var msiName in _msiAndFeaturesToInstall.Keys)
        {
            var featuresToInclude = _msiAndFeaturesToInstall[msiName];
            InstallPackage(workingDir, msiName, featuresToInclude);
        }

        //step 3: cleanup
        if (Directory.Exists(workingDir))
        {
            var di = new DirectoryInfo(workingDir);
            ClearReadOnly(di);

            Directory.Delete(workingDir, true);
        }

    }

    /// <summary>
    /// Recursively clear the ReadOnly attribute from all files and subfolders
    /// for the given folder.
    /// </summary>
    /// <param name="directory"></param>
    private void ClearReadOnly(DirectoryInfo directory)
    {
        directory.Attributes &= ~FileAttributes.ReadOnly;

        foreach (var file in directory.GetFiles())
        {
            file.Attributes &= ~FileAttributes.ReadOnly;
        }

        foreach (var subDir in directory.GetDirectories())
        {
            ClearReadOnly(subDir);
        }
    }

    /// <summary>
    /// For a single MSI, extract the files and run any custom actions
    /// </summary>
    private void InstallPackage(string workingDir, string msiName, List<string> featuresToInclude)
    {
        var msiPath = Path.Combine(workingDir, "AttachedContainer");
        var msiFilePath = Path.Combine(msiPath, msiName);

        Console.WriteLine("Extracting package: {0}", msiName);

        using (var msiPackage = new InstallPackage(msiFilePath, DatabaseOpenMode.Transact) { WorkingDirectory = _targetFolder })
        {
            UpdateMSIDirectoryTableToUseNewApplicationFolder(msiPackage);

            UpdateMSIFeatureTableToSetInstallableFeatures(msiPackage, featuresToInclude);

            //Set the InternalUI level on the MSI so that it doesn't show when we open the Session object
            var prevUILevel = Microsoft.Deployment.WindowsInstaller.Installer.SetInternalUI(InstallUIOptions.Silent);
            try
            {
                using (var session = Microsoft.Deployment.WindowsInstaller.Installer.OpenPackage(msiPackage, ignoreMachineState: true))
                {

                    SetEngineVarsInSession(session);
                    ExtractPackageFiles(session, msiPackage);

                }
            }
            finally
            {
                Microsoft.Deployment.WindowsInstaller.Installer.SetInternalUI(prevUILevel);
            }

            msiPackage.Close();
        }

    }

    private void ExtractPackageFiles(Session session, InstallPackage msiPackage)
    {

        //using a hashset to ensure distinct list
        var fileKeysToInstall = new HashSet<string>();
        foreach (var feature in session.Features)
        {
            var featureLevel = msiPackage.ExecuteIntegerQuery(
                string.Format("SELECT `Level` FROM `Feature` WHERE `Feature` = '{0}'",
                              feature.Name))
                .First();

            if (featureLevel != 1) continue;

            var featureComponents =
                msiPackage.ExecuteStringQuery(
                    string.Format("SELECT `Component_` FROM `FeatureComponents` WHERE `Feature_` = '{0}'",
                                  feature.Name));

            foreach (var installableComponent in session.Components
                .Where(cp => featureComponents.Contains(cp.Name)))
            {
                var componentFileKeys = msiPackage.ExecuteStringQuery(
                    string.Format("SELECT `File` FROM `File` WHERE `Component_` = '{0}'",
                        installableComponent.Name));

                foreach (var fileKey in componentFileKeys)
                {
                    fileKeysToInstall.Add(fileKey);
                }
            }
            msiPackage.ExtractFiles(fileKeysToInstall);
        }

        //temp folder is left behind, so we need to clean it up
        //see http://sourceforge.net/p/wix/bugs/2330/
        var witempPath = Path.Combine(_targetFolder, "WITEMP");
        if (Directory.Exists(witempPath))
        {
            ClearReadOnly(new DirectoryInfo(witempPath));
            Directory.Delete(witempPath, true);
        }
    }

    /// <summary>
    /// Override the APPLICATIONFOLDER in the MSI database so
    /// that files are installed to the target folder.
    /// </summary>        
    private void UpdateMSIDirectoryTableToUseNewApplicationFolder(InstallPackage msiPackage)
    {
        //Update folders
        msiPackage.WorkingDirectory = _targetFolder;

        //The "APPLICATIONFOLDER" special name is used to denote the application top-level folder.
        //In this case, the user has selected their own top-level folder, so we need to
        //replace the existing value with the "." special value, which denotes the extracted
        //folder.               
        var installFolderName = "APPLICATIONFOLDER";
        if (msiPackage.Directories.ContainsKey(installFolderName))
        {
            var record = new Record(".", installFolderName);
            msiPackage.Execute("UPDATE `Directory` SET `DefaultDir` = ? WHERE `Directory` = ?",
                               record);
        }

        msiPackage.UpdateDirectories();
        msiPackage.Commit();
    }

    private void UpdateMSIFeatureTableToSetInstallableFeatures(InstallPackage database, List<string> featuresToInstall)
    {
        bool installDefault = !featuresToInstall.Any();
        if (installDefault) return;   

        var packageFeatures = new List<string>();

        if (database.Tables.Contains("Feature"))
        {
            packageFeatures.AddRange(database.ExecuteStringQuery("SELECT `Feature` FROM `Feature`"));

            foreach (var feature in packageFeatures)
            {
                //a Level of "1" will be installed by default, anything higher than that will not,
                // so by setting all others to "100", they will not be installed/extracted
                int newInstallLevel = featuresToInstall.Contains(feature) ? 1 : 100;

                Console.WriteLine("Setting feature {0} to install level {1}", feature, newInstallLevel);

                using (var record = new Record(newInstallLevel, feature))
                {
                    database.Execute("UPDATE `Feature` SET `Level` = ? WHERE `Feature` = ?",
                                     record);
                }
            }
        }
        else
        {
            throw new Exception("Feature table not found");
        }
        database.Commit();

    }

    private void SetEngineVarsInSession(Session session)
    {
        var propertiesToCopy = new[] {"PROP1", "PROP2"};
        foreach (var property in propertiesToCopy)
        {
            var propValue = _bootstrapperApplication.Engine.StringVariables[property];
            session[property] = propValue;
        }
    }

    private void ExtractComponentInstallersFromBundle(string bundlePath, string tmpFolder)
    {
        Unbinder unbinder = null;

        try
        {
            unbinder = new Unbinder();
            unbinder.Message += MessageEventHandlerMethod;
            unbinder.Unbind(bundlePath, OutputType.Bundle, tmpFolder);
        }
        finally
        {
            if (null != unbinder)
                unbinder.DeleteTempFiles();
        }

    }

    private void MessageEventHandlerMethod(object sender, MessageEventArgs e)
    {
        Console.WriteLine("MESSAGE: ID: {0}, LEVEL: {1}, RESOURCE: {2}, LINES: {3}",
                   e.Id,
                   e.Level,
                   e.ResourceName,
                   e.SourceLineNumbers);
    }

}


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:

UAC prompt

UAC, if you're not familiar, is the feature in Windows where applications that need elevated permissions will "ask" for it by having Window pop up a "This app is trying to do something serious -- Do you want to allow it?" dialog, while completely blocking out the rest of your desktop. Many installers require UAC, because they install "Machine Wide" (ie: not specific to just the current User).

In the world of installers, there are two basic camps: force the permission elevation up front or request it only when you need it. The former is easier, the latter is what Microsoft wants you to do. The big catch: you can only request elevated permissions when you start a process - so once a process is already loaded, you can't elevate it. Instead, you must start up a new process and use inter-process communication to feed data back and forth.

In other .Net apps, to force the elevation up front, you just create an assembly manifest file with the appropriate markers. However, the manifest in this case is actually associated with the WiX engine and you can't just change it. If you try to extract the manifest using the typical .net tools, modify it and reattach, it changes the byte signature of the assembly, and it appears that WiX uses hard-coded byte offsets to determine where the embedded files are located. The result: changing the manifest causes the engine to fail to find your assemblies. You could download the WiX source code, change the embedded manifest file, recompile, etc. Be careful, though -- I'm not a lawyer, but I believe the WiX reciprocal-style open source license would require you to open source your installer code if you go this route.

For the most part, the MSIs are the only parts that will need elevated access, and WiX takes care of all of that for you. The only time you really need to worry about it is if you need to do something in your bootstrapper itself that requires elevated permissions (such as certain Registry operations, IIS operations, etc).

Also note that the base class exposes an Elevate() method. This will force the WiX engine to spin up the elevated process it uses for running the MSIs when you call it (assuming it hasn't already been created). It does not elevate the current process (ie: your code). Sorry.

There are a couple of other notes:

UAC shield

If UAC is in play, it is suggested you show the little "shield" icon on the button that immediately precedes the action that will prompt for elevation. But if UAC is not enabled, you shouldn't show it. So how do you know if you should show the shield? That's pretty easy, actually. There are two rules:

  • The operating system supports UAC (is a version after WinXP). Here's a little helper to make that determination:
/// <summary>
/// True if the current operating system supports UAC.
/// Vista or higher only.
/// </summary>
internal static bool OperatingSystemSupportsUAC
{
    get { return Environment.OSVersion.Version.Major >= 6; }
}
  • The process is not already running in administrator mode. If the user choose "Run as Administrator", or if it was started by another administrator process (such as the Add/Remove programs control panel), then it will not need to ask for elevation permissions. Here's another helper: (add using System.Security.Principal; to your file)
/// <summary>
/// True if the current user is in the Admin group, or the process
/// is already running with elevated permissions.
/// </summary>
private static bool UserIsRunningAsAdmin
{
    get
    {
        var principal = new WindowsPrincipal(WindowsIdentity.GetCurrent());
        return principal.IsInRole(WindowsBuiltInRole.Administrator);
    }
}

So, add them together, and you get this:

var ShieldDisplayRequired = OperatingSystemSupportsUAC && !UserIsRunningAsAdmin;

Getting access to the actual shield and putting it on a button is another issue altogether. I used a combination of the concepts raised in these two postings, along with some special sauce of my own, which I won't repost here. But you should be able to get a good start from these links:

Note, though, that if your installer is not also signed, then the UAC prompt will alert the user that the installer is untrusted. To really do this right, you should sign the installer too, but there’s a couple extra steps you must take to do that. I’ll post about that separately.



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:

Running the action

Once the Engine's planning phase is complete (see previous post), we can call Engine.Apply(IntPtr.Zero) to execute the action. Note: the parameter to Apply() is a window handle. You could also pass handle to your UI window, but getting that value is beyond what I want to get into here. I believe this is used by Windows when prompting for UAC, but this all works fine with IntPtr.Zero. The Apply action will provide status via events as well:

BootstrapperApplication.ExecuteMsiMessage += EventProviderOnExecuteMsiMessage; 
BootstrapperApplication.Progress += EventProviderOnProgress; 
BootstrapperApplication.ExecuteComplete += EventProviderOnExecuteComplete; 
BootstrapperApplication.ExecuteFilesInUse += EventProviderOnExecuteFilesInUse; 
BootstrapperApplication.ExecutePackageBegin += EventProviderOnExecutePackageBegin; 
BootstrapperApplication.ExecutePackageComplete += EventProviderOnExecutePackageComplete; 
BootstrapperApplication.ApplyComplete += BootstrapperOnApplyComplete;

this.Engine.Apply(IntPtr.Zero); 

The ExecuteMsiMessage callback provides some non-user friendly message from the MSI engine, which I just log. I use the ExecuteComplete to set my percentage variables to 100% (since the last progress event might only by 95% or so) and the ExecutePackageBegin and ExecutePackageComplete to set a UI label saying "Currently installing XXX package".

The ExecutePackageBegin includes the packageId for the package currently being installed. You'll need to use that to lookup the Name, etc, from the Package models you generated previously (see Part 2 of this series).

private void EventProviderOnExecutePackageBegin(object sender, ExecutePackageBeginEventArgs executePackageBeginEventArgs) 
{
    var inFlightPkgId = executePackageBeginEventArgs.PackageId;
    var inFlightPkg = BundlePackages.FirstOrDefault(pkg => pkg.Id == inFlightPkgId);

    if (inFlightPkg == null)
    {
        CurrentlyProcessingPackageName = string.Empty;
    }
    else
    {
        CurrentlyProcessingPackageName = inFlightPkg.Name;
    }
} 

While the action is being performed, progress will be provided via events, which will tell you which package is currently being installed, percentage completions, etc. One note: if a rollback is initiated due to a failure or error in the install, the progress percentages will decrease as the rollback occurs. Also note, this is the best place to handle user cancellations (see below section for that topic).

private void EventProviderOnProgress(object sender, ProgressEventArgs progressEventArgs) 
{ 
    //update local properties (which are likely bound to a ProgressBar or something 
    CurrentComponentProgressPercentage = progressEventArgs.ProgressPercentage; 
    OverallProgressPercentage = progressEventArgs.OverallPercentage;

    //... handle user cancellations here

} 

For the ExecuteFileInUse, I haven't actually been successful in forcing this error, but here's an example of the code I have to handle it:

private void EventProviderOnExecuteFilesInUse(object sender, ExecuteFilesInUseEventArgs executeFilesInUseEventArgs) 
{
    var message = new StringBuilder("The following files are in use. Please close the applications that are using them.n"); 
    foreach (var file in executeFilesInUseEventArgs.Files) 
    { 
        message.AppendLine(" - " + file); 
    }

    var userButton = MessageBox.Show(message.ToString(), "Files In Use", MessageBoxButto.OKCancel, MessageBoxImage.Warning);

    if (userButton != MessageBoxResult.OK)
        executeFilesInUseEventArgs.Result = Result.Cancel;
} 

When the apply action is complete, it will fire the ApplyComplete handler. This is where you can set status, etc. Here is what my method looks like:

private void BootstrapperOnApplyComplete(object sender, ApplyCompleteEventArgs applyCompleteEventArgs) 
{ 
    BootstrapperApplication.ApplyComplete -= BootstrapperOnApplyComplete;

    //using "ActionResult" property to store the result for use
    // when I call Engine.Quit()

    if (applyCompleteEventArgs.Status >= 0)
    {
        ActionResult = ActionResult.Success;
    }
    else
    {
        ActionResult = ActionResult.Failure;
    }
} 

Finishing it out

When you're ready to close down the app, even if you haven't actually done anything (user cancellation, etc), you'll need to call Engine.Quit() with one of these ActionResult values: (hint: you'll need to cast the Enums to an int)

ActionResult.Success
The action was run and was successful.
ActionResult.UserExit
Action was cancelled by user, therefore was unsuccessful.
ActionResult.Failure
Action had errors and was unsuccessful.
ActionResult.NotExecuted
No actions were performed.

Be careful that you return the correct values, as that affects the return code of the executable (which the caller may be monitoring in a scripted install) as well how the Add/Remove Programs list shows (or doesn't show) your app and if locally cached instances of your installer are deleted, etc. An incorrect return value may result in an Add/Remove programs listing that never gets removed on uninstall, or never shows up in the first place, and other oddities.

Note too that you'll need to handle closing your UI windows, etc, on your own -- calling Engine.Quit() does not close your UI or terminate your threads, etc.

Engine.Quit((int) ActionResult.Success);

Canceling while action is being performed

Most of the WiX events have an EventArgs parameter that includes a Result property. This can be used to cancel asynchronous engine operations. For instance, the progress event fires frequently, so is a good candidate. If the user clicks your cancel button during the install, you can set Result=Result.Cancel on the next event to signal the engine to stop and initiate the rollback process. This assumes you provide the user a cancel button. You'll also want to monitor if the user closes your UI window using the "X" button in the top right corner.

private void EventProviderOnProgress(object sender, ProgressEventArgs progressEventArgs) 
{ 
    //.... 
    if (_userHasCancelled) 
        progressEventArgs.Result = Result.Cancel; 
} 


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:

Detecting current state

In order to determine which, if any, of the bundled packages and features, as well as the bundle itself, are already installed on the system, and at what versions, we'll ask the WiX engine to detect current state by calling Engine.Detect() on the BootstapperApplication base class. Note this is an asynchronous process, so the call will return immediately and we'll get our results via a series of events. As such, before calling Engine.Detect(), we need to register our event handlers for the detect events (these are also on the base class). Note: You may not need both DetectPackageComplete and DetectRelatedMsiPackage, depending on your needs.

//
// Call Engine.Detect, asking the engine to figure out what's on the machine.
// The engine will run async and use callbacks for reporting results.
//

// This is called when the bundle is detected
BootstrapperApplication.DetectRelatedBundle += HandleExistingBundleDetected;

// This is called when a package in the bundle is detected
BootstrapperApplication.DetectPackageComplete += SetPackageDetectedState;

// This is called when a package in the bundle is detected
BootstrapperApplication.DetectRelatedMsiPackage += HandleExistingPackageDetected;

// This is called when a Feature in the bundle's packages is detected
BootstrapperApplication.DetectMsiFeature += SetFeatureDetectedState;
BootstrapperApplication.DetectComplete += DetectComplete;
BootstrapperApplication.Detect();

As the engine determines a package or feature from our bundle is already on the system, it will fire the associated event. The specialized EventArgs parameter provided to each event handler will have the packageId (and featureId for features) and the current state. As each of these fire, you'll want to search the package and feature models you've built to find the package/feature identified by the event args, and set the current state Enum on each based on the incoming value.

Note: in these examples, the "BundlePackages" variable is a reference to the collection of Package model objects I suggested you create in Part 3 (Context Data)

private void HandleExistingPackageDetected(object sender, DetectRelatedMsiPackageEventArgs e)
{
    string existingPackageProductCode = e.ProductCode;

    RelatedOperation actionToBeApplicedToExistingPackage = e.Operation;
    string existingPackageId = e.PackageId;
    Version existingPackageVersion = e.Version;

    //update your model objects here (search models by PackageId)
}

private void HandleExistingBundleDetected(object sender, DetectRelatedBundleEventArgs e)
{
    Version existingBundleVersion = e.Version;
    string existingBundleProductCode  = e.ProductCode;
    RelatedOperation actionToBeAppliedToExistingBundle = e.Operation;

    //update your model object here
}

/// <summary>
/// when engine detects a package, populate the appropriate local objects,
/// including current installed state of the package on the system
/// </summary>
private void SetPackageDetectedState(object sender, DetectPackageCompleteEventArgs args)
{
    var package = BundlePackages.FirstOrDefault(pkg => pkg.Id == args.PackageId);
    PackageState currentState = args.State;
    package.CurrentInstallState = currentState;
}

/// <summary>
/// when engine detects a feature, populate the appropriate local objects,
/// including current installed state of the package on the system
/// </summary>
private void SetFeatureDetectedState(object sender, DetectMsiFeatureEventArgs args)
{
    var package = BundlePackages.FirstOrDefault(pkg => pkg.Id == args.PackageId);
    var feature = package.AllFeatures.FirstOrDefault(feat => feat.Id == args.FeatureId);
    FeatureState currentState = args.State;

    feature.CurrentInstallState = args.State;
}

Below are the values for RelatedOperation:

public enum RelatedOperation
{
    None,

    /// <summary>
    /// The related bundle or package will be downgraded.
    /// </summary>
    Downgrade,

    ///<summary>
    /// The related package will be upgraded as a minor revision.
    ///</summary>
    MinorUpdate,

    ///<summary>
    /// The related bundle or package will be upgraded as a major revision.
    ///</summary>
    MajorUpgrade,

    ///<summary>
    /// The related bundle will be removed.
    ///</summary>
    Remove,

    ///<summary>
    /// The related bundle will be installed.
    ///</summary>
    Install,

    ///<summary>
    /// The related bundle will be repaired.
    ///</summary>
    Repair,
};

Below are the values for FeatureState. For the most part, you'll likely only care about Unknown (state not yet discovered), Absent (not installed) and Local (installed).

public enum FeatureState
{
    Unknown, 
    Absent,  
    Advertised, 
    Local, 
    Source,
}

Below are the values for PackageState. For the most part, you'll likely only care about Unknown (state not yet discovered), Absent (not installed) and Present (installed)

public enum PackageState
{
    Unknown,
    Obsolete,
    Absent,
    Cached,
    Present,
    Superseded,
}

When the Detect action is complete, it will fire the DetectComplete handler, where you'll want to perform whatever UI actions you require before moving forward with the installation, such as prompting the user if they want to run a "typical" or "custom" install, or if packages are already installed, asking if they want to fully uninstall or just add/remove features. Here's what my method looks like:

    /// 
    /// Once the engine completes the Detect phase, unregister event handlers,
    /// release the main thread and register the planning phase event handlers
    /// 
    void DetectComplete( object sender, DetectCompleteEventArgs e)
    {
        BootstrapperApplication.DetectPackageComplete -= SetPackageDetectedState;
        BootstrapperApplication.DetectMsiFeature -= SetFeatureDetectedState;
        BootstrapperApplication.DetectComplete -= DetectComplete;

       //logic to continue here — likely to allow user to select package state, etc, in the UI
    }

Planning future state

Before we tell the windows installer to go off and run the install actions, we need to tell it what we want it to do. This is achieved by calling Engine.Plan() with an action enum (install, uninstall, etc) on the BootstrapperApplication base class. Similar to the Detect() sequence, this will initiate an asynchronous process, so before calling, we need to register event handlers:

        BootstrapperApplication.PlanPackageBegin += SetPackagePlannedState;
        BootstrapperApplication.PlanMsiFeature += SetFeaturePlannedState;
        BootstrapperApplication.PlanComplete += BootstrapperOnPlanComplete;
        this.Engine.Plan(LaunchAction.Install);

The engine will then fire an event for each package and feature in our bundle, each with a specialized event args parameter. Our job is to set the requested state on the event args for each.

    /// 
    /// when engine plans action for a package, set the requested future state of
    /// the package based on what the user requested
    /// 
    private void SetPackagePlannedState( object sender, PlanPackageBeginEventArgs planPackageBeginEventArgs)
    {
        var pkgId = planPackageBeginEventArgs.PackageId;
        var pkg = BundlePackages.FirstOrDefault(p => p.Id == pkgId);

        //I’m assuming a property “RequestedInstallState” on your model
        //of type RequestState.
        planPackageBeginEventArgs.State = pkg.RequestedInstallState;
    }

    /// 
    /// when engine plans action for a feature, set the requested future state of
    /// the package based on what the user requested
    /// 
    private void SetFeaturePlannedState( object sender, PlanMsiFeatureEventArgs planMsiFeatureEventArgs)
    {           
        var pkg = BundlePackages.First(p => p.Id == planMsiFeatureEventArgs.PackageId);
        var feature = pkg.AllFeatures.First(feat => feat.Id == planMsiFeatureEventArgs.FeatureId);

        //I’m assuming a property “RequestedState” on your model
        //of type FeatureState.
        planMsiFeatureEventArgs.State = feature.RequestedState;
    }

Below are the values for FeatureAction. For the most part, you'll likely only care about None (don't change from the current state), AddLocal (install it), Reinstall, Remove (uninstall).

public enum FeatureAction
{
    None,
    AddLocal,
    AddSource,
    AddDefault,
    Reinstall,
    Advertise,
    Remove,
}

Below are the values for RequestState. For the most part, you'll likely only care about None (don't change from current state), ForceAbsent (force uninstall), Absent (uninstall), Present (install) and Repair.

public enum RequestState
{
    None,
    ForceAbsent,
    Absent,
    Cache,
    Present,
    Repair,
}

When the plan action is complete, it will fire the PlanComplete event handler. This is where you'll want to start up the next set of actions. In my case, I'm going straight from Plan to Apply (see below), so my method looks like this:

private void BootstrapperOnPlanComplete(object sender, PlanCompleteEventArgs args)
    {           
        BootstrapperApplication.PlanComplete -= BootstrapperOnPlanComplete;

        //Code to initiate Apply action goes here.. See Part 5 post for more details.
    }


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:

Getting access to installer (and package) metadata

A lot of information is embedded in the WiX xml files, such as package/feature layout, names, descriptions, ids, etc, which we use to build out our bundle models, but almost none of it is made available at runtime via the event args. However, WiX does generate a BootstrapperApplicationData.xml file which includes a lot of that information and is included in the files available at runtime. We can parse that file at runtime in order to access that metadata, which I suggest you do before you run the detection logic (see below) in order to have a populated model to use in the event handlers. Since the file, along with all of our assemblies and .msi files, are placed in a randomly-name temp folder, we can’t know ahead of time where the file will live, so we must use our assembly’s path to find it.

You can then parse the XML to get the metadata. I would suggest running a makeshift installer in debug mode and setting a breakpoint here to inspect the contents of the XML in order to get a full list of what’s available. Here’s an example of how I get data from the file. Note: in this example, my domain objects are MBAPrereqPackage, BundlePackage and PackageFeature, each of which take an XML node object in their constructor and further parse the data into the object’s properties.

const  XNamespace ManifestNamespace = ( XNamespace) “http://schemas.microsoft.com/wix/2010/BootstrapperApplicationData” ;

public void Initialize()
{

    //
    // parse the ApplicationData to find included packages and features
    //
    var bundleManifestData = this.ApplicationData;
    var bundleDisplayName = bundleManifestData 
                              .Element(ManifestNamespace + “WixBundleProperties“ )
                              .Attribute( “DisplayName“)
                              .Value;

    var mbaPrereqs = bundleManifestData.Descendants(ManifestNamespace + “WixMbaPrereqInformation“)
                                       .Select(x => new MBAPrereqPackage(x))
                                       .ToList();

    //
    //exclude the MBA prereq packages, such as the .Net 4 installer
    //
    var pkgs = bundleManifestData.Descendants(ManifestNamespace + “WixPackageProperties“)
                                 .Select(x => new BundlePackage(x))
                                 .Where(pkg => !mbaPrereqs.Any(preReq => preReq.PackageId == pkg.Id));

    //
    // Add the packages to a collection of BundlePackages
    //
    BundlePackages.AddRange(pkgs);

    //
    // check for features and associate them with their parent packages
    //
    var featureNodes = bundleManifestData.Descendants(ManifestNamespace + “WixPackageFeatureInfo“);
    foreach ( var featureNode in featureNodes)
    {
       var feature = new PackageFeature(featureNode);
       var parentPkg = BundlePackages.First(pkg => pkg.Id == feature.PackageId);
       parentPkg.AllFeatures.Add(feature);
       feature.Package = parentPkg;
    }
}

/// 
/// Fetch BootstrapperApplicationData.xml and parse into XDocument.
/// 
public XElement ApplicationData
{
    get
    {
        var workingFolder = Path.GetDirectoryName(this.GetType().Assembly.Location);
        var bootstrapperDataFilePath = Path.Combine(workingFolder, “BootstrapperApplicationData.xml”);

        using (var reader = new StreamReader(bootstrapperDataFilePath))
        {
            var xml = reader.ReadToEnd();
            var xDoc = XDocument.Parse(xml);
            return xDoc.Element(ManifestNamespace + “BootstrapperApplicationData“);                   
        }
    }
}

Access to command line parameters (Install/Upgrade/Modify/Uninstall, Silent mode, etc)

Along with the Engine property provided in the base class, a Command property is also exposed. There are a few properties off that Command object that are very useful:

The Action property, which exposes a LaunchAction enum value, tells you how the installer was initiated. If the user just double-clicked on the executable, it will come in as Install, but if command-line parameters are used to execute a specific action, that will be translated into this enum. This includes clicking “Uninstall” from the Add/Remove programs list, etc.

/// 
/// Requested action from the commandline
/// 
public LaunchAction RunMode { get { return Command.Action; } }

public enum LaunchAction
{
  Unknown,
  Help,
  Layout,
  Uninstall,
  Install,
  Modify,
  Repair,
}

The Display property, which exposes a Display enum value, tells you if the user wants silent mode, etc. These map to the Windows Installer commandline allowed values.

/// 
/// Requested display mode from the commandline
/// (Full, Passive/Silent, Embedded)
/// 
public Display DisplayMode { get { return Command.Display; } }

public enum Display
{
  Unknown,
  Embedded,
  None,
  Passive,
  Full,
}

And then the CommandLine property exposes the rest of the command line. Note that WiX will actually remove several of the parameters that are exposed via other properties (Display, Action, etc)

/// 
/// Full application command line
/// 
public IEnumerable<string> CommandLine { get { return (Command.CommandLine ?? string.Empty).Split(‘ ‘ ); } }


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:

You’ll need at least two projects in your Visual Studio solution: The Bootstrapper project and a .Net assembly for it to run.  I would suggest adding a WPF project for your .Net assembly.

The Bootstrapper Project

Creating the bundle itself uses XML files similar to the Package WiX files you may already be accustomed to. You’ll need to install the WiX toolkit “Votive” component, which provides Visual Studio templates and integration.

In your Visual Studio solution, you’ll first, add a "Bootstrapper" project, which will add some placeholder .wxs files.

There's a <Bundle> element, which includes general metadata about the top-level installer (name, version, etc).  If you're building your own UI using .NET, you'll need to include <BootstrapperApplicationRef Id="ManagedBootstrapperApplicationHost"> where ManagedBootsrapperApplicationHost is a pre-defined trigger for WiX to load your managed UI. Inside that tag, you'll need to define a <PayloadGroup> element (or <PayloadGroupRef> to define the group elsewhere).  The PayloadGroup defines files that are unpacked at runtime along with your assembly and is used for other assemblies yours depends upon.  You'll want to include BootstrapperCore.config and Microsoft.Deployment.WindowsInstaller.dll in any case, as well as you're installer assembly.

Then, you'll need a <Chain> element to define the MSI's that will be installed by the bundle.  You'll likely want to include one of the pre-defined .NET installers to ensure .Net is on the system before you're .Net-based UI is loaded.  In the example below, I'm using the NetFx40Web PackageGroup which pulls the .Net 4.0 web installer, but only if .Net 4 (full) is not already installed on the system.  The web installer UI will be shown to the user before any of your code is executed.   One HUGE word of caution here:  Because the .Net installer will technically be part of your install chain, if the user installs .Net but then cancels your install, your installer will still be listed in the Add/Remove programs since one if it's components (the .Net installer) completed.  Tread with caution.

Of final note: There is a <MsiProperty> element inside the MsiPackage tag that is used to allow Engine variables (things you can set in your code) to pass-through to the MSIs.

<?xml version="1.0" encoding =" UTF-8" ?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"
     xmlns:util="http://schemas.microsoft.com/wix/UtilExtension" >

  <Bundle Name="My Super Great Product Bundle"
          Version="1.0.0.0"
          Manufacturer="John M. Wright"
          IconSourceFile="jwright.ico"
          UpgradeCode="{D4578DG3-ABCD-1234-8693-ACAAF4A3A785}"
          AboutUrl="http://wrightthisblog.blogspot.com"
          Compressed="yes" >
   
      < BootstrapperApplicationRef Id =" ManagedBootstrapperApplicationHost" >
               < PayloadGroupRef Id =" InstallerPayload" />
      </ BootstrapperApplicationRef>

      <Chain>
           <!-- Install .Net 4 Full -->
           < PackageGroupRef Id =" NetFx40Web" />

           <!— my packages to install -->
           < PackageGroupRef Id =" InstallerPackages" />     
       </ Chain>
   </ Bundle>

 < Fragment>
    < PayloadGroup Id =" InstallerPayload">
      < Payload SourceFile =" $(var.jwright.Installer.TargetPath)"/>
      < Payload SourceFile="$(var.jwright.Installer.TargetDir)\\BootstrapperCore.config" />     
      < Payload SourceFile="$(var.jwright.Installer.TargetDir)\\Microsoft.Deployment.WindowsInstaller.dll" />
    </ PayloadGroup>
 </ Fragment>

 < Fragment>
    < PackageGroup Id =" InstallerPackages" >

      < MsiPackage SourceFile="$(var.MyProductInstaller.TargetPath)"
          Compressed="yes" EnableFeatureSelection="yes" Vital="yes">

        < MsiProperty Name="APPLICATIONFOLDER" Value="[MyInstallFolder]" />
      </ MsiPackage>

      < MsiPackage SourceFile="$(var.AnotherProductInstaller.TargetPath)"
          Compressed="yes" EnableFeatureSelection="yes" Vital="yes">

    </ PackageGroup>
  </Fragment>
</Wix>

Your .Net Assembly Project

Once the native code bootstrapper loads up, it will attempt to load our managed code.  We must do two things in order to get the handoff and communication working.

First, we must create a class that extends the WiX BootstrapperApplication base class.  This class must then override the Run() method, which is what gets called by WiX once our class is loaded.

Important Note:  The BootstrapperApplication base class includes an Engine property, which is a reference to the WiX engine.  Throughout this blog, when I say you call Engine.Somemethod(), you would do this from within this custom class by calling this.Engine.Somemethod();

namespace jwright.Installer
{
    /// <summary>
    /// This is the main entry point into the installer UI, including communication with the
    /// installer engine process via the WiX base class.
    /// </summary>
    public class CustomBootstrapperApplication : BootstrapperApplication
    {

        /// <summary>
        /// Entry point of managed code
        /// </summary>
        protected override void Run()       
        {          
             //... do your thing here 
        }
    }
}

Then, we must add an attribute to the assembly (in our AssemblyInfo.cs) designating that class as the one we want WiX to load:

//WiX -- denotes which class is the Managed Bootstrapper
[assembly: BootstrapperApplication( typeof( CustomBootstrapperApplication))]

Additionally, you'll need to create a file called BootstrapperCore.config in your .Net project which will have app.config style data for your installer.  One key element to include is the node with , which denotes which runtime(s) can be used for your app.  Additionally, you'll want to include the tag under which further defines the  .Net runtime version(s) you want to utilize, including Full vs Client designations.  You can also include any other elements that would normally go into an App.Config file, such as web service endpoint declarations.  In the below example, I'm stipulating that the full .net 4 framework is the only one supported for my managed installer assembly.

<?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="Your.Installer.Assembly.Name.Goes.Here">
      <supportedFramework version="v4\\Full" />
    </host>
  </wix.bootstrapper>

</configuration>

Classes for keeping track of state at runtime

My suggestion is for you to create a set of Bundle, Package and Feature model objects to collect and track the metadata and instance data at runtime, since this will all come from several different sources at different times, during execution.  I'll assume you're going this route in the rest of this series.

Some thoughts on your models:

  • Bundle (there will only be one of these so you could just use your bootstrapper class as the model)
    • Has an Id
    • Contains a collection of Packages
    • Can have addition metadata you may want to display to the user, such as Name, Description, version, etc
  • Package
    • Has an Id
    • Contains a collection of Features, which may be empty depending on how you configure your bundle.
    • Has a current state (PackageState enum)
    • Has a future/requested state  (RequestState enum)
    • Can have addition metadata you may want to display to the user, such as Name, Description, version, etc
  • Feature
    • Has an Id
    • Has a current state (FeatureState enum)
    • Has a future/requested state (FeatureAction enum)
    • Can have addition metadata you may want to display to the user, such as Name, Description, etc


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;