Part 4 of Writing Your Own .Net-based Installer with WiX - Handling Current and Future State

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:

Detecting current state

In order to determine which, if any, of the bundled packages and features, as well as the bundle itself, are already installed on the system, and at what versions, we'll ask the WiX engine to detect current state by calling Engine.Detect() on the BootstapperApplication base class. Note this is an asynchronous process, so the call will return immediately and we'll get our results via a series of events. As such, before calling Engine.Detect(), we need to register our event handlers for the detect events (these are also on the base class). Note: You may not need both DetectPackageComplete and DetectRelatedMsiPackage, depending on your needs.

//
// Call Engine.Detect, asking the engine to figure out what's on the machine.
// The engine will run async and use callbacks for reporting results.
//

// This is called when the bundle is detected
BootstrapperApplication.DetectRelatedBundle += HandleExistingBundleDetected;

// This is called when a package in the bundle is detected
BootstrapperApplication.DetectPackageComplete += SetPackageDetectedState;

// This is called when a package in the bundle is detected
BootstrapperApplication.DetectRelatedMsiPackage += HandleExistingPackageDetected;

// This is called when a Feature in the bundle's packages is detected
BootstrapperApplication.DetectMsiFeature += SetFeatureDetectedState;
BootstrapperApplication.DetectComplete += DetectComplete;
BootstrapperApplication.Detect();

As the engine determines a package or feature from our bundle is already on the system, it will fire the associated event. The specialized EventArgs parameter provided to each event handler will have the packageId (and featureId for features) and the current state. As each of these fire, you'll want to search the package and feature models you've built to find the package/feature identified by the event args, and set the current state Enum on each based on the incoming value.

Note: in these examples, the "BundlePackages" variable is a reference to the collection of Package model objects I suggested you create in Part 3 (Context Data)

private void HandleExistingPackageDetected(object sender, DetectRelatedMsiPackageEventArgs e)
{
    string existingPackageProductCode = e.ProductCode;

    RelatedOperation actionToBeApplicedToExistingPackage = e.Operation;
    string existingPackageId = e.PackageId;
    Version existingPackageVersion = e.Version;

    //update your model objects here (search models by PackageId)
}

private void HandleExistingBundleDetected(object sender, DetectRelatedBundleEventArgs e)
{
    Version existingBundleVersion = e.Version;
    string existingBundleProductCode  = e.ProductCode;
    RelatedOperation actionToBeAppliedToExistingBundle = e.Operation;

    //update your model object here
}

/// <summary>
/// when engine detects a package, populate the appropriate local objects,
/// including current installed state of the package on the system
/// </summary>
private void SetPackageDetectedState(object sender, DetectPackageCompleteEventArgs args)
{
    var package = BundlePackages.FirstOrDefault(pkg => pkg.Id == args.PackageId);
    PackageState currentState = args.State;
    package.CurrentInstallState = currentState;
}

/// <summary>
/// when engine detects a feature, populate the appropriate local objects,
/// including current installed state of the package on the system
/// </summary>
private void SetFeatureDetectedState(object sender, DetectMsiFeatureEventArgs args)
{
    var package = BundlePackages.FirstOrDefault(pkg => pkg.Id == args.PackageId);
    var feature = package.AllFeatures.FirstOrDefault(feat => feat.Id == args.FeatureId);
    FeatureState currentState = args.State;

    feature.CurrentInstallState = args.State;
}

Below are the values for RelatedOperation:

public enum RelatedOperation
{
    None,

    /// <summary>
    /// The related bundle or package will be downgraded.
    /// </summary>
    Downgrade,

    ///<summary>
    /// The related package will be upgraded as a minor revision.
    ///</summary>
    MinorUpdate,

    ///<summary>
    /// The related bundle or package will be upgraded as a major revision.
    ///</summary>
    MajorUpgrade,

    ///<summary>
    /// The related bundle will be removed.
    ///</summary>
    Remove,

    ///<summary>
    /// The related bundle will be installed.
    ///</summary>
    Install,

    ///<summary>
    /// The related bundle will be repaired.
    ///</summary>
    Repair,
};

Below are the values for FeatureState. For the most part, you'll likely only care about Unknown (state not yet discovered), Absent (not installed) and Local (installed).

