Introduction

This is the second part of a two part article. This article assumes you have read and understood the techniques and concepts described in Part 1.

In the previous article you saw how properly defining an UpgradeCode could be used to upgrade an existing application while preserving legacy files if needed.

We also saw in the previous article that an upgrade does not work if you change the per-machine or per-user level between setups. We will look at one of several possible solutions to this potential problem, as well as look a little deeper into how upgrades function in general.

What You'll Need:

You need to read and work through Part 1 first - this article assumes you have prior knowledge of the concepts from Part 1. You'll also need Orca to manually edit the MSI package. The Custom Action DLL required is included in the Zip.

What the Zip Contains:

The Zip file contains the two simple Hello World setups from Part 1, but configured to use a Custom Action DLL to synchronize per-user/per-machine upgrade context.

The Custom Action DLL and source is also included as a VS.NET 2002 project so it is compatible with both 2002 and 2003. The Custom Action has been tested on Win98, Win2K, and WinXP.

Note: I have never worked with WinME. The Custom Action behavior may need to be modified to work on WinME. The code currently assumes WinME behaves like Win98. If WinME has Users and Admins like WinNT, the code will need to be modified to work correctly.

More About Install Design and Upgrades

Windows Installer organizes a setup in terms of Features, Components, and Resources. Features are the view of the product presented to the user, for example, "Typical", "Custom", "Complete" options.

At the other end of the spectrum is a Resource. A Resource is for example a file to be installed. But the scope of Resources extends to registry entries, fonts, and covers basically all changes made on the target machine.

A Component is a logical grouping of Resources associated with one or more Features, and is the atomic unit which the Windows Installer recognizes when it makes decisions about whether to install or uninstall various resources. Components are organized by the setup developer to ensure the integrity of the Feature(s) installed. Component definitions represent the developer's strategy to enforce the installation "business rules" if you will. For example, if an application executable has a shortcut, that shortcut should be installed if the executable is installed. Similarly, the shortcut should be removed if the executable is removed. Proper organization of Resources into logical Components is the mechanism that enforces these behaviors.

Components are also used to enforce rules across multiple setups that are related products, i.e., setups that share an UpgradeCode. There are a number of Component rules that must be enforced to maintain application integrity over multiple version installs/upgrades.

The most basic of these is that a given file with a specific target location must have a unique identifier. This unique identifier must also be preserved across multiple setups. For example, a version 1 setup installs a file named HelloWorld.exe to the application folder. A version 2 setup installs a newer version of HelloWorld.exe to the application folder. The ComponentID for the file HelloWorld.exe must be the same in both setups for the Component rules to be properly enforced during an upgrade. If the two IDs are not the same, the Component rules are broken - a big no-no. Component rules also apply to all Resources.

You may get away with it depending on a lot of factors, but breaking Component rules means you have possibly corrupted the integrity of the application, and doing so may also give rise to a host of anomalous behaviors. Among the common symptoms of broken Component rules are applications which leave behind artifacts upon uninstalling, shortcuts which trigger the setup to run in repair mode, and possibly even application failure if registry entries are corrupted.

Simply setting the UpgradeCodes in two setups to relate them is only a small part of the picture. The UpgradeCode only allows a Windows Installer to determine that two setups are related. But in order to actually perform a proper upgrade, Component rules must be enforced across the two setups.

Let's look at how the VS.NET IDE helps us in this process.

VS.NET IDE Setup Build Behaviors

The VS.NET Setup IDE does not support the concept of multiple Features. VS.NET also applies a very simplified form of Component assignment - one Resource per Component. That means a unique Component and ComponentID is created for each file, each shortcut, each registry entry that you author into the setup using the VS.NET IDE.

And a very important function the setup IDE performs for you which is not documented anywhere is that when you set the UpgradeCode in the VS.NET IDE, the wizard that builds the actual MSI uses some whiffle-dust to enforce Component rules for you automatically across the two setups. Basically what it does is it sets the ComponentIDs in the new setup to the same ones found in the old setup for all resources common to both setups.

