Custom WiX Managed Bootstrapper Application

by bryanpjohnston

I put together a sample project to show the minimum code needed to create your own managed bootstrapper application using WiX. I wrote it in C# using the MVVM pattern. This is indeed a bare bones example and if you are serious about writing your own managed bootstrapper application, you should download the WiX 3.6 source code and follow their example (see src\Setup\WixBA).

My solution contains three projects (can download the full source here).

  • TestBA:  The bootstrapper UX.
  • BootstrapperSetup:  The main bootstrapper executable that lists the packages to be installed.
  • DummyInstaller:  A dummy .msi that gets installed by the bootstrapper.

TestBA

My managed bootstrapper application is called TestBA and it is written in C# and follows the MVVM pattern. To keep the code I had to write to a minimum, I used the MVVM Light Toolkit (http://mvvmlight.codeplex.com/).

Note the following references used by this project:

  • BootstrapperCore.dll (included with the WiX SDK)
  • Microsoft.Deployment.WindowsInstaller.dll (included with the WiX SDK)
  • WindowsBase.dll (for threading)

Let’s examine each file that makes up my bootstrapper application.

AssemblyInfo.cs

The important part about is to identify that this class is a bootstrapper application like so:

using Microsoft.Tools.WindowsInstallerXml.Bootstrapper;
[assembly: BootstrapperApplication(typeof(Examples.Bootstrapper.TestBA))]

BootstrapperCore.config

The is the config file that accompanies your managed bootstrapper application. The important part is that the assembly name match the name of your bootstrapper application. See below assemblyName=”TestBA”. For more information, read this blog post by Heath Stewart.

<?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="TestBA">
            <supportedFramework version="v4\Full" />
            <supportedFramework version="v4\Client" />
        </host>
    </wix.bootstrapper>
</configuration>

TestBA.cs

This is the class that actually implements the BootstrapperApplication class. I create my ViewModel, View and set the View’s DataContext, then show the UI.

using System.Windows.Threading;
using Microsoft.Tools.WindowsInstallerXml.Bootstrapper;
namespace Examples.Bootstrapper
{
    public class TestBA : BootstrapperApplication
    {
        // global dispatcher
        static public Dispatcher BootstrapperDispatcher { get; private set; }
        // entry point for our custom UI
        protected override void Run()
        {
            this.Engine.Log(LogLevel.Verbose, "Launching custom TestBA UX");
            BootstrapperDispatcher = Dispatcher.CurrentDispatcher;

            MainViewModel viewModel = new MainViewModel(this);
            viewModel.Bootstrapper.Engine.Detect();

            MainView view = new MainView();
            view.DataContext = viewModel;
            view.Closed += (sender, e) => BootstrapperDispatcher.InvokeShutdown();
            view.Show();
            Dispatcher.Run();
            this.Engine.Quit(0);
        }
    }
}

MainView.xaml

My view is nothing special. I have three buttons that are bound to commands for Install, Uninstall and Exit. I also include a little rotating circle that appears when the bootstrapper is installing or uninstalling. If you look at the WiX source code itself, you can see a good example at how to implement a progress bar to track installation progress.

<Window x:Class="Examples.Bootstrapper.MainView"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d"
        Title="My Ugly Bootstrapper Application" Width="400" MinWidth="400" Height="300" MinHeight="300">
    <Window.Resources>
        <BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
    </Window.Resources>
    <Grid>
        <TextBlock Text="Welcome to my test bootstrapper application." Margin="10" FontSize="18" HorizontalAlignment="Center" Foreground="Red" VerticalAlignment="Top" />
        <Ellipse Height="100" Width="100" HorizontalAlignment="Center" VerticalAlignment="Center" StrokeThickness="6" Margin="10"
                 Visibility="{Binding Path=IsThinking, Converter={StaticResource BooleanToVisibilityConverter}}">
            <Ellipse.Stroke>
                <LinearGradientBrush>
                    <GradientStop Color="Red" Offset="0.0"/>
                    <GradientStop Color="White" Offset="0.9"/>
                </LinearGradientBrush>
            </Ellipse.Stroke>
            <Ellipse.RenderTransform>
                <RotateTransform x:Name="Rotator" CenterX="50" CenterY="50" Angle="0"/>
            </Ellipse.RenderTransform>
            <Ellipse.Triggers>
                <EventTrigger RoutedEvent="Ellipse.Loaded">
                    <BeginStoryboard>
                        <Storyboard TargetName="Rotator" TargetProperty="Angle">
                            <DoubleAnimation By="360" Duration="0:0:2" RepeatBehavior="Forever" />
                        </Storyboard>
                    </BeginStoryboard>
                </EventTrigger>
            </Ellipse.Triggers>
        </Ellipse>
        <StackPanel Orientation="Horizontal" VerticalAlignment="Bottom" HorizontalAlignment="Right">
            <Button Content="Install" Command="{Binding Path=InstallCommand}" Visibility="{Binding Path=InstallEnabled, Converter={StaticResource BooleanToVisibilityConverter}}" Margin="10" Height="20" Width="80"/>
            <Button Content="Uninstall" Command="{Binding Path=UninstallCommand}" Visibility="{Binding Path=UninstallEnabled, Converter={StaticResource BooleanToVisibilityConverter}}" Margin="10" Height="20" Width="80"/>
            <Button Content="Exit" Command="{Binding Path=ExitCommand}" Margin="10" Height="20" Width="80" />
        </StackPanel>
    </Grid>
