Part 2 of Writing Your Own .Net-based Installer with WiX - Creating the Projects in Visual Studio

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