How to Manually Create an Installation for the Windows Installer Service Gary Chirhart

How to Manually Create
an Installation for the
Windows Installer Service
Gary Chirhart
Senior Software Developer
Wise Solutions, Inc.
Table of Contents
Table of Contents
Introduction ...............................................................................................Page 1
Built-in Functionality Saves Time ..............................................................Page 2
What’s in an Installer Package ..................................................................Page 3
Creating a Database Layout .......................................................................Page 4
Reading/Writing a Windows Installer File ..................................................Page 7
A Simple Viewer Example ..........................................................................Page 8
Using Properties ......................................................................................Page 12
Conditions................................................................................................Page 13
Developing the User Interface..................................................................Page 14
Sequence Tables ......................................................................................Page 17
Merge Modules ........................................................................................Page 19
Transforms...............................................................................................Page 20
Conclusion...............................................................................................Page 21
Introduction
Introduction
By now, most software developers have begun exploring the new Windows Installer service that is part of
Windows 2000. A key component of Microsoft’s Zero Administration Initiative for Windows, the Windows
Installer is designed to standardize installs and uninstalls on the Windows platform and make creating
installations easier.
Windows Installer offers many advantages to developers and systems administrators. Standardization of
installs and uninstalls will simplify an administrator’s job by creating one set of rules for file overwrites,
instead of leaving rule selection up to individual developers. Windows Installer also includes many other
features for easier development that the installation author can automatically include in the install.
Another reason to use the Windows Installer is that the Application Specification for Windows 2000
(formerly logo requirements) requires an install that uses a Windows Installer package that passes
validation requirements. Since there is a Windows Installer runtime for Windows 9x and Windows NT
4.0, most of these benefits can be seen on existing platforms.
Sounds great, doesn’t it? But how is an installation built using Windows Installer? This white paper will
explain the nuts and bolts of creating an installation package using the Installer and recommend ways
to use Windows Installer to its best advantage. We’ll review the database layout, how to read and write to
the database, how to create a custom user interface, and how to further customize your install beyond
Windows Installer’s built-in functionality. We’ll also examine merge modules and transforms, two other
features that aid developers and administrators.
Page 1
Built-in Functionality Saves Time
Built-in Functionality Saves Time
Windows Installer has many built-in functions that save time and effort. One beneficial built-in function is
automatic add/remove, uninstall, reinstall, and repair support. If an application is already installed,
Windows Installer will detect it and display a maintenance user interface instead of an application install
interface. In maintenance mode, Windows Installer detects which parts of an application are installed and
can allow a user to add or remove features or reinstall the application. The repair functionality also appears
in maintenance mode, but is also automatically run every time a shortcut for the application is activated in
Windows 2000.
The repair functionality is particularly useful for administrators whose users like to recklessly clean up disk
space. The next time the "broken" application runs, Windows Installer checks a key list of files and registry
keys, and repairs the application if needed. The install author also gets built-in network install functionality.
By selecting Install to Network from the context menu, an administrator can create an install image on a
network share point from which users can install the application. Users can choose whether to install the
files locally or run the application from the network share.
Administrators will also benefit from the Installer’s advertising feature. Advertising allows an application to
appear to be installed, but files are not actually installed on the system until the application is activated
through a shortcut, extension, or COM server. Advertising makes an application available on the desktop, but
since it’s not installed until it is activated, companies save on licensing fees and disk space. Once a user
clicks on a shortcut or double-clicks on a file associated with that application, the application installs automatically.
The same functionality can be written into the application itself. For example, an application may have a
particular feature that is rarely used, so it is not installed by default. A developer could put Windows Installer
API calls in the code that activates that feature to make it install on demand. Using installation on demand
saves disk space and avoids requiring the user to exit the program and rerun the setup to install features that
aren’t installed by default. Using Windows 2000, an administrator can control this process by remotely
assigning an application to desktops which simply advertises the product on the destination computer and
provides the user with application entry points, such as shortcuts and file extensions. The administrator can
also publish the application. Publishing does not provide entry points to the user, but only to other applications through COM.
Another benefit gained by using the Windows Installer is that it runs as a service under Windows NT/Windows
2000. Thus, installs can run using elevated privileges to perform registry and file updates, even for users with
very low access rights. System policies can be set up to limit this feature to applications only provided by an
administrator. Other installs that a user chooses to run would do so only under the user’s privilege level. An
additional advantage is that the install is run as a transaction. If an error prevents the install from completing properly, Windows Installer will rollback the system to the state before the install began. Obviously, rollback requires more disk space, but it can be disabled using system policies.
Page 2
What’s in an Installer Package
What’s in an Installer Package
A Windows Installer installation package can contain everything required to perform an install or uninstall
with a user interface. The package file itself is a COM-structured storage file containing the installation
database and a summary stream. Optionally, the package file can contain additional streams with the actual
file bits compressed in cabinet files. Package files have the extension .msi and are associated with
msiexec.exe, which kicks off the installation process.
The installation database contains tables and relationships like typical relational databases. Each table has
one or more columns specified as keys that must be unique for that table. Windows Installer also supports
foreign keys. Foreign key columns are usually denoted by the name of the column they reference, followed
by an underscore. Figure 1 contains some of the important tables for creating an install and the relationship of these tables to each other.
Figure 1.
Page 3
Creating a Database Layout
Creating a Database Layout
When building a basic file install, the main tables to be populated are Feature, FeatureComponents,
Component, Directory, File, Media, and Property. Actions and a user interface are also important, but installer
templates typically have data in these tables that support a simple user interface. Third-party tools handle the
complexity of populating these tables without the user having to worry about the details. Of course, it is still a
good idea to know what happens behind the scenes to have full control of your install.
Features represent a logical grouping of resources in an install. Features are what users typically might turn on
or off during a custom install. Examples are sample files, templates, and multiple language support. These
items can be installed or not, depending on the user’s needs.
The Feature table contains information for each feature in an install. Since features can be nested, the
Feature_Parent column specifies a feature’s parent key. A child feature will typically inherit properties from its
parent and can only be installed if the parent feature is installed. The Display column determines if and how a
feature will be displayed in a feature selection tree control in the user interface. It orders the features for display in the feature tree; a value of zero means the feature is hidden. In addition, an odd value specifies that the
feature is expanded initially and an even value means the feature is collapsed initially. The Level column
assigns an integer to the feature that is compared with the InstallLevel property at runtime. If the Level value
of the feature is less than InstallLevel, the feature is turned on initially; otherwise, it’s turned off. InstallLevel is
usually set by a complete, custom, or typical selection in a dialog, allowing features to be set as part of a typical
installation by the install author.
The Directory_ column is a foreign key into the Directory table that optionally specifies a directory that can be
configured at runtime. Typically, the top-level feature is assigned to the main install directory entry so that
configuring the directory for this feature causes all other features to move below it. The Directory_ column can
be used to place features of an install at completely different locations. For example, a graphics program may
have a large feature containing sample graphics. If the program were authored to take advantage of the
Windows Installer, the install could allow this feature to be installed at a different location, such as a network
drive, and the application would then query the Windows Installer for this location at runtime. Finally, the
Attribute column contains a bit mask of values describing how the feature should be installed. You can specify
whether a feature should be installed locally or run from the original source medium, such as a CD or a network drive. You can also specify that a feature is required for a successful install and whether or not the feature
can be advertised.
From the developer’s viewpoint, components are the smallest piece of an install. Files, registry keys, shortcuts,
and other installation resources are assigned directly to a component. A component may only be installed or
uninstalled as a single unit. In the Components table, the Directory_ column assigns the component to a single destination directory and any files assigned to this component are installed in that directory.
The KeyPath column represents the critical file, registry key, or ODBC data source assigned to the component; if
missing, this will cause Windows Installer to repair the entire component. Several table relationships also rely
on the key path of a component. For example, advertised shortcuts are assigned to a component whose key
path specifies the target file for the shortcut. Because of this relationship, a component only can be assigned
one advertised shortcut target, COM object, service, or extension server.
Windows Installer keeps reference counts on components to manage any install resource instead of file reference counting. Each component is assigned a GUID in the ComponentId column to make it unique for component reference counting. Through reference counting components, all resources of an install including registry keys can be reference counted.
Page 4
Creating a Database Layout
The Condition column allows the installation author to specify a condition that must be met for the
component to be installed. If a component’s condition resolves to FALSE during installation, the component
will not be installed regardless of which features are installed. The Condition column typically is used to
install different files on different operating systems by setting a condition that evaluates to TRUE for a
specific operating system. I’ll explain conditions in more detail later.
Features usually are made up of several components that are installed when the feature is installed. The
FeatureComponents table is used to assign components to features. Using this table, a component can be
assigned to multiple features, but the component itself will only be installed once, regardless of how many
features with which it’s associated.
The Directory table is used to specify both the source (if the files are left uncompressed outside of the .msi
file) and destination directory layouts for a product. It contains a key column, the Directory_Parent
column, and the DefaultDir column. In Windows Installer, directory structure is built one folder at a time.
The DefaultDir column holds the name of a single subdirectory and the Directory_Parent column specifies
the key to the parent directory. The entire directory structure is built using this recursive relationship.
There are a few noteworthy features about the Directory table that aren’t readily apparent. If a property is set
that matches the key column, the value of the property overrides the DefaultDir and Directory_Parent
columns. This feature is used to set directories during install through the user interface or command line.
The syntax for the DefaultDir column can contain a colon separating the target directory from the source
directory if they differ. Also, each source and destination directory name can contain both a short and long
file name separated by a pipe '|' symbol. A period can be used to designate that a given directory’s location is
in same path as the parent directory without using a subdirectory. These constructs are commonly used to
separate operating system or processor specific files of the same name in source directories, while putting
them in the same place on the destination machine. Figure 2 contains an example of Directory table
entries using this syntax. The destination directory for all files associated with this application will be put in
[TARGETDIR]\MyApplication, but the source path is different for files associated with components assigned
to the Win95files directory. These files can be found at [SourceDir]\MyApplication\Win95 for an installation
with files outside the .msi file.
Directory
Directory_Parent
DefaultDir
Destination Directory
SourceDir
[TARGETDIR]
MyApplication
TARGETDIR
MyAppl~1|MyApplication
[TARGETDIR]\MyApplication
Win95files
MyApplication
.:Win95
TARGETDIR]\MyApplication
WinNTfiles
MyApplication
.:WinNT
TARGETDIR]\MyApplication
TARGETDIR
Figure 2.
The File table contains information on the version, size, and language for files to be installed. You can also
set attributes such as read-only, hidden, and system on a per file basis. The sequence number is a critical
column within a file row. This number represents the order of the files in the cabinet files; for merge modules, these numbers must be sequential starting from one.
Page 5
Creating a Database Layout
File sequence numbers are related to the LastSequence column in the Media table. The Media table specifies
where source files are located during an install. A file belongs to the media entry with the lowest
LastSequence value greater than or equal to the file’s sequence number. Therefore, media rows can be set up
to specify different source media for different sets of files. Each media row also has a Cabinet column
naming the cabinet file for its range of files. A Cabinet entry that is preceded by a '#' specifies that the
cabinet is inside the .msi file as a separate stream. The Cabinet column of the media row then has the
format #Cabs.cabinetname, where cabinetname matches the Name column in the Cabs table.
To support the advertising concept, Windows Installer has implemented several tables that represent classes,
extensions, typelibs, Prog ids, App ids, verbs, and MIME types. These tables are used to populate the registry
during an advertised install, so that an advertised application that has an extension server can be installed
on demand when a user invokes a file with that extension. Since this information is contained in these
tables, it should be left out of the registry table. In fact, a verification check on registry keys that should be
in the advertising tables will be flagged as an ICE33 (Internal Consistency Evaluator) error.
Page 6
Reading/Writing a Windows Installer File
Reading/Writing a Windows Installer File
Having reviewed some of the more important tables, the next step is to populate them for the application.
An installation author can use a table editor such as Orca (included in the SDK), a third party tool such as
Wise for Windows Installer or InstallShield for Windows Installer, or write to the database using APIs provided
by msi.dll. Due to the complexity of creating a valid install, the SDK recommends that you validate any .msi
file that is created before performing an install. If you choose to use Orca or write code to manipulate a
Windows Installer database, you should use one of the third party tools or the MsiVal2.exe command line
tool in the SDK to validate your install database using ICE. MsiVal2.exe checks your package file for
database consistency errors and other Windows Installer requirements.
Directly manipulating records in an installation package is similar to writing SQL code. In fact, the
installer database format supports parameterized queries, inner joins, and a transaction mode where changes
can be committed or rolled back. Most of the APIs in the Windows Installer use the MSIHANDLE datatype,
which is an unsigned long used as a handle to such things as the database object, the installer object, views,
and records. Also, most APIs return an unsigned integer as an error code that can be checked against
ERROR_SUCCESS. The common APIs used for data manipulation are shown in Figure 3.
Windows Installer API
Description
MsiOpenDatabase
Opens an installer database. Can specify read-only,
transacted, or create.
MsiDatabaseOpenView
Prepares a SQL query and assigns it to a view object.
MsiViewExecute
Performs the query assigned to the view and
assigns a result set to the view.
MsiViewGetColumnInfo
Retrieves a single record that contains column names or data types.
MsiViewFetch
Reads the next sequential record from the view.
MsiRecordDataSize
Gets the size of a particular column from a record.
Most often used with binary data.
MsiRecordGetString,
MsiRecordGetInteger,
MsiRecordReadStream
Read data from a column of a record to an
integer value or a character pointer.
MsiCreateRecord
Creates a new record of a given size to write to the installer database.
MsiRecordSetString,
MsiRecordSetInteger,
MsiRecordSetStream
Write data to a column of a record from
an integer or character pointer.
MsiViewModify
Performs an action on a fetched record such as
inserting, updating, or deleting.
MsiViewClose
Closes the result set assigned to the view.
MsiCloseHandle
Closes Windows Installer handles.
Figure 3.
Page 7
A Simple Viewer Example
A Simple Viewer Example
As an example, I have written an MFC application that opens an .msm or .msi file for browsing its tables
using many of the Windows Installer APIs shown in Figure 3. The core of the code is contained in the
function CSimpleViewerDlg::ReadWindowsInstallerTable shown in Figure 4. ReadWindowsInstallerTable
takes as parameters the path to a database file, the name of the table to read, and references to the number of
columns read, a string array of column names, a word array of types, and an object list containing string
arrays of the data in each record.
The first step to reading or editing an .msi file is to call MsiOpenDatabase. The second parameter, of type
LPCTSTR, can be a new output path or, as in my example, a predefined persistence constant specifying the
open mode such as read-only, transact, or create. After opening the database file, the next step is to create a
view of the data I am interested in. I simply created a query that selects all of the records of the given table
and passes it to MsiDatabaseOpenView. MsiDatabaseOpenView prepares the query and assigns it to the view
object passed in the third parameter. MsiViewExecute actually performs the query using the view handle and
an optional handle to a record that contains SQL parameter values.
SimpleMSIViewer then builds a list of columns and data types by calling MsiViewGetColumnInfo. The first
call reads the column names into a record, then loops through each column of the record to fill the string
array. The second call to MsiViewGetColumnInfo reads the data types of the columns into a record. Each
column of the returned record has a string value that indicates the type of data and size. The first character of
the string represents the data type and can be either 's' for strings, 'l' for localizable strings, 'i' for integers, or
'v' for binary streams. If the character representing the type is lower case, the field is not allowed to contain a
NULL value. The type character is followed by an integer specifying the length of the data field. Two things to
note with the length integer are that strings of zero length are interpreted as variable length strings and the
length of integers is interpreted as a length in bytes to hold the integer.
After reading the column information, I am ready to read the actual records. Using the handle to the open view,
MsiViewFetch is used to read each record from the resulting query. The two parameters are a handle to the view
and an output handle to the fetched record. MsiViewFetch creates a record based on how many columns are
read by the query. The SDK help file recommends that you reuse the same record for each fetch for performance
reasons. Records are fetched in sequential order until there are no more records, which will be indicated by a
NULL handle for the record and a return value of ERROR_NO_MORE_ITEMS.
Reading the values of the record into standard variable types requires MsiRecordGetString or
MsiRecordGetInteger. Note that the field number for these functions starts at one instead of zero. Each table
can also have one binary stream column, which is read using MsiRecordDataSize, to get the length of the object,
followed by a call to MsiRecordReadStream. My example calls the appropriate function to read strings or
integers and fills up the object list with string arrays of records containing the data for a table. The calling
function uses this information to display the rows in a list control. Figure 5 shows the dialog table in template.
BOOL CMsjDlg::ReadWindowsInstallerTable( const char *szPathName,
const char *szTableName,
UINT &uColumns,
CStringArray &saColumnNames,
CWordArray &waTypes,
CObList &olRecords)
{
MSIHANDLE hDatabase, hView, hRecord;
char szSelect[256], szTemp[256], szValue[32768];
Page 8
A Simple Viewer Example
long lValue;
BOOL bSuccess = TRUE;
DWORD dwLength;
if (MsiOpenDatabase(szPathName,MSIDBOPEN_READONLY,&hDatabase)
!= ERROR_SUCCESS) {
return FALSE;
}
// build query for all records in this table
wsprintf(szSelect,"SELECT * from %s",szTableName);
if (MsiDatabaseOpenView(hDatabase,szSelect,&hView) != ERROR_SUCCESS) {
return FALSE;
}
// execute query - not a parameter query so second parameter is NULL.
if (MsiViewExecute(hView,NULL) != ERROR_SUCCESS) {
return FALSE;
}
// read column names
MsiViewGetColumnInfo(hView,MSICOLINFO_NAMES,&hRecord);
// get total number of columns in table.
uColumns = MsiRecordGetFieldCount(hRecord);
saColumnNames.SetSize(uColumns);
waTypes.SetSize(uColumns);
// read in the column names from the record to our StringArray
for (unsigned int i = 0; i < uColumns; ++i) {
dwLength = 256;
if (MsiRecordGetString(hRecord,i + 1,szTemp,&dwLength) != ERROR_SUCCESS) {
return FALSE;
}
saColumnNames[i] = szTemp;
}
MsiCloseHandle(hRecord);
// get the data types for each column
if (MsiViewGetColumnInfo(hView,MSICOLINFO_TYPES,&hRecord)
!= ERROR_SUCCESS) {
return FALSE;
}
for (i = 0; i < uColumns; i++) {
long length;
dwLength = 256;
MsiRecordGetString(hRecord,i + 1,szTemp,&dwLength);
length = atol(&szTemp[1]);
switch(tolower(szTemp[0])) {
case('s'):
// normal string type
case('l'):
// localizable string
waTypes[i] = TYPE_STRING;
break;
case('i'):
waTypes[i] = TYPE_INTEGER;
break;
case('v'):
waTypes[i] = TYPE_BINARY;
break;
}
}
MsiCloseHandle(hRecord);
// read records until there are no more records
while (MsiViewFetch(hView,&hRecord) == ERROR_SUCCESS) {
CStringArray *psaRecord = new CStringArray;
Page 9
A Simple Viewer Example
psaRecord->SetSize(uColumns);
olRecords.AddTail(psaRecord);
for (i = 0; i < uColumns; i++) {
switch(waTypes[i]) {
case(TYPE_INTEGER):
lValue = MsiRecordGetInteger(hRecord,i + 1);
(*psaRecord)[i].Format("%d",lValue);
break;
case(TYPE_STRING):
dwLength = 32768;
MsiRecordGetString(hRecord,i + 1,szValue,&dwLength);
(*psaRecord)[i] = szValue;
break;
case(TYPE_BINARY):
(*psaRecord)[i] = "{Binary Data}";
/*
don't read binary data into string, if you want to read it into
a BYTE buffer, the code looks like:
DWORD dwLen = MsiRecordDataSize(hRecord,i + 1);
char *pBinary = new char[dwLen];
MsiRecordReadStream(hRecord,i + 1,pBinary,&dwLen);
*/
break;
}
}
}
MsiCloseHandle(hRecord);
MsiViewClose(hView);
MsiCloseHandle(hView);
MsiCloseHandle(hDatabase);
return TRUE;
}
Figure 4.
Figure 5.
Page 10
A Simple Viewer Example
Writing records to a Windows Installer package is similar to reading; you open the database and a view.
Instead of performing fetches, however, you need to create new records using MsiCreateRecord, which requires
a count of how many fields are needed in the record. Then, each field is filled with data using
MsiRecordSetString, MsiRecordSetInteger, or MsiRecordSetStream.
MsiRecordSetStream is has a different signature than the functions dealing with integers and strings.
It requires a file path to load into the stream, so you need to have your binary data in a file before you can
add it to a record. After the record is ready, call MsiViewModify to write the record. MsiViewModify supports
the SQL-like operations MSIMODIFY_ASSIGN, MSIMODIFY_INSERT, MSIMODIFY_UPDATE and
MSIMODIFY_DELETE. You may find that MsiViewModify fails, returning ERROR_FUNCTION_FAILED
without any other information. This function typically fails for one of three reasons. Either the data in a
column does not meet criteria specified in the _Validation table, a field is NULL when it should not be, or a
foreign key does not have a corresponding primary key row in another table. I have not found a way to get
this information through APIs, but have learned it through trial and error.
Another possible problem can occur when the select statement uses the wild card character to specify selecting
all columns. Such a select statement can result in a mismatch between the columns you think you are
writing and the actual column order in the query. This problem can become bigger because the database
tables themselves can change from version to version of Windows Installer. It is best to hard code your
column names and their order in all queries.
Page 11
Using Properties
Using Properties
Windows Installer supports changing an install at runtime through properties. The installer uses properties as
variables to hold information used during an installation. During an install, conditional statements typically
use properties to verify system state, determine a user choice, or in some way alter an install. Information
about properties is kept in the Property table. The two columns are Property, which is the key, and Value.
Both are string types and Windows Installer SDK .MSI templates tend to limit the size of the value column to
128 characters, though this isn't necessarily enough to hold a full file path.
Three types of properties exist in the Windows Installer. Private properties typically describe the system
environment during install and are set by the installer. Examples include built-in directories, product install
state, system resolution and color, and operating system. Private properties are not alterable by the user at
runtime. Public properties, on the other hand, are used to customize an install at runtime. A user or systems
administrator can set the value of public properties using the command line, a transform (discussed below),
or selections made in the user interface. Restricted public properties are similar to public properties, but
cannot be changed by a user in a managed installation.
The property types are differentiated through different capitalization. In order for a property to be passed
through to the server side, it must be in all capital letters. Therefore, private properties, which include lower
case letters, are never passed through to the actual install execution. Therefore, any conditions that occur in
the Execute sequence (discussed below) can be based on private properties. On locked down machines, only
restricted public properties are seen in the execute sequences. For more information on restricted public
properties, see the SDK.
One important property to note is ProductCode. This property’s value is a GUID for an entire product.
Windows Installer tracks product codes for applications installed. Rerunning an install whose product
code is already tracked as being installed will set the Installed property. The user interface typically will
show maintenance or repair dialogs when the Installed property is set and the install itself will perform
different functions.
There is a Formatted data type in Windows Installer, which provides a way to take advantage of properties at
runtime. The Formatted type contains text and can contain strings that are resolved to common installer
values. The most common is the use of braces "[]" to get the value of a property. For example, to display the
value of the property ProductName in a dialog, place brackets around it for a text control’s text value, such as
"Installing [ProductName]".
Another useful construct in Formatted columns is [#FileKey]. This expression is replaced at runtime by the
full path to the file whose key is referenced. The filekey expression is often used to set a registry key to point to
a file whose location is not determined until installation. [!FileKey] is similar, except that it resolves to the
short path name to a file, which is useful for some registry references that require short file names.
Page 12
Conditions
Conditions
Properties are mainly used to gather information about the system or obtain information from the user to
customize an install. To do this, Microsoft has implemented a condition syntax. Conditions are expressions
that contain Properties and logic that can evaluate to TRUE or FALSE. Conditions are found throughout a
Windows Installer database file. The LaunchCondition table contains conditions that must be met for an
install to occur. If the condition is not met, the install will be halted and an author-supplied message will be
displayed. The install author also can use conditions within components to make the components install only
when certain criteria are met. One example is an install where different files need to be installed depending
on the destination operating system. The components are created with a condition based on the properties
Windows9X and WindowsNT (which are set to TRUE if the installation is running under Windows 9x or
Windows NT respectively. Files associated with these components are only installed on the proper operating
system. Dialogs use conditions extensively to hide controls, determine the next dialog in a wizard, and
execute custom actions. Finally, every action in the install can have a conditional expression associated with it.
For the most part, condition syntax is straightforward. It uses an SQL or BASIC-like syntax that handles
operators, text strings, and integers. There are extensions to the syntax to handle the specific needs of
Windows Installer. Be careful using properties in conditions; they are case sensitive and if the property is not
defined, the reference to it will resolve to an empty string without warning.
Since property values are stored as strings, the authors of the syntax have included extra operators for string
manipulation. A tilde "~" before an operator denotes case insensitive compares. There are also operators to
determine if a string contains, begins with, or ends with another string. Another useful feature of the syntax is
building conditions based on whether a component or feature is already installed or will be installed.
During an install, a DLL can get the value of a condition by using the API, MsiEvaluateCondition. This
function is useful for performing different actions based on the system state or user choices. DLL calls in
Windows Installer packages are explained in more detail later on.
Page 13
Developing the User Interface
Developing the User Interface
One of the most difficult areas to hand code is the user interface. Luckily, there are third party Windows
Installer tools available, that give you complete control over this aspect of the install. Even with these tools to
simplify the process, you’ll need some knowledge of how the user interface works if you want to make slick
customizations to standard dialogs.
The user interface during an install is contained in several tables. The dialog table holds information for the
size, location, and attributes of the dialogs. Be aware that the units of size and location are not in standard
dialog units. The units used are 1/12 of the system font, which can throw you off if you’re trying to use a
typical dialog editor.
Of course, dialogs are useless without controls, so there are several tables that describe the controls that can
be placed in dialogs. Microsoft has provided several control types for customizing dialogs. The Control table is
the basic table used. This table contains all the information necessary for basic controls such as type, size,
location, and other attributes. A control can be assigned a property value that is set when the control is
changed. For example, an edit control’s text can set the value of the property to which it’s assigned.
Remember that only public properties can be passed to the installer service, so if you’re setting a property that
will be used during an execute sequence, it must be capitalized. Several control types require additional rows
in other tables, such as RadioButton, ComboBox and ListBox. These tables hold the individual items for these
multiple item controls.
An important part of a user interface is how the controls interact with the user. You can make a control's
attributes dependent on a condition by using the ControlCondition table. Other than the dialog and the
control, this table specifies a condition and an action to take if the condition is true. Valid actions in this table
are setting the control as the default, disabling, enabling, hiding, and showing the control. The condition is
evaluated at runtime to modify these attributes of a control.
The main table for interacting with the user during an install is the ControlEvent table. This table assigns
events that will occur when a control is triggered. These events are published to the installer and to all other
controls in the dialog. If the installer subscribes to the published event, it will execute an action. If another
control subscribes to the event, then the attributes of the subscribing control can change. The most common
actions change the attributes of another control, display a new dialog, end a dialog, or call a custom action.
The most useful events that can be published by controls are EndDialog, SpawnDialog, DoAction, SetProperty,
and NewDialog:
• EndDialog simply closes the current dialog when its control is triggered;
• The SpawnDialog event opens another dialog as a child popup dialog. When the dialog called by a
SpawnDialog event is closed using the EndDialog event, the parent dialog is redisplayed;
• The event DoAction calls custom actions that are described below;
• SetProperty sets a specific property’s value.
Connecting dialogs in a wizard is one of the most difficult parts of setting up a good user interface. The
NewDialog event is used to navigate to another dialog in a wizard. Each back and next button in a wizard
requires NewDialog events specifying the dialog to go to next. There is no record kept of which dialogs have
been shown, so the back buttons must have proper events to return to the previous dialogs. The difficulty arises
when properties determine which dialog is shown next. This requires a control event for each possible next
dialog and corresponding control events in the destination dialog that return the user to the first dialog.
Page 14
Developing the User Interface
The control event conditions must be mutually exclusive and cover any possible user choice. If a user choice
is not covered, the button can look pressed, but nothing will happen. If the conditions are not mutually
exclusive, the event with the lowest ordering value is run first. The biggest challenge is making sure that for
all property values, the wizard operates linearly for the user. Once you have your wizard hooked up, the
EndDialog event closes the entire wizard.
As mentioned above, by subscribing to events, a control can change its attributes based on an event published
by the installer or another control. The EventMapping table is used to subscribe to events for a control. An
EventMapping row has data for the dialog, the control, an event to subscribe to, and an attribute to set for the
control. The new value of the attribute is passed in the Argument column of the ControlEvent row. Some
common events that can be subscribed to are TimeRemaining, ActionText, SetProgress, SelectionNoItems, and
SelectionPath. For example, a SelectionTree control publishes the SelectionNoItems event with a NULL
argument when there are no items selected in the tree. A PushButton can subscribe to this event and set its
enabled attribute when this event is published. Since the argument of the SelectionNoItems event is NULL,
the enabled attribute of the button is set to NULL and the button is disabled.
Setting up tab ordering is worth noting. The Dialog table entry for each dialog has the column Control_First,
which specifies the first control in the tab order. From there, ordering is handled in each row of the Control
table. The column Control_Next specifies the next control in the tab order.
I have set up a simple example to illustrate some of these concepts. First, I created a new row in the Dialog
table called WebDialog and gave it the same dimensions as other dialogs in the install. Then, I copied controls
such as the graphics, lines, and back and next buttons from a different wizard dialog onto the new dialog.
To get the dialog in the wizard, I changed the Argument field for the NewDialog event in the Next button in the
previous dialog to WebDialog and repeated for the Back button in the dialog following my new dialog.
To make the wizard sequence complete, I added NewDialog events for WebDialog’s Back and Next buttons.
At this point, I have a dialog that will display during install and act like a wizard with full back and next
functionality. The main point I am demonstrating here is how controls and properties work together to
dynamically configure an install. I came up with a radio button group that asks the customer which part of
the website they’d like to visit. To do this, I created two properties, CheckWeb and WebAddress. (You could do
this with one property, but this example shows property syntax.) To create the radio button, I added a row to
the Control table for our dialog of type RadioButtonGroup and assigned the property CheckWeb to it. The
items of the radio button group require entries in the RadioButton table. These rows are keyed by the property
associated with the RadioButtonGroup control row. The main fields are shown in Figure 6. The Value
column specifies the value to which the CheckWeb property is set if that radio button is selected. The Order
column is for ordering the radio buttons when using the up and down arrows on the dialog. Note that you
must size and locate each radio button independently of its order.
Property
Value
Text
Order
CheckWeb
Product
Product Information.
1
CheckWeb
FAQ
Frequently Asked Questions.
2
CheckWeb
None
Do not visit the web site.
3
Figure 6.
Page 15
Developing the User Interface
At this point, I have a radio button that sets the CheckWeb property to a certain value depending on which
radio button is selected. Graphically, the dialog looks like Figure 7. Next, I want to set a property for which
web site to visit based on this property. To do this, I created additional ControlEvent rows for the Next button
in WebDialog. These rows are shown in Figure 8. Note that to set a property in an event requires enclosing
the property name in brackets as the event and putting its new value in the argument column. If you want to
set a property to NULL, use empty curly braces {} in the argument. I need to set the WebAddress property to
NULL in the next button in case the user moves on to the next dialog, then comes back and decides not to go
to any web site. If your events do not seem to occur, make sure the event ordering is correct. If a NewDialog or
EndDialog event occurs before a set property event, the property will never be set. I’ll return to this example
after I talk about sequences to make my install actually open up the proper HTML page.
Figure 7.
Event
Argument
Condition
[WebAddress]
http://www.wisesolutions.com/products/wfwifaq.asp
CheckWeb = "FAQ"
1
[WebAddress]
http://www.wisesolutions.com/products/products.asp?PRODID=12
CheckWeb = "Product"
2
[WebAddress]
{}
CheckWeb = "None"
3
NewDialog
User_Information_Dialog
1
10
Figure 8.
Page 16
Ordering
Sequence Tables
Sequence Tables
Windows Installer uses actions to perform each step of an install. Standard actions are used to install files,
set registry keys, create shortcuts, etc. These standard actions will usually be set up correctly in a template and
normally should not be changed. If you are starting from scratch, the SDK documentation mentions
suggested locations for standard actions.
Windows Installer has six sequence tables in which the actions appear. An install can run in three different
modes: INSTALL, ADVERTISE, or ADMIN. The INSTALL sequence is the typical mode for installing,
uninstalling, or repairing an install. The ADVERTISE mode only advertises the application. The ADMIN
mode is used to copy an installation to a central network server for client installation. Each of these modes
has two sequences associated with it: the user interface and the execute sequences. The user interface
sequence is run with user privileges and gathers information from the user about where to install the product
and which features to include. The execute sequence actually builds an install script that is handed off to the
installer service in Windows 2000 that performs the install. The six tables that contain the sequences for the
three install modes and the two phases are InstallUiSequence, InstallExecuteSequence, AdminUISequence,
AdminExecuteSequence, AdvtUISequence, and AdvtExecuteSequence. The templates for making an install
usually have the proper actions filled in each of these tables.
Each of these sequence tables can be extended using custom actions. Windows Installer allows a setup author
to call DLLs, EXEs, VBScript, and Jscript, or set a property or directory value in a custom action. However,
there are some notable limitations here. First, the only argument to a function in a DLL is a handle to the
installer object. The declaration is:
UINT __stdcall MyFunc(MSIHANDLE hInstall)
Remember that this is not a handle to the database itself, but to the install object. The install object can be
used to read properties, set feature/component states, and read other high level information. In order to read
raw data from the database tables, you will need a handle to the database obtained by calling
MsiGetActiveDatabase. If your DLL requires any parameters other than the installer handle, you must insert
custom actions that set properties before your DLL call and read those properties in your DLL.
A second limitation occurs when you use custom actions to call an executable file. The problem is that
Windows Installer seems to use the CreateProcess API internally and not ShellExecute. Because of this, file
associations are not checked and the only files you can call are executables.
Let’s return to the user interface example. I have the WebAddress property either set to a web address to be
visited or set to blank if the user doesn’t want to surf. I can create a custom action to call a DLL that reads
this property and performs a ShellExecute call on the property. To do this, I need a DLL file. Figure 9 has
the source to a simple DLL that just gets the handle to the database, reads the property, and calls ShellExecute
on the property value. To create our custom action, I need to insert the DLL into the binary table. I can do
this through code or by using a Windows Installer tool, such as Wise for Windows Installer or InstallShield for
Windows Installer. Then, I create a CustomAction row of type 1 (call a DLL stored in the binary table), set its
source column to the binary row key, and set the Target to the function name, OpenWebPage. Finally, I add
a DoAction ControlEvent row associated with the ExitDialog’s Finish button that calls the custom action
before EndDialog is called. Now, I have a new dialog that sets properties and eventually can launch a browser
at the end of an install.
Page 17
Sequence Tables
UINT __stdcall OpenWebPage(MSIHANDLE hInstall)
{
TCHAR szProperty[] = "WebAddress";
TCHAR szValue[256];
DWORD cchValue = sizeof(szValue)/sizeof(TCHAR);
// if I need a database pointer
MSIHANDLE hDatabase = MsiGetActiveDatabase(hInstall);
if ((MsiGetProperty(hInstall, szProperty, szValue, &cchValue)
!= ERROR_SUCCESS) {
return ERROR_GEN_FAILURE;
}
ShellExecute(NULL,"Open",szValue,NULL,NULL,SW_MAXIMIZE);
return ERROR_SUCCESS;
}
Figure 9.
Page 18
Merge Modules
Merge Modules
Merge modules are a very useful feature of the Windows Installer. A merge module is basically a redistributable
piece of an install. Merge modules can be used for large projects that have many different setup configurations
to package a set of files in a standard way for each of the installs. Another common use is for a runtime that
might be used in installs outside of your company such as Microsoft Visual Basic Runtime. The reusable part
of an install can be authored as a merge module and inserted into any .msi file. A merge module file has the
same format as a .msi file, with a few differences in table layout, and has an .msm extension.
A merge module basically represents what can be placed under a single feature in an install. Therefore, merge
modules do not have a Feature or FeatureComponents table. The module itself is assigned a GUID; keys for
every row in every table have this GUID tacked on to the end of them. This rule is a way of tracking what part
of an install came from which merge module. The GUID is also represented in the ModuleSignature table,
which has only one row specifying the language and version of the module. Also, each component in a merge
module will have a corresponding row in the ModuleComponent table.
One nice feature about merge modules is that they can be dependent on other modules and also can exclude
other modules using the ModuleDependency and ModuleExclusion tables. This allows a setup author to further
break down an install into smaller modules and still know which modules are needed when a merge module is
added to an install. The Directory table in merge modules is also set up differently. When a module is inserted
into a .msi file, the module's TARGETDIR is replaced with a specified target directory during the merge. All
directories in the module must be children of this target directory. See the SDK for more information on setting
up the directory structure of a merge module.
Once a module is created, it can be merged into an install package file. Merging is accomplished using
mergemod.dll, which is distributed as part of the SDK. This DLL has a COM interface, IMsmMerge, which
allows you to open the installer database, open the merge module, and perform the merge. The merge is
actually done by feature, so the Connect call merges the module into any additional features after the Merge
function is called. You can then Extract the files or cabs to insert them into the install database.
The operations of IMsmMerge are shown in Figure 10.
OpenDatabase
Opens database in given path.
OpenModule
Opens the module in given path and language.
Merge
Merges the module into given feature and redirect directory.
Connect
Merge the module into additional features.
ExtractCab
Extracts module cab into given file name.
ExtractFiles
Extracts files of module into given directory.
CloseModule
Closes mergemodule.
CloseDatabse
Closes install database.
Figure 10.
Page 19
Transforms
Transforms
Transforms are another useful feature of the Windows Installer. Transforms record a difference between two
versions of a .msi file. One or more transforms can be applied at runtime to dynamically modify the original
package file, using command line arguments to set the TRANSFORMS property. Transforms can be used to
translate user interface text, change which features are installed by default, or even add items to an install.
Since the .mst transform file only contains the differences between two versions of a .msi file, it is usually very
small and is better than shipping several versions of the .msi file. There is a restriction in version 1.0 of
Windows Installer that any files added or modified by a transform must be placed in the source directory of the
.msi file in order to be found during install. Third party tools do exist to create transforms, but you can also
use code to create a transform between two .msi files as follows in Figure 11:
// open the new database and the base database
if (MsiOpenDatabase(csNewDB, MSIDBOPEN_READONLY, &hDatabase) ==
ERROR_SUCCESS) {
if (MsiOpenDatabase(csBaseDB, MSIDBOPEN_READONLY, &hDatabase2) ==
ERROR_SUCCESS) {
// create the transform and set the proper summary stream information
if (MsiDatabaseGenerateTransform(hDatabase, hDatabase2, csTransform,
0, 0) == ERROR_SUCCESS) {
MsiCreateTransformSummaryInfo(hDatabase, hDatabase2, csTransform,
0, 0);
}
MsiCloseHandle(hDatabase2);
}
MsiCloseHandle(hDatabase);
}
Figure 11.
Page 20
Conclusion
Conclusion
Building your installation using the Windows Installer requires some up-front planning. Deciding which
features to have and which features are advertised will make your install easier to use for customers to use.
Knowing this in advance will also allow your application to take advantage of the application specific features of
Windows Installer, such as finding out whether a feature is available and where it’s installed.
Windows Installer has many useful features, such as automatic repair, that make it a good choice for l
owering Total Cost of Ownership (TCO) for systems administrators. Developers also benefit because these
features are built into the operating system. By using the Windows Installer, you can make your product self
repair, advertise itself, and track its install state without writing any additional code. Finally, Windows Installer
provides consistent rules for application setup, resulting in easier software management.
Obviously, manually creating an installation for Windows Installer can be a tedious, error-prone task.
Fortunately, there are products available, such as Wise for Windows Installer, that simplify this process by offering
an easy-to-use development environment to create, modify and debug installations for Windows Installer.
Page 21
5880 N. Canton Center Rd.
Phone: (734) 456-2100
Fax: (734) 456-2456
Suite 450 • Canton, MI 48187
Orders: (800) 554-8565
www.wisesolutions.com