</Window>

MainViewModel.cs

This is my ViewModel  obviously. In the constructor I pass in the bootstrapper engine, which I like to think of as my Model. That may or may not be incorrect, but that’s how it makes sense in my head. You can also see in the constructor that I subscribe to three events.

The DetectPackageComplete event gets fired after Burn checks whether or not a package in the chain is installed on the machine, so this is where we decide whether or not we want to show the Install or Uninstall button.

The PlanComplete event is fired when we are done planning the install (or uninstall) and this is where we instruct Burn to go ahead and proceed with the installation (or uninstallation).

The ApplyComplete event is fired when the installation/uninstallation is complete, so we can hide the install/uninstall buttons and the spinny circle.

using Microsoft.Tools.WindowsInstallerXml.Bootstrapper;
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
namespace Examples.Bootstrapper
{
    public class MainViewModel : ViewModelBase
    {
        //constructor
        public MainViewModel(BootstrapperApplication bootstrapper)
        {
            this.IsThinking = false;
            this.Bootstrapper = bootstrapper;
            this.Bootstrapper.ApplyComplete += this.OnApplyComplete;
            this.Bootstrapper.DetectPackageComplete += this.OnDetectPackageComplete;
            this.Bootstrapper.PlanComplete += this.OnPlanComplete;
        }
        #region Properties
        private bool installEnabled;
        public bool InstallEnabled
        {
            get { return installEnabled; }
            set
            {
                installEnabled = value;
                RaisePropertyChanged("InstallEnabled");
            }
        }
        private bool uninstallEnabled;
        public bool UninstallEnabled
        {
            get { return uninstallEnabled; }
            set
            {
                uninstallEnabled = value;
                RaisePropertyChanged("UninstallEnabled");
            }
        }
        private bool isThinking;
        public bool IsThinking
        {
            get { return isThinking; }
            set
            {
                isThinking = value;
                RaisePropertyChanged("IsThinking");
            }
        }
        public BootstrapperApplication Bootstrapper { get; private set; }
        #endregion //Properties
        #region Methods
        private void InstallExecute()
        {
            IsThinking = true;
            Bootstrapper.Engine.Plan(LaunchAction.Install);
        }
        private void UninstallExecute()
        {
            IsThinking = true;
            Bootstrapper.Engine.Plan(LaunchAction.Uninstall);
        }
        private void ExitExecute()
        {
            TestBA.BootstrapperDispatcher.InvokeShutdown();
        }
        /// <summary>
        /// Method that gets invoked when the Bootstrapper ApplyComplete event is fired.
        /// This is called after a bundle installation has completed. Make sure we updated the view.
        /// </summary>
        private void OnApplyComplete(object sender, ApplyCompleteEventArgs e)
        {
            IsThinking = false;
            InstallEnabled = false;
            UninstallEnabled = false;
        }
        /// <summary>
        /// Method that gets invoked when the Bootstrapper DetectPackageComplete event is fired.
        /// Checks the PackageId and sets the installation scenario. The PackageId is the ID
        /// specified in one of the package elements (msipackage, exepackage, msppackage,
        /// msupackage) in the WiX bundle.
        /// </summary>
        private void OnDetectPackageComplete(object sender, DetectPackageCompleteEventArgs e)
        {
            if (e.PackageId == "DummyInstallationPackageId")
            {
                if (e.State == PackageState.Absent)
                    InstallEnabled = true;
                else if (e.State == PackageState.Present)
                    UninstallEnabled = true;
            }
        }
        /// <summary>
        /// Method that gets invoked when the Bootstrapper PlanComplete event is fired.
        /// If the planning was successful, it instructs the Bootstrapper Engine to
        /// install the packages.
        /// </summary>
        private void OnPlanComplete(object sender, PlanCompleteEventArgs e)
        {
            if (e.Status >= 0)
                Bootstrapper.Engine.Apply(System.IntPtr.Zero);
        }
        #endregion //Methods
        #region RelayCommands
        private RelayCommand installCommand;
        public RelayCommand InstallCommand
        {
            get
            {
                if (installCommand == null)
                    installCommand = new RelayCommand(() => InstallExecute(), () => InstallEnabled == true);
                return installCommand;
            }
        }
        private RelayCommand uninstallCommand;
        public RelayCommand UninstallCommand
        {
            get
            {
                if (uninstallCommand == null)
                    uninstallCommand = new RelayCommand(() => UninstallExecute(), () => UninstallEnabled == true);
                return uninstallCommand;
            }
        }
        private RelayCommand exitCommand;
        public RelayCommand ExitCommand
        {
            get
            {
                if (exitCommand == null)
                    exitCommand = new RelayCommand(() => ExitExecute());
                return exitCommand;
            }
        }
        #endregion //RelayCommands
    }
}