public enum FeatureState
{
    Unknown, 
    Absent,  
    Advertised, 
    Local, 
    Source,
}

Below are the values for PackageState. For the most part, you'll likely only care about Unknown (state not yet discovered), Absent (not installed) and Present (installed)

public enum PackageState
{
    Unknown,
    Obsolete,
    Absent,
    Cached,
    Present,
    Superseded,
}

When the Detect action is complete, it will fire the DetectComplete handler, where you'll want to perform whatever UI actions you require before moving forward with the installation, such as prompting the user if they want to run a "typical" or "custom" install, or if packages are already installed, asking if they want to fully uninstall or just add/remove features. Here's what my method looks like:

    /// 
    /// Once the engine completes the Detect phase, unregister event handlers,
    /// release the main thread and register the planning phase event handlers
    /// 
    void DetectComplete( object sender, DetectCompleteEventArgs e)
    {
        BootstrapperApplication.DetectPackageComplete -= SetPackageDetectedState;
        BootstrapperApplication.DetectMsiFeature -= SetFeatureDetectedState;
        BootstrapperApplication.DetectComplete -= DetectComplete;

       //logic to continue here — likely to allow user to select package state, etc, in the UI
    }

Planning future state

Before we tell the windows installer to go off and run the install actions, we need to tell it what we want it to do. This is achieved by calling Engine.Plan() with an action enum (install, uninstall, etc) on the BootstrapperApplication base class. Similar to the Detect() sequence, this will initiate an asynchronous process, so before calling, we need to register event handlers:

        BootstrapperApplication.PlanPackageBegin += SetPackagePlannedState;
        BootstrapperApplication.PlanMsiFeature += SetFeaturePlannedState;
        BootstrapperApplication.PlanComplete += BootstrapperOnPlanComplete;
        this.Engine.Plan(LaunchAction.Install);

The engine will then fire an event for each package and feature in our bundle, each with a specialized event args parameter. Our job is to set the requested state on the event args for each.

    /// 
    /// when engine plans action for a package, set the requested future state of
    /// the package based on what the user requested
    /// 
    private void SetPackagePlannedState( object sender, PlanPackageBeginEventArgs planPackageBeginEventArgs)
    {
        var pkgId = planPackageBeginEventArgs.PackageId;
        var pkg = BundlePackages.FirstOrDefault(p => p.Id == pkgId);

        //I’m assuming a property “RequestedInstallState” on your model
        //of type RequestState.
        planPackageBeginEventArgs.State = pkg.RequestedInstallState;
    }

    /// 
    /// when engine plans action for a feature, set the requested future state of
    /// the package based on what the user requested
    /// 
    private void SetFeaturePlannedState( object sender, PlanMsiFeatureEventArgs planMsiFeatureEventArgs)
    {           
        var pkg = BundlePackages.First(p => p.Id == planMsiFeatureEventArgs.PackageId);
        var feature = pkg.AllFeatures.First(feat => feat.Id == planMsiFeatureEventArgs.FeatureId);

        //I’m assuming a property “RequestedState” on your model
        //of type FeatureState.
        planMsiFeatureEventArgs.State = feature.RequestedState;
    }

Below are the values for FeatureAction. For the most part, you'll likely only care about None (don't change from the current state), AddLocal (install it), Reinstall, Remove (uninstall).

public enum FeatureAction
{
    None,
    AddLocal,
    AddSource,
    AddDefault,
    Reinstall,
    Advertise,
    Remove,
}

Below are the values for RequestState. For the most part, you'll likely only care about None (don't change from current state), ForceAbsent (force uninstall), Absent (uninstall), Present (install) and Repair.

public enum RequestState
{
    None,
    ForceAbsent,
    Absent,
    Cache,
    Present,
    Repair,
}

When the plan action is complete, it will fire the PlanComplete event handler. This is where you'll want to start up the next set of actions. In my case, I'm going straight from Plan to Apply (see below), so my method looks like this:

private void BootstrapperOnPlanComplete(object sender, PlanCompleteEventArgs args)
    {           
        BootstrapperApplication.PlanComplete -= BootstrapperOnPlanComplete;

        //Code to initiate Apply action goes here.. See Part 5 post for more details.
    }