If you build two related setups on different machines, or presumably if you have reinstalled VS.NET between builds on the same machine, the "magic" no longer works.

I cannot cover all the possibilities in a reasonable length article, but I encourage you to study the SDK documentation on Components and Resources carefully.

And be aware that if your setups are built under conditions where the VS.NET IDE cannot enforce the Component rules for you, you will have to synchronize them manually by changing the ComponentIDs in your upgrade MSI using Orca. This is a tedious but necessary step which requires a careful, detailed comparison of the two setups.

You've been warned. You must synchronize the ComponentIDs for the two versions for all resources which have the same name and target for the upgrade to work properly under all conditions.

The rest of this article will assume that your upgrade version setup is properly configured to enforce the Component rules.

Configuring an Upgrade to Work When Per-User/Per-Machine Is Unknown

In the previous Part 1 article, you saw how properly defining an UpgradeCode could be used to upgrade an existing application while preserving legacy files if needed. We also saw in the previous article that an Upgrade does not work if you change the per-machine or per-user level between setups.

This is because per-machine installs save their information in the HKLM tree, while per-user installs save their information in the HKCU tree. The UpgradeCode is used by the Windows Installer to find a previously installed related product. If the Windows Installer cannot find the UpgradeCode because it is not looking in the right tree, i.e., because the per-user or per-machine levels of the two installs differ, then the upgrade action is essentially ignored.

When you set the ALLUSERS property explicitly to 2 in the MSI, it will default to the highest level possible. This means if the user has admin privileges, the setup will start in what amounts to "per-machine" mode. The Windows Installer runs an action named FindRelatedProducts to search for a related setup on the target machine. But this action only searches the registry tree that matches the mode the version 2 setup is running in. This means that if the new version setup is running in per-machine mode, a previous per-machine install will be found and upgraded. But a per-User install will not be found because the FindRelatedProducts action will only search the HKLM tree. You cannot configure a Windows Installer to look both places directly.

What you can do is create a Custom Action for an upgrade setup that can look both places, and then configure the upgrade so it works in all cases. So basically the Custom Action is used to override the built-in FindRelatedProducts action, and to dynamically configure the running setup to match the per-machine or per-user mode of the previously installed version.

Custom Actions

You probably already know about Installer classes, but this type of a Custom Action does not help us in this scenario because Installer based custom actions only run at the end of the install sequence.

But there are other types of custom actions that allow you more control over the execution sequence. They can be written in script, as an executable, or as a DLL embedded in the MSI. We'll use the last type, and I suggest you use the DLL format for all custom actions you create other than Installer classes. Although it is more work to create a DLL, they are much more stable and versatile than VBScript.

DLLs used as Custom Actions must include at least a couple of required MSI headers, link the msi.lib standard library, must export their function calls as standard C calls, not COM calls, either by declaration or by a definition file, and generally should be created so that they only use resources that already exist on the target machine - typically Win32 or MSI API calls.

You can debug DLLs from a running install but it is a hassle. I find it easiest to work out the basic functionality in a regular project so it can be debugged easily in the IDE, then convert the code to conform to Windows Installer requirements.

Finally, a Custom Action should never throw up a message box. This interferes with running the install in unattended or silent mode. You should process all messages through the MSI API to allow the install to both log and process the messages according to the mode it is running in.

You can find out more about DLL Custom Actions from the SDK as well as by searching other articles on Code Project.

About the Custom Action Logic

The Custom Action DLL performs the following operations to essentially override the intrinsic Windows Installer FindRelatedProducts action so that it works for all cases:

Determine system and user information. If the platform does not support per-user/per-machine we do not take any action - the setup will always work.

Retrieve the previous ProductCode from the new setup as a custom property.

Search the HKLM tree for the ProductCode to see if the previous version is installed at all.