BootstrapperSetup

 This is the bootstrapper project that specifies which packages to install, and tells Burn to use the TestBA UX. Notice we need the reference to the WiX BalExtension. The BootstrapperApplicationRef must be “ManagedBootstrapperApplicationHost”. Also, notice the payloads for my bootstrapper application. We need to make sure the following gets included with the bootstrapper:

  • TestBA.dll
  • BootstrapperCore.config
  • Microsoft.Deployment.WindowsInstaller.dll
  • GalaSoft.MvvmLight.WPF4.dll (this is only because my TestBA.dll relies on the MVVM Light Toolkit)

Below, the DummyInstaller.msi is the package that I want to install.

Please also note the fragment at the bottom. Since our bootstrapper UX was written in .NET, we need to make sure .NET is installed. The WiX variable WixMbaPrereqPackageId specifies which package is required by our MBA. For more information, read this blog post by Heath Stewart.

<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi" xmlns:util="http://schemas.microsoft.com/wix/UtilExtension" xmlns:bal="http://schemas.microsoft.com/wix/BalExtension">
  <Bundle Name="My Test Application" Version="1.0.0.0" Manufacturer="Bryan" UpgradeCode="PUT-GUID-HERE">
    <BootstrapperApplicationRef Id="ManagedBootstrapperApplicationHost">
      <Payload SourceFile="..\TestBA\BootstrapperCore.config"/>
      <Payload SourceFile="..\TestBA\bin\Release\TestBA.dll"/>
      <Payload SourceFile="..\TestBA\bin\Release\GalaSoft.MvvmLight.WPF4.dll"/>
      <Payload SourceFile="C:\Program Files\WiX Toolset v3.6\SDK\Microsoft.Deployment.WindowsInstaller.dll"/>
    </BootstrapperApplicationRef>
    <Chain>
      <PackageGroupRef Id='Netfx4Full' />
      <MsiPackage SourceFile="..\DummyInstaller\bin\Release\DummyInstaller.msi" Id="DummyInstallationPackageId" Cache="yes" Visible="no"/>
    </Chain>
  </Bundle>
  <Fragment>
    <!-- Managed bootstrapper requires .NET as a dependency, since it was written in .NET.
       WiX provides a Bootstrapper for the bootstrapper. The fragment below includes .NET.
       For more information or examples see Heath Stewart's blog or the WiX source:
       http://blogs.msdn.com/b/heaths/archive/2011/10/28/introducing-managed-bootstrapper-applications.aspx
       -->
    <WixVariable Id="WixMbaPrereqPackageId" Value="Netfx4Full" />
    <WixVariable Id="WixMbaPrereqLicenseUrl" Value="NetfxLicense.rtf" />
    <util:RegistrySearch Root="HKLM" Key="SOFTWARE\Microsoft\Net Framework Setup\NDP\v4\Full" Value="Version" Variable="Netfx4FullVersion" />
    <util:RegistrySearch Root="HKLM" Key="SOFTWARE\Microsoft\Net Framework Setup\NDP\v4\Full" Value="Version" Variable="Netfx4x64FullVersion" Win64="yes" />
    <PackageGroup Id="Netfx4Full">
      <ExePackage Id="Netfx4Full" Cache="no" Compressed="yes" PerMachine="yes" Permanent="yes" Vital="yes"
                  SourceFile="C:\Program Files\Microsoft SDKs\Windows\v7.0A\Bootstrapper\Packages\DotNetFX40\dotNetFx40_Full_x86_x64.exe"
                  DownloadUrl="http://go.microsoft.com/fwlink/?LinkId=164193"
                  DetectCondition="Netfx4FullVersion AND (NOT VersionNT64 OR Netfx4x64FullVersion)" />
    </PackageGroup>
  </Fragment>
</Wix>

Hope this makes sense.