Part 5 of Writing Your Own .Net-based Installer with WiX - Executing the Action

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:

Running the action

Once the Engine's planning phase is complete (see previous post), we can call Engine.Apply(IntPtr.Zero) to execute the action. Note: the parameter to Apply() is a window handle. You could also pass handle to your UI window, but getting that value is beyond what I want to get into here. I believe this is used by Windows when prompting for UAC, but this all works fine with IntPtr.Zero. The Apply action will provide status via events as well:

BootstrapperApplication.ExecuteMsiMessage += EventProviderOnExecuteMsiMessage; 
BootstrapperApplication.Progress += EventProviderOnProgress; 
BootstrapperApplication.ExecuteComplete += EventProviderOnExecuteComplete; 
BootstrapperApplication.ExecuteFilesInUse += EventProviderOnExecuteFilesInUse; 
BootstrapperApplication.ExecutePackageBegin += EventProviderOnExecutePackageBegin; 
BootstrapperApplication.ExecutePackageComplete += EventProviderOnExecutePackageComplete; 
BootstrapperApplication.ApplyComplete += BootstrapperOnApplyComplete;

this.Engine.Apply(IntPtr.Zero); 

The ExecuteMsiMessage callback provides some non-user friendly message from the MSI engine, which I just log. I use the ExecuteComplete to set my percentage variables to 100% (since the last progress event might only by 95% or so) and the ExecutePackageBegin and ExecutePackageComplete to set a UI label saying "Currently installing XXX package".

The ExecutePackageBegin includes the packageId for the package currently being installed. You'll need to use that to lookup the Name, etc, from the Package models you generated previously (see Part 2 of this series).

private void EventProviderOnExecutePackageBegin(object sender, ExecutePackageBeginEventArgs executePackageBeginEventArgs) 
{
    var inFlightPkgId = executePackageBeginEventArgs.PackageId;
    var inFlightPkg = BundlePackages.FirstOrDefault(pkg => pkg.Id == inFlightPkgId);

    if (inFlightPkg == null)
    {
        CurrentlyProcessingPackageName = string.Empty;
    }
    else
    {
        CurrentlyProcessingPackageName = inFlightPkg.Name;
    }
} 

While the action is being performed, progress will be provided via events, which will tell you which package is currently being installed, percentage completions, etc. One note: if a rollback is initiated due to a failure or error in the install, the progress percentages will decrease as the rollback occurs. Also note, this is the best place to handle user cancellations (see below section for that topic).

private void EventProviderOnProgress(object sender, ProgressEventArgs progressEventArgs) 
{ 
    //update local properties (which are likely bound to a ProgressBar or something 
    CurrentComponentProgressPercentage = progressEventArgs.ProgressPercentage; 
    OverallProgressPercentage = progressEventArgs.OverallPercentage;

    //... handle user cancellations here

} 

For the ExecuteFileInUse, I haven't actually been successful in forcing this error, but here's an example of the code I have to handle it:

private void EventProviderOnExecuteFilesInUse(object sender, ExecuteFilesInUseEventArgs executeFilesInUseEventArgs) 
{
    var message = new StringBuilder("The following files are in use. Please close the applications that are using them.n"); 
    foreach (var file in executeFilesInUseEventArgs.Files) 
    { 
        message.AppendLine(" - " + file); 
    }

    var userButton = MessageBox.Show(message.ToString(), "Files In Use", MessageBoxButto.OKCancel, MessageBoxImage.Warning);

    if (userButton != MessageBoxResult.OK)
        executeFilesInUseEventArgs.Result = Result.Cancel;
} 

When the apply action is complete, it will fire the ApplyComplete handler. This is where you can set status, etc. Here is what my method looks like:

private void BootstrapperOnApplyComplete(object sender, ApplyCompleteEventArgs applyCompleteEventArgs) 
{ 
    BootstrapperApplication.ApplyComplete -= BootstrapperOnApplyComplete;

    //using "ActionResult" property to store the result for use
    // when I call Engine.Quit()

    if (applyCompleteEventArgs.Status >= 0)
    {
        ActionResult = ActionResult.Success;
    }
    else
    {
        ActionResult = ActionResult.Failure;
    }
} 

Finishing it out

When you're ready to close down the app, even if you haven't actually done anything (user cancellation, etc), you'll need to call Engine.Quit() with one of these ActionResult values: (hint: you'll need to cast the Enums to an int)

ActionResult.Success
The action was run and was successful.
ActionResult.UserExit
Action was cancelled by user, therefore was unsuccessful.
ActionResult.Failure
Action had errors and was unsuccessful.
ActionResult.NotExecuted
No actions were performed.

Be careful that you return the correct values, as that affects the return code of the executable (which the caller may be monitoring in a scripted install) as well how the Add/Remove Programs list shows (or doesn't show) your app and if locally cached instances of your installer are deleted, etc. An incorrect return value may result in an Add/Remove programs listing that never gets removed on uninstall, or never shows up in the first place, and other oddities.

Note too that you'll need to handle closing your UI windows, etc, on your own -- calling Engine.Quit() does not close your UI or terminate your threads, etc.

Engine.Quit((int) ActionResult.Success);

Canceling while action is being performed

Most of the WiX events have an EventArgs parameter that includes a Result property. This can be used to cancel asynchronous engine operations. For instance, the progress event fires frequently, so is a good candidate. If the user clicks your cancel button during the install, you can set Result=Result.Cancel on the next event to signal the engine to stop and initiate the rollback process. This assumes you provide the user a cancel button. You'll also want to monitor if the user closes your UI window using the "X" button in the top right corner.

private void EventProviderOnProgress(object sender, ProgressEventArgs progressEventArgs) 
{ 
    //.... 
    if (_userHasCancelled) 
        progressEventArgs.Result = Result.Cancel; 
}