How to Manually Create an Installation for the Windows Installer Service Gary Chirhart
Transcription
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