Assuming the previous version is installed, determine if it was per-user or per-machine by searching for the UpgradeCode in the HKLM tree. Searching both trees is redundant because we first determine if the product is installed at all, and if that is true, then failure to find an UpgradeCode in the HKLM tree implies the existing install must be per-user.

Compare the previous install to the current user's permissions. If the previous install is per-machine, and the current user does not have per-machine install permissions, we cannot properly upgrade the existing application so we display a message and abort.

If the permissions are OK, we set a Windows Installer property - PREVIOUSVERSIONSINSTALLED - to the old ProductCode so the upgrade works. This property is set internally by the FindRelatedProducts action, which we are overriding so the upgrade works regardless of the previous install level.

This particular Custom Action also enforces a couple of additional behaviors. If the previous install was per-user, we will allow the user to choose per-user or per-machine. But if the previous install was per-machine, we will force the upgrade to install per-machine, and we do this by setting the FormFolder_AllUsers property to ALL, and also hiding the controls that allow the user to change the option. We could allow an admin user to change from per-machine to per-user, but we would probably want to display a warning that doing so would remove the application for all other users. That involves a lot of control event modifications that I think deviates from the basic task at hand just for the sake of allowing a behavior which is possible but that has little real merit.

So instead we'll add and set a custom property that simply hides the option.

About the Code

The code is included in the Zip, along with the compiled DLL. The project is in VS 2002 so it is compatible for all.

If you are not a C++ programmer, you can find the particulars of how to set the includes and other configuration properties to compile a Custom Action DLL discussed elsewhere, including a previous article I wrote on PID Validation.

The code is a little verbose, but I wanted to make it as simple as possible for people (like me) who don't do a lot of C++ programming to understand and modify. So I did not put strings in a Resource table, I used an extern function call rather than a definition file, and so on and so forth. The code also has minimal error handling - I allocate strings on the heap and assume that memory is available etc.

The code is well commented, and since I have already described its behavior we will not discuss it in any further detail.

Configuring the Upgrade Install to Apply the Custom Action

Develop and compile your version 2 setup, taking care to set the UpgradeCode in the IDE to match your previous version, as described in Part 1 of this article. But do not make any modifications to the MSI - we'll use a different approach here.

Open the release build of the MSI in Orca.

Embed the DLL in the MSI as a binary resource:

The DLL can be embedded directly into the MSI itself. Open the binary table and add a new row.

Name: SetUpgradeContextDLL

Data: Use the file browser that appears to browse to the compiled SetUpgradeContext.DLL file and select it. Click OK.

Define the Custom Action:

This tells the Windows Installer how to call the DLL. Open the CustomAction table, and add a new row. Set the values to:

Action: SetUpgradeContext

Type: 1

Source: SetUpgradeContextDLL

Target: The function call prototype string exactly as shown below.

_SetUpgradeContext@4

The Action value is used to identify the custom action. The Type value of 1 specifies the action is in the form of a DLL. The Source property directs the setup to use the binary resource we embedded. The Target property prototypes the syntax for the exported function call. When you are done, you should see something like this:

Sequence the Custom Action execution:

Now that we have embedded our custom action source and defined how to call it, we need to sequence the call in order to direct the Windows Installer on when and under what conditions to call the action. The logical sequence is some time after the FindRelatedProducts action, since that is the behavior we plan to override. We also want to apply a condition - the custom action should only be called during an install, not during a repair or remove action.

We also need to add the same sequence to both the InstallExecuteSequence and the InstallUISequence tables.

Open the InstallExecuteSequence, click on the sequence header to order the table in the order of action execution. In my setups I put the custom action call after FindRelatedProducts and the action that checks for a newer version. I used a Sequence value of 202. Your setup may be different.

Add a new row and set the values to:

Action: SetUpgradeContext

Condition: NOT Installed

Sequence: 202 (or as required by your setup sequence)

The result should look like this:

Repeat exactly for the InstallUISequence table:

