In the previous lesson we learned how to create a fully customizable, rich installer package with the usual look and feel, bitmaps, icons, license agreement—everything we have seen in other people's installer packages. The vast majority of installation tasks can be solved with our accumulated knowledge. Still, there are times when we need a little bit more, something not found in the standard package.
3.1 Queueing Up
There are many steps, so-called actions the Windows Installer carries out during an installation. The basic ones and those additional items required by the specific installer (this depends on the features you use, registry searches, user interface, etc) are automatically scheduled, in other words, their sequence is predetermined by the toolset when it creates the installer database. For a common .msi file, this might look like:
- AppSearch
- LaunchConditions
- ValidateProductID
- CostInitialize
- FileCost
- CostFinalize
- InstallValidate
- InstallInitialize
- ProcessComponents
- UnpublishFeatures
- RemoveShortcuts
- RemoveFiles
- InstallFiles
- CreateShortcuts
- RegisterUser
- RegisterProduct
- PublishFeatures
- PublishProduct
- InstallFinalize
- RemoveExistingProducts
You can check out the actual action sequence in an installer using Orca, an MSI editor from the Windows Installer SDK:

Reordering these events can be done using the appropriate tags. Actually, we have as many as four of them:
- AdminUISequence
- InstallUISequence
- AdminExecuteSequence
- InstallExecuteSequence
The ones starting with Admin- refer to administrative installs (launched using msiexec /a). These type of installations create a source image of the application onto the network so that users in the workgroup can later install from this source image instead of the original media. This functionality comes for free, we've never bothered with it so far, yet our previous samples can all be installed this way (try it!).
So, for the moment, this only leaves two tags. InstallExecuteSequence is always consulted by the installer to determine the actions, InstallUISequence is only considered when the installer runs in full or reduced UI mode (yet another functionality to experiment with, try msiexec /qn, /qb and /qr). Because we need to schedule our registry search before the launch condition in all UI cases, insert that line into both tags. Compile and run, it should work now, keep renaming the registry key to check.
You can see the sequence order numbers in the Orca screenshot above. Although you can use these numbers as well, it is much easier not to bother with them, just tell WiX the relative sequence of your actions: simply specify which sequence your action should come Before or After. To remove an action from the chain of execution, use the Suppress = yes attribute.
<InstallExecuteSequence> <LaunchConditions After='AppSearch' /> <RemoveExistingProducts After='InstallFinalize' /> </InstallExecuteSequence>
3.2 Extra Actions
There are many other standard actions available but not scheduled by default. ScheduleReboot, for instance, will instruct the user to reboot after the installation:
<InstallExecuteSequence> <ScheduleReboot After='InstallFinalize' /> </InstallExecuteSequence>
If the need to reboot depends on a condition (for instance, the operating system the installer is running on), use a condition:
<InstallExecuteSequence> <ScheduleReboot After='InstallFinalize'>Version9X</ScheduleReboot> </InstallExecuteSequence>
It's not only these so-called standard actions that you can schedule and re-schedule. There are a couple of custom actions as well (custom here means that they don't appear in the standard course of events but you can use them wherever and whenever you like). A very common need is to launch the application you've just installed.
Custom actions need to be mentioned in two places in the source file. First as a child of the Product tag (for instance, between the closing Feature and the UI tag). This CustomAction tag will specify what to do. To launch an executable we've just installed, refer to it using the Id identifier of the File tag specifying the file. You also have to provide a command line, although it can be left empty if not needed:
<CustomAction Id='LaunchFile' FileKey='FoobarEXE' ExeCommand='' Return='asyncNoWait' />
Second, we have to schedule the action the usual way. The link between the action and the scheduling entry is provided by the matching Id—Action attribute pair. If the execution of the custom action is conditional, we can specify the condition inside the Custom tag. We need the condition here so that we only launch the executable when we make an installation but not when we remove the product:
<InstallExecuteSequence> ... <Custom Action='LaunchFile' After='InstallFinalize'>NOT Installed</Custom> </InstallExecuteSequence>
In some cases, we want to start a helper utility we carry along in the installation package but we don't install on the user's machine (for instance, a readme file viewer or a special configuration utility). Instead of the File, we refer to the identifier in a Binary tag. The scheduling is the same:
<CustomAction Id='LaunchFile' BinaryKey='FoobarEXE' ExeCommand='' Return='asyncNoWait' />
We can also launch any other executable on the user's machine if we provide its name in a property:
<Property Id='NOTEPAD'>Notepad.exe</Property> <CustomAction Id='LaunchFile' Property='NOTEPAD' ExeCommand='[SourceDir]Readme.txt' Return='asyncNoWait' />
Custom actions can also specify how their return will be handled, using a Return attribute. Possible values are: check will wait for the custom action to finish and check its return value, ignore will wait for the action but the return value will be ignored, asyncWait will run asynchronously but the installer will wait for the return value at the end of the scheduling sequence and asyncNoWait will simply launch the action and then leave it alone, the action might still be running after the installer finishes. This last value is the one we use when we want to launch an application or a readme file after the installation.
If we encounter an error the normal machinery can't report, we can display an error message and terminate the installation. The Error attribute can contain either the actual message text or the Id identifier of an Error tag:
<CustomAction Id='AbortError' Error='Cannot solve this riddle. Giving up.' />
There is no direct way to assign the value of a property to another one but a custom action can bridge this gap. The Value attribute can be a formatted string, thus we can perform some string manipulation, too (note that path references always have their trailing backslash automatically, there is no need to add an extra one):
<CustomAction Id='PropertyAssign' Property='PathProperty' Value='[INSTALLDIR][FilenameProperty].[ExtensionProperty]' />
A directory can also be set to a similarly formatted string representing a path:
<CustomAction Id='PropertyAssign' Directory='INSTALLDIR' Value='[TARGETDIR]\Program Files\Acme\Foobar 1.0\bin' />
3.3 What's Not in the Book
For very specialized actions that the Windows Installer provides no solution for (eg. checking the validity and integrity of the registration key entered by the user), we can use yet another type of custom action: a DLL we write. For our example, we use a simplistic approach: we will accept the user key if its first digit is '1'.
The following source can be directly compiled with Visual C++ but minimal modifications, if any, will be required to compile it with a different compiler. The msi.h and msiquery.h header files can be acquired from the MSI SDK. You also have to link against msi.lib.
#include <windows.h>
#include <msi.h>
#include <msiquery.h>
#pragma comment(linker, "/EXPORT:CheckPID=_CheckPID@4")
extern "C" UINT __stdcall CheckPID (MSIHANDLE hInstall) {
char Pid[MAX_PATH];
DWORD PidLen = MAX_PATH;
MsiGetProperty (hInstall, "PIDKEY", Pid, &PidLen);
MsiSetProperty (hInstall, "PIDACCEPTED", Pid[0] == '1' ? "1" : "0");
return ERROR_SUCCESS;
}
To use this DLL, add the following lines to the appropriate places (now, nearing the end of the third lesson, you might be able to do this yourself but if you want to cheat, download SampleCA).
<Condition Message='This installation can only run in full UI mode.'> <![CDATA[UILevel = 5]]> </Condition> <CustomAction Id='CheckingPID' BinaryKey='CheckPID' DllEntry='CheckPID' /> <CustomAction Id='RefusePID' Error='Invalid key. Installation aborted.' /> <InstallExecuteSequence> <Custom Action='CheckingPID' After='CostFinalize' /> <Custom Action='RefusePID' After='CheckingPID'>PIDACCEPTED = "0" AND NOT Installed</Custom> </InstallExecuteSequence> <Binary Id='CheckPID' SourceFile='CheckPID.dll' />
To summarize: first, we won't allow the installer to run with reduced or no UI because the user can't enter a registration key in those cases. The reason for the ugly CDATA wrapper is that XML attributes special meaning to some characters, most notably < and >. Wherever they appear in a different context, meaning less-than or greater-than, we have to escape them by wrapping the whole expression into a CDATA. Although this actual case could get away without it because it only checks for equality, it is a good habit to learn to wrap all similar conditional values just in case we need to modify them later, introducing such XML conflicts.
Then, we have a custom action named CheckingPID running after CostFinalize, when we instruct the installer to start the actual installation after having specified which features we need and where we want to install. This action will call the CheckPID function in our CheckPID.dll, bundled with the installer. The DLL sets the PIDACCEPTED property to either 1 or 0, according to its decision on the validity of the user key entered and stored into the PIDKEY property by the control involved. Note that using properties (with all uppercase names, otherwise Windows Installer will not treat them as public properties) is the only way to pass arguments to and from the custom action.
We have a second custom action named RefusePID, scheduled to run after the previous action. This is a conditional custom action, only run if the returned PIDACCEPTED property is found to be zero. In this case, the custom action will display an error message and abort the installation. But we will only be interested in this value during the installation, not while we're uninstalling the product.
To get an understanding of how these actions are called and how they relate to each other, we might run the installer with verbose logging. As it will be verbose, using a text editor and searching for our property and custom action names (“PID” will do just fine) might help locate what's really happening.
msiexec /i SampleCA.msi /l*v SampleCA.log
If the DLL we need to call has been installed rather than just included in the package, we can use:
<CustomAction Id='CheckingPID' FileKey='HelperDLL' DllEntry='CheckPID' />
3.4 Custom Actions in Controls
Granted, what we've just done was not elegant. We should check right when the user enters the key, display a warning and give the user a chance to re-enter the key instead of aborting the installation in a later phase. Let's see how we can accomplish this.
We can link custom actions to two kinds of user interface controls, push buttons and check boxes. We can use the Publish tag we already know to accomplish this. The Value attribute carries the name of the custom action:
<Control Id="..." Type="PushButton" ...> <Publish Event="DoAction" Value="CheckingPID">1</Publish> </Control>
This will cause the custom action to call our DLL when the user presses the Next button of the User Information page. The custom action will be linked to this UI event, thus we no longer need to schedule it in the InstallExecuteSequence tag. However, the custom action definition remains in the source:
<CustomAction Id='CheckingPID' BinaryKey='CheckPID' DllEntry='CheckPID' />
To warn the user, we need a message box. This is just another dialog, similar to our previously created User Information page. We can put it into a serarate source file as a fragment and refer to it using a DialogRef tag, as earlier. Just to illustrate that there is another solution as well, we now specify it directly in our main source file, right inside the UI section.
<Dialog Id="InvalidPidDlg" Width="260" Height="85" Title="[ProductName] [Setup]" NoMinimize="yes">
<Control Id="Icon" Type="Icon" X="15" Y="15" Width="24" Height="24"
ToolTip="Information icon" FixedSize="yes" IconSize="32" Text="Exclam.ico" />
<Control Id="Return" Type="PushButton" X="100" Y="57" Width="56" Height="17" Default="yes" Cancel="yes" Text="&Return">
<Publish Event="EndDialog" Value="Return">1</Publish>
</Control>
<Control Id="Text" Type="Text" X="48" Y="15" Width="194" Height="30" TabSkip="no">
<Text>
The user key you entered is invalid. Please, enter the key printed on the label
of the jewel case of the installation CD.
</Text>
</Control>
</Dialog>
We also have to update the User Information page because we have to call our custom action and the new message box from this dialog:
<Control Id="Next" Type="PushButton" X="236" Y="243" Width="56" Height="17" Default="yes" Text="[ButtonText_Next]"> <Publish Event="DoAction" Value="CheckingPID">1</Publish> <Publish Event="SpawnDialog" Value="InvalidPidDlg">PIDACCEPTED = "0"</Publish> <Publish Event="NewDialog" Value="SetupTypeDlg">PIDACCEPTED = "1"</Publish> </Control>
Now, when the user presses the Next button, the function in the DLL is called (every time because the condition evaluates to true). The function in our DLL will check the PIDKEY property and set PIDACCEPTED to signal whether the key was accepted. If it was, we go on to SetupTypeDlg. If it wasn't, we display our error message.
There is only one small item left, as we mentioned an icon in the message box, we also have to provide it in the installer:
<Binary Id="Exclam.ico" SourceFile="Exclam.ico" />
The full source can be downloaded as SampleAskKey.
By the way, it's not necessarily a nice and safe thing to have the user key appear in the log file. To avoid this, use:
<Property Id="PIDKEY" Hidden='yes' />
3.5 How to Manage Custom Actions?
A common question is whether custom actions can be written in managed code, C#, VB.NET or something similar. After all, these runtime environments offer a much richer set of features, besides, there might be some programmers working with these languages who are less familiar with other programming languages.
Earlier, in the version 2 era, writing custom actions in managed code was only possible with hacks and was considered very bad and dangerous practice. Version 3 brought changes with the introduction of its Deployment Tools Foundation (DTF), a set of .NET class libraries and related resources. If you can accept the obvious dependency limitations (you have to make sure .NET is present on the installation machine, possibly using a bootstrap installer first, and you can have problems during uninstallation as well, if the user removes the .NET Framework before uninstalling your application), here is our previous sample custom action ported to C#:
namespace WiXTutorial.Samples
{
using System;
using System.Collections.Generic;
using System.IO;
using Microsoft.Deployment.WindowsInstaller;
public class SampleCheckPID
{
[CustomAction]
public static ActionResult CheckPID(Session session)
{
string Pid = session["PIDKEY"];
session["PIDACCEPTED"] = Pid.StartsWith("1") ? "1" : "0";
return ActionResult.Success;
}
}
}
There is one tiny bit we have to modify in our source code of SampleAskKeyNET. The name of the DLL will be different because the straight managed DLL has to be wrapped into a special package bridging the gap between Windows Installer and the managed world:
<Binary Id="CheckPID" SourceFile="CheckPIDPackage.dll" />
Also prepare a small CustomAction.config file. It will describe the runtime your managed custom action is dependent upon.
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<startup>
<supportedRuntime version="v2.0.50727"/>
</startup>
</configuration>
There is a price to pay for the relative simplicity of working with a .NET language: the build process is more complicated. If you use an IDE, it might be simpler, you can find a sample project file under the DTF branch of the WiX source package. Here we only can show you the command line way. Look for Microsoft.Deployment.WindowsInstaller.dll, MakeSfxCA.exe and sfxca.dll in your installed WiX toolset, you will find them there. Where we indicated a path, you have to supply an absolute full path or else MakeSfxCA will give an error message and the resulting DLL will not be usable.
csc.exe /target:library /reference:path\Microsoft.Deployment.WindowsInstaller.dll /out:CheckPID.dll CheckPID.cs MakeSfxCA.exe path\CheckPIDPackage.dll path\sfxca.dll path\CheckPID.dll path\CustomAction.config path\Microsoft.Deployment.WindowsInstaller.dll candle.exe SampleAskKeyNET.wxs UserRegistrationDlg.wxs light.exe -ext WixUIExtension -out SampleAskKeyNET.msi SampleAskKeyNET.wixobj UserRegistrationDlg.wixobj
DTF has its own documentation in the toolset, thus we won't discuss it further in our tutorial. Just use the documentation and the sample code there.
3.6 At a Later Stage
Many custom actions that set properties, feature or component states, target directories or schedule system operations by inserting rows into sequence tables, can in many cases use immediate execution safely. Other custom actions that require to change the system directly or to call another system service must be deferred to such time when the installation script is executed. The Windows Installer writes these deferred custom actions into the installation script for later execution.
Deferred custom action is defined in the following way:
<CustomAction Id="MyAction" Return="check" Execute="deferred" BinaryKey="CustomActionsLibrary" DllEntry="MyAction" HideTarget="yes"/>
The Execute attribute will specify the deferred status of our custom action. We need to refer to the DLL function we need to call using the DllEntry attribute (don't forget the decoration for C++-style names like _MyAction@4 if your compiling environment requires this). And finally, HideTarget will allow us to disable logging the parameteres passed to this custom action if security considerations so dictate.
Because the installation script will be executed outside of the normal installation session, this session may no longer exist during the execution of the deferred action; neither the original session handle nor any property data set during the installation sequence will be available to the deferred action. The very limited amount of information the custom action can obtain consists of three properties:
- CustomActionData
- value at time custom action is processed in sequence table. This property is only available to deferred execution custom actions, immediate ones do not have access to it,
- ProductCode
- the unique GUID code for the product,
- UserSID
- the user's security identifier (SID), set by the installer.
If we need to pass any other property data to the deferred action, we can use a secondary custom action to set this value beforehand. The simplest solution is a property assignment custom action. Set it up in a way that the name of the property set will be the same as the Id attribute of the deferred custom action:
<Property Id="SOME_PUBLIC_PROPERTY">Hello, from deferred CA</Property> <CustomAction Id="MyAction.SetProperty" Return="check" Property="MyAction" Value="[SOME_PUBLIC_PROPERTY]" />
Scheduling the property assignment before the deferred action is important, too:
<InstallExecuteSequence> <Custom Action="MyAction.SetProperty" After="ValidateProductID" /> <Custom Action="MyAction" After="MyAction.SetProperty" /> </InstallExecuteSequence>
The data we wanted to pass will appear in the CustomActionData property. Should we need to pass more than one piece of information, we have to devise a way to incorporate them into this single property, for instance, to use a semicolon separated list of Name=Value pairs.
#include <windows.h>
#include <msi.h>
#include <msiquery.h>
#include <tchar.h>
#pragma comment(linker, "/EXPORT:MyAction=_MyAction@4")
extern "C" UINT __stdcall MyAction (MSIHANDLE hInstall) {
TCHAR szActionData[MAX_PATH] = {0};
DWORD dActionDataLen = MAX_PATH;
MsiGetProperty (hInstall, "CustomActionData", szActionData, &dActionDataLen);
MessageBox (NULL, szActionData, _T("Deferred Custom Action"), MB_OK | MB_ICONINFORMATION);
return ERROR_SUCCESS;
}
Vadym Stetsyak
