Creating a custom UI installer with WIX Burn Bootstrapper

If your requirement is to create an installer providing it a look and feel of your own or if you want to get access to the installation progress details, then WiX Burn is what you need! In order to get an idea of Burn you can go through Let’s talk about Burn

Let us now look at how we can create an installer using the burn bootstrapper. This installer is going to have our own WPF UI, display a progress percentage and install a chain of MSIs.

First of all we will be creating a class library named CustomBA. The bootstrapper that we are going to create will be configured to use this assembly. This assembly will drive the burn engine while using the WPF code to show the custom UI.

Go to File -> New Project and select “Class Library” project under Visual C#. Now add a reference to BootstrapperCore.dll which would allow us to plug a new user interface into the burn engine.You would find this at the WiX SDK directory. Most probably this would be C:\Program Files (x86)\WiX Toolset v3.10\SDK.   Then add references to PresentationCore, PresentationFramework, System.Xaml and WindowsBase. Also download and add a reference to Galasoft.MvvmLight.WPF4.dll in order to  implement the Model-View-ViewModel pattern. You could also use Prism if you would want to.

Next we need to add the XML configuration file which would tell Burn to use our 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="CustomBA">
<supportedFramework version="v4\Full" />
<supportedFramework version="v4\Client" />
</host>
</wix.bootstrapper>
</configuration>

You could use the same content as above, apart from the assemblyName of host element, which would be the name of your assembly project.

Under properties of your project, locate the AssemblyInfo.cs class and add the below given attribute.


[assembly: BootstrapperApplication(typeof(CustomBA.CustomBA))]

CustomBa.CustomBA specifies the namespace and the class name of the class which is going to extend the ‘BoostrapperApplication’ class. Let’s add this class to the library project and call it “CustomBA”. Below is how it would look like.


public class CustomBA : 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, &amp;quot;Launching custom TestBA UX&amp;quot;);
BootstrapperDispatcher = Dispatcher.CurrentDispatcher;

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

MainView view = new MainView();
view.DataContext = viewModel;
view.Closed += (sender, e) =&amp;gt; BootstrapperDispatcher.InvokeShutdown();
view.Show();
Dispatcher.Run();

this.Engine.Quit(0);
}
}

Here the MainViewModel class is going to wrap the class to the Burn Engine while the MainView class is going to define the WPF UI. The Engine.Detect method would check if our bundle has already been installed and decide whether to present the user with an install button or uninstall button. Since the rest of the code is pretty much self explanatory, let us now move into the implementation of the MainViewModel class.


public class MainViewModel : ViewModelBase
{
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;

this.Bootstrapper.CacheAcquireProgress += (sender, args) =>
{
this.cacheProgress = args.OverallPercentage;
this.Progress = (this.cacheProgress + this.executeProgress) / 2;
};
this.Bootstrapper.ExecuteProgress += (sender, args) =>
{
this.executeProgress = args.OverallPercentage;
this.Progress = (this.cacheProgress + this.executeProgress) / 2;
};
}

#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");
}
}

private int progress;
public int Progress
{
get { return progress; }
set
{
this.progress = value;
RaisePropertyChanged("Progress");
}
}

private int cacheProgress;
private int executeProgress;

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()
{
CustomBA.BootstrapperDispatcher.InvokeShutdown();
}
private void OnApplyComplete(object sender, ApplyCompleteEventArgs e)
{
IsThinking = false;
InstallEnabled = false;
UninstallEnabled = false;
this.Progress = 100;
}
private void OnDetectPackageComplete(object sender, DetectPackageCompleteEventArgs e)
{
if (e.PackageId == "KubeInstallationPackageId")
{
if (e.State == PackageState.Absent)
InstallEnabled = true;

else if (e.State == PackageState.Present)
UninstallEnabled = true;
}
}

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
}

As you can see, this class will be responsible for being a bridge between our custom code and the burn engine. The OnApplyComplete method would be invoked when the Bootstrapper ApplyComplete event is fired. Therefore it consists the logic for updating the view. The OnDetectPackageComplete method would be invoked when DetectPackageComplete event is fired. Therefore we check for the pacakge ID and set the installation scenario. Package ID here is the ID that you have specified inside the <Package> element of the MSI. The OnPlanComplete method would be fired when the Bootstrapper PlanComplete event is fired. Based on the result of the planning state, Bootstrapper engine will install the package. Go through this stack overflow question to understand the sequence of bootstrapper events.

Now that we have set everything up, let’s work on the view part of things.

Right click on your CustomBA project and select “User Control” file under Visual C# category. Insert the below code to create a simple UI with install/uninstall button, exit button, progress spinner and a progress percentage bar.


<Window x:Class="CustomBA.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>

<WrapPanel Margin="10" >

<Label VerticalAlignment="Center">Progress:</Label>
<Label Content="{Binding Progress}" />
<ProgressBar Width="200" Height="30" Value="{Binding Progress}" Minimum="0" Maximum="100" />
</WrapPanel>
<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>

Make sure that you specify exact name of your XAML’s code-behind in “Class” attribute of the Window element(That is the MainView.xaml.cs you get when you expand the MainView.xaml file in project explorer).  In my case it is, “CustomBA.MainView”. If this class name is wrong, you will see an error in the code-behind file’s initializeComponent method. Below is how your code-behind file (MainView.xaml.cs) should look like.


public partial class MainView : Window
{

public MainView()
{
InitializeComponent();
}
}

Now compile the CustomBA project and you will get the CustomBA.dll file. Let us now create the EXE. Create a new project using the “Bootstrapper project” template under “Windows Installer XML” project category. Your bundle.wxs would be created with some sample values. We need to add the MSI package element inside the chain element. The ID you specify here for the MSI is what we have specified as packageID under MainViewModel.cs. Remember? Okay moving on, let’s include the burn related files that we build earlier into the exe as well. So after these changes, below is how your bundle.wxs would look like.


<?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="Kube Installer" Version="1.0.0.0" Manufacturer="Zone24x7" UpgradeCode="C82A383C-751A-43B8-90BF-A250F7BC2863">
<BootstrapperApplicationRef Id="ManagedBootstrapperApplicationHost" >
<Payload SourceFile="..\CustomBA\BootstrapperCore.config"/>
<Payload SourceFile="..\CustomBA\bin\Release\CustomBA.dll"/>
<Payload SourceFile="..\CustomBA\bin\Release\GalaSoft.MvvmLight.WPF4.dll"/>
<Payload SourceFile="C:\Program Files (x86)\WiX Toolset v3.8\SDK\Microsoft.Deployment.WindowsInstaller.dll"/>
</BootstrapperApplicationRef>
<WixVariable Id="WixMbaPrereqLicenseUrl" Value=""/>
<WixVariable Id="WixMbaPrereqPackageId" Value=""/>
<Chain>
<MsiPackage SourceFile="..\KubeInstaller\bin\Release\KubeInstaller.msi" Id="KubeInstallationPackageId" Cache="yes" Visible="no"/>
</Chain>

</Bundle>
</Wix>

Build this project and you will get your EXE that looks the way you defined it to look like! Play around with the WPF controls  and you could come up with some really fancy setups. Remember the Visual Studio 2012 and above setup UIs? Well they use the same methodology too!

Sometimes you might encounter this issue where your EXE installs and uninstalls without any issues, but won’t upgrade properly. When you perform a major upgrade, it would still leave the old EXE as it is and install the new one side by side. In that case follow this post of mine.

If you want to pass the install location as a parameter during installation, then read through this post .

There are other tricky parts that you might encounter during the process. I will cover them in other blog posts under the wix category! Happy WiXing!

Advertisements