Let's take a minute and review. So far we have added the CA resource to our upgrade setup as a binary embedded DLL. We have defined the CustomAction table entry that tells the Windows Installer how to call the DLL. We have also sequenced the actual function call in our install execution action tables. We have done everything we need to allow the Windows Installer to find and call our function.

But we need to add some custom properties to support the custom action function call.

Add the Additional Property Entries required by the Custom Action

We need a custom property that the DLL will read to determine the ProductCode of our previous install. The DLL will use this value to search the registry, and if the previous version is found, it will also set an intrinsic property, PREVIOUSVERSIONSINSTALLED, to the previous version ProductCode. This will instruct the Windows Installer to remove the existing product.

First find the ProductCode GUID for the version you are upgrading from. You can get this from the version 1 VS.NET Setup project, or you can open the version 1 MSI in Orca and find it in the property table. Copy this GUID exactly.

Open the property table in the version 2 install. Add a new row, and set the values to:

Property: OldProductCode

Value: {05BE32C8-423F-4A2A-9162-B79CAD08866D}

Use your GUID, not mine!!!!

We also set a second custom property to allow hiding the AllUsers option controls. This property will be set by the DLL to enforce a per-machine upgrade if appropriate. Otherwise the property will be left at a default value of 0 which will allow the AllUsers options to be displayed.

Again we are in the property table in the version 2 install. Add a new row and set the values to:

Property: HideAllUsers

Value: 0

When you are done, you should have something like this:

Do not add the ALLUSERS entry we used in part 1, and do not change the FormFolder_AllUsers property either. Our CA will now configure all this for us.

The CA is now configured to run properly. But we still need a change to conditionally hide the AllUsers option controls.

Apply new conditions to the AllUsers controls:

We will be working with a table we have not seen yet: ControlCondition.

As you probably already know, the Windows Installer hides the AllUsers option group in the dialog interface if you are on a Win9x system, or if the user does not have admin privileges on an NT system. This is accomplished by a control condition that is evaluated at runtime.

We can also change control conditions to implement our own behaviors.

In this case we want to evaluate the custom HideAllUsers property. If the DLL has set it to 1 we hide the option controls. This requires a simple conditional expression change.

Open the ControlCondition table in Orca. Find the entries for the dialog named FolderForm. Then find the entries for the AllUsersRadioGroup option controls. Then find the Hide action. Make sure you have the right entry! In Orca you can double click on the cell to edit an existing entry.

The original condition will be:

NOT (VersionNT>=400 AND Privileged=1)

Change this to read:

(NOT (VersionNT>=400 AND Privileged=1) OR HideAllUsers=1)

When you are done it should look like this:

Now, if we hide the option group, we need to hide the label associated with it.

Find the entries for AllUsersText. Find the Hide action. Change the condition to the same as the option group.

When you are done it should look like this:

Now our CA is fully functional and also the Windows Installer applies the appropriate control conditions.

Set the RemoveExistingProducts sequence:

OK, at this point our Custom Action implementation is complete. But all the CA does is it configures the upgrade version 2 setup to set the per-user/per-machine context, and it also finds a previous version install even if the initial contexts were different. But the CA does not actually cause the existing version to be removed.

This still happens when the RemoveExistingProducts action runs. So as in Part 1, again you need to decide what is appropriate for your situation - either uninstall first-install-last or install first-uninstall last - and configure the sequence value for the RemoveExistingProducts action. This was described in detail in Part 1.

Save and Test

Now we are done! Save the modified MSI, close it from Orca, and test carefully!

History

None, so far ....

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

Comments and Discussions

I also have the problem of not knowing which previous version is already installed and thus may need the ProductCode for all previous versions. But - could we not find the previous version using the UpgradeCode instead? After all, isn't that what Windows Installer does itself when searching for a previous version? The upgrade code must be the same for all versions in order for Windows Installer to be able to upgrade to a newer version.

