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.
I am using Wix 3.8.
I want to have a single installer UI which will present four applications.Based on user selection the particular application has to be installed/ uinstalled /upgraded.
So I added four msis in a custom bootsrapper bundle. In the Custom UI(using WPF) we show all four application names
and based on the user selection I set bundle variable value. Install condition attribute is set to this bundle variable.
Also based on the user selection package list will be updated to have only the relevant package(s) .In this way install is working fine.
Install adds only the bundle entry in the Add /Remove Programs; and not individual entry for each msi.
I am also able to manage uninstalling a package based on the user selection.The issue is while uninstalling one msi package it removes the entry of the bundle from Add/Remove Programs list.
Now how do I unistall other packages?
The behaviour what I need is I want to retain the bundle entry in the Add/remove Programs list ; Once the last package is also uninstalled I want to remove the
entry from Add /Remove Programs List. Please advice me how to achieve this?
I am using Wix 3.8
I want to have a single installer UI which will present four applications.Based on user selection
the particular application has to be installed/ uinstalled /upgraded.
So I added four msis in a custom bootsrapper bundle. In the Custom UI we show all four application names
and based on the user selection I set bundle variable value. Install condition attribute is set to this bundle variable.
Also based on the user selection package list will be updated to have only the relevant package(s) .
In this way install is working fine. Install add only the bundle entry in the Add /Remove Programs; and not individual entry for each msi.
I am also able to manage uninstalling a package based on the user selection.
The issue is while unistalling one msi package it removes the entry of the bundle from Add/Remove Programs list.
Now how do I unistall other packages?
The behaviour what I need is I want to retain the bundle entry in the Add/remove Programs list ; Once the last package is also uninstalled I want to remove the
entry from Add /Remove Programs List. Please advice me how to achieve this?
Hi Veena, Could you please share your code. Let me go through it whether i can help.
hey, thanks a lot for the tutorial. I’ve a problem while performing a major upgrade. While upgrading, if I increment the bundle version and the included MSI versions, it won’t uninstall the existing EXE in control panel. I had a look at the logs and it has stopped with detect phase for uninstalling. But if I increment only the EXE version, leaving the MSI versions as they are, then the uninstall during the upgrade runs well. I have gone through some comments above and noticed that several ppl have experienced this. You can test this yourself, by running the TestBA project. Increase both the setup version and EXE version to 2.0.0.0 and it wont uninstall the existing EXE. Can you please help me with this? Stuck with this for over 3 days now.
hey i followed your tutorial. But performing a major upgrade is an issue for which I can’t find any solutions. When I increment the bundle EXE version as well as the MSI version, it leaves the old EXE as it is though it updates the new MSIs. I went through the logs and saw that while unisntalling it stops at the detect phase and nothing happens. If increment only the EXE version and leave the MSI version as it is, then the upgrade works fine. How can I perform the upgrade incrementing both the EXE and the msi. You can test this yourself. Install the exe from the TestBA project. Then if you increase exe vesrion to 2.0.0.0 and msi version to 2.0.0.0, it wont remove the old exe. Please advice.
Hi Bryan,
I am trying to find out a way to include installation on remote machines using the msi developed from WiX. Is there any default option as such, or can I use custom action in WiX to run a VB Script, enter the IP address, and the user credentials of the machine where I need to install and fire up my msi from the central system?
Спасибо!!!
[…] Burn custom UI tutorial: https://bryanpjohnston.com/2012/09/28/custom-wix-managed-bootstrapper-application/ […]
[…] allows you to write your own setup GUI application (advanced): https://github.com/rstropek/Samples/tree/master/WiXSamples/CustomBurnUI (more samples […]