Getting the progress percentage from a burn bootstrapper installer

One of the basic requirements from an installer created using burn bootstrapper would be to display the progress percentage. I had a tough time finding a proper solution to this as most of the solutions on the internet didn’t work properly while displaying the uninstall percentage. So I have put together variety of things I had searched into one single solution.  So let’s get started!

In order to display the progress bar, we need to handle two events. First of which is the CacheAcquireProgress. This will give you the percentage related to caching the package. Next is the ExecuteProgress percentage, which will give you the percentage for the executed packages. Now most of the sites had specified to add both of the values and divide it by two. This cannot be done as some actions will not be having a cache phase. So in order to find the denominator,  we need to use the OnApplyBegin in v4 of WiX and OnApplyPhaseCount in versions below 4. Since there hasn’t been a stable v4 release yet, I will give you sample of how its done in versions under v4 with the OnApplyPhaseCount method.

Create a view for the percentage bar as shown below.


<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>

Now let’s bind this to a property called progress.


private int progress;
public int Progress

{

get
{
return this.progress;
}
set
{
this.progress = value;
this.RaisePropertyChanged(() => this.Progress);
}
}

Now let’s add the event handlers for the CacheAcquireProgress and ExecuteProgress events.


private int cacheProgress;
private int executeProgress;

private int phaseCount;

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

We then get the phase count by hooking onto the ApplyPhaseCount method as shown below.

WixBA.Model.Bootstrapper.ApplyPhaseCount += this.ApplyPhaseCount;

private void ApplyPhaseCount(object sender, ApplyPhaseCountArgs e)
{
    this.phaseCount= e.PhaseCount;
} 

This would give you the perfect progress percentage for your custom installer!

Passing install path as an argument to burn bootstrapper

I had to add this extra little thing to my Burn bootstrapper EXE where I had to enable the user to pass the installation location as a command line argument. So here is how to do it.

First of all in your chain element of the bootstrapper project’s bundle.wxs, add the MsiProperty element which would allow us to pass value to a variable. Below is an example of such element.


<Chain>
<MsiPackage SourceFile="Awesome1.msi">
<MsiProperty Name="InstallLocation" Value="[InstallerPath]" />
</MsiPackage>
</Chain>

Inside the MSI package’s setup project, add the directory ID to be “InstallLocation” and have it defined as a property as shown below.

<Property id="InstallLocation"/>

<Directory Id="TARGETDIR" name="SourceDir">

<Directory Id="InstallLocation" name=""My Program">

Now back in the bundle.wxs file, add the BalExtension name space as shown below.


xmlns:bal="http://schemas.microsoft.com/wix/BalExtension";

Now declare the variable which is going to hold the install path that would be passed as a command line argument.

<Variable Name="InstallerPath" bal:Overridable="yes"/>

overridable should be set to yes for all the variables that would get their values from command line arguments. Now just run the EXE passing the value.


BootstrapperSetup.exe /i InstallerPath=G:\

That’s all folks! now you have a bootstrap installer that takes the install path as a command line argument!

 

Burn Bootstrapper installer major upgrade doesn’t uninstall previous version

This post provides the solution for one of the worst nightmares I’ve ever had! I created this burn bootstrapper installer setup which installs and uninstall properly. But behaves abnormally during a major upgrade. That is, when you perform a major upgrade, the previous installed version wouldn’t uninstall and the new version will be installed side by side. If this was your issue, then you’re at the right place!

First of all make sure you’ve done the major upgrade the way it is expected to be done. If you have miss any of the following steps, then take a deep breath and just do it!

  • Change the product element’s ID attribute to a new GUID
  • Increment the product element’s version attribute
  •  Add and configure a major upgrade element. Which would look like,

 


<MajorUpgrade DowngradeErrorMessage="A newer version of [ProductName] is already installed"

But in my case, I had done all this and I still was facing the issue. Wasted a lot of time on this as I couldn’t find an answer for this in any blogs or stackoverflow questions. I turned to my installer logs and this is what I found there.

[0980:3888][2016-04-22T16:49:19]i100: Detect begin, 2 packages
[0980:3888][2016-04-22T16:49:19]i102: Detected related bundle: {f57e276b-2b99-4f55-9566-88f47c0a065c}, type: Upgrade, scope: PerMachine, version: 1.0.1.0, operation: None
[0980:3888][2016-04-22T16:49:19]i103: Detected related package: {8C442A83-F559-488C-8CC4-21B1626F4B8E}, scope: PerMachine, version: 1.0.1.0, language: 0 operation: Downgrade
[0980:3888][2016-04-22T16:49:19]i103: Detected related package: {8201DD23-40A5-418B-B016-4D29BE6F010B}, scope: PerMachine, version: 1.0.1.0, language: 0 operation: Downgrade
[0980:3888][2016-04-22T16:49:19]i101: Detected package: KubeUpdaterServiceInstallerId, state: Obsolete, cached: Complete
[0980:3888][2016-04-22T16:49:19]i101: Detected package: MosquittoInstallerId, state: Obsolete, cached: Complete
[0980:3888][2016-04-22T16:49:19]i199: Detect complete, result: 0x0
[0980:3888][2016-04-22T16:51:43]i500: Shutting down, exit code: 0x0

As you can see, it just stopped at the detect complete state. It was supposed to begin the planning phase but it didn’t! I wasted a lot of time in find a solution and in the end arrived at one!

There is this method called “DetectComplete” which is called at the end of the detect phase. So I hooked onto that method and called the plan phase manually. Now the upgrade function works like a charm! it smoothly installs the new version while removing any previous contents! So below is the implementation of it.


void DetectComplete(object sender, DetectCompleteEventArgs e)
{
Bootstrapper.Engine.Log(LogLevel.Verbose,&quot;fired! but does that give you any clue?! idiot!&quot;);
if (LaunchAction.Uninstall == Bootstrapper.Command.Action)
{
Bootstrapper.Engine.Log(LogLevel.Verbose, &quot;Invoking automatic plan for uninstall&quot;);
Bootstrapper.Engine.Plan(LaunchAction.Uninstall);
}
}

Hope this helps someone else looking for a solution for this same issue!

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!

Getting started with WIX Toolset

So I was assigned a task to create an installer for a specific project I’ve been working on and WiX Toolset was the undisputed suggestion thrown onto the table. WiX allows you to package your application files, install services, create new registry entries and eventually everything anything you could imagine to do with an installer! (Even creating you own WPF User interface for the installation process, remember the Visual Studio 2012 or above setup installers?) If you’re new to WiX and not sure where to get started with it, then here is how you could master it!

Find the latest release of WiX from WiX page for releases and install it. This will also add WiX project templates to the Visual Studio version you have installed.This plugin support is available from above Visual Studio 2005 on wards. You can find a good tutorial at the WiX Site Tutorial Page but you might feel lost in the process of trying to understand the variety of things by following the chain of hyper links!

There is no better way for starting off your journey in WiX than reading the book “WiX 3.6: A Developer’s Guide to Windows Installer XML” by Nick Ramirez. It provides you with the ins and outs of WiX as it claims to do! Now do not get intimidated by the page numbers because you need not to read the whole thing to get started! Just read through Chapter 1 and Chapter 2 and you are ready to go! You could then read the rest of the chapters to get your self introduced to the world of possibilities in Wix! (Even hovering through the content page should be adequate) If you’re in real hurry, just read the rest as you come to face with the new requirements of your installer.

Hope that this has guided you on where to start learning wix and later I’ll be adding stuff on tricky parts for which you might not the solutions that easily from neither the book nor the internet. Happy WiXing!