In reply to my own question: I have actually found a way to find the ProductCode(s) from an UpgradeCode. These are found as the name of a subkey under the registry key HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UpgradeCodes

In this key there is a value with no data and a name which is the ProductCode for the installed application with this UpgradeCode. If there are several applications installed with this upgrade code, they are all listed here. If you do a version 1 install for "Everyone" and a version 2 install that is supposed to first uninstall version 1 but you have it set for "Just me", there will be two values (ProductCodes) in the key for the UpgradeCode for this application.

However, the UpgradeCode and the ProductCodes are not just stored as a standard GUID. Instead, the numbers in the first 3 blocks are reversed and the the pairs of numbers in the last two blocks are reversed. For example, if your UpgradeCode is:

{01234567-89AB-CDEF-0123-456789ABCDEF}

You should be looking for a key with the name:

76543210BA98FEDC1032547698BADCFE

The product codes found in this key (as the name of the values) have to be changed back to a normal GUID as above.

Now I just have to find out where my already installed apps are located since I need to preserve some configuration data set by the user before I install the new version.

Your article gives a wonderful solution ,but I got a small problem:
All msi files are built by automatic build process (TFS-build), so I can't manually edit with Orca, but with VS setup project editor.Also I got some custom actions, implemented within >net assembly, but those actions run after the user asked all the questions and made all his choises. Is there any chance I can add your custom action to mine/include it not with Orca?

hi
this article has helped alot. i have developed an application which has many version such as v1.0, v2.0.. and so on. All my clients are using different version like--one client is using v1.0 and other is v2.0. Now i want all of them to upgrade to v3, i.e client using v 1.0 and v2.0 both need to upgrade to v3, with the same setup. how do i acheive this..? In property table for oldproductcode i am able to give only one entry.. kindly help..

If a machine only has one version installed on it, but you do not know which one, you can write a custom action DLL that runs at the beginning of the setup, searches the registry, determines which version if any is installed, and sets the installer properties at runtime to uninstall the version it finds.

This is not trivial, but it is do-able.

If however, there is a possibility that users have installed say Version 1 and then installed Version 2 on top of the previous version, you have a problem. Unless thiings have changed, and again, I am not up to date on this technology now, the installer engine only allows you to uninstall one existing version.

If this is your situation, what I would probably do is write a custom bootstrapper application that runs outside of any Windows Installer package and executes msiexec.exe as command line operations to uninstall all existing versions.

This process doesn't work...there are still two versions of Hello World in the list in Add/Remove Programs. Is there any way to uninstall the previous version so that the entry from that list disappears?

We have created setup using .NET installer. We always update product code of installer for each version. On XP when we try to install new application then the previous application gets automatically deleted. But same setup when run on Vista it doesn't delete previous version from machine. And it shows two installed application entry with the same name in Installed application list. How can we woraround this on Vista. Any pointers will be appreciated. Thanks,Jaydeep

Actually whenever i m upgrading my installer version, I am creating one folder where I am putting old version files.This is done through the overriding the Installer class.I want to delete this folder in the end of installation(when all the custom actions are ended).If I am upgrading my Installer, then the installer will call unInstall() for old version files and then it will call Install() for new version files.

unInstall() will be called in two scenerios:-
1. When straight way I am uninstalling the installer.
2. When I am upgrading the installer.

I need information in my unInstall() that is it UNINSTALLATION or UPGRADTION of installer.

For this I was using customActionData which was having property value of control one of the userInterface Editor Dialog Box(CheckBox DialogBox).

I am getting right value of property in Install() but not in unInstall().

First of all vongratulation for your excellents articles. I have a problem, supose I have distributed 2 versions 1 and 2 and now i want to distributed the third version, so I have to set the oldroductcode to the MSI version 3 as the 2nd version productcode, but if some body is still using the firts version this won't be uninstalled.

If the config file has been modified on the machine after it was originally installed and you use an "Install First Uninstall Last" strategy, the config file should not be replaced by the newer version in the Version 2 setup.