The root issue is that the Windows Installer does not save Property values for you. That means if the user enters values in the install UI or passes them on the command-line, those values will be not be present during repair, upgrade nor uninstall. That last one, uninstall, catches people all the time. So let's solve the problem simply then solve it completely.
The Simple Solution
As is often the case, the simple solution is not the correct solution but it definitely shows us the way. In this case, what we’re going to do is squirrel away the user provided Property values in registry keys and use a RegistrySearch to read them back out for repair, upgrade and uninstall. The code for this simple solution is pretty straight forward.
1: <?xml version='1.0'?>
2: <Wix xmlns='http://schemas.microsoft.com/wix/2006/wi'>
3: <Product Id='*' Name='Demo Remember Property' Language='1033'
4: Version='0.1.0.0' Manufacturer='RobMensching.com LLC'
5: UpgradeCode='c2e7dfa4-3faf-4c41-9bcc-eabf7657a2b9'>
6: <Package InstallScope='perUser' InstallerVersion='200' />
7:
8: <MajorUpgrade DowngradeErrorMessage="A newer version installed" />
9:
10: <Property Id='REMEMBERME'>
11: <RegistrySearch Id='RememberProperty' Root='HKCU'
12: Key='SOFTWARE\Wix\DemoRememberProperty'
13: Name='Remembered' Type='raw' />
14: </Property>
15:
16: <Feature Id='MainFeature' Level='1'>
17: <Component Directory='ApplicationFolder'>
18: <RegistryValue Root='HKCU'
19: Key='SOFTWARE\Wix\DemoRememberProperty'
20: Name='Remembered' Value='[REMEMBERME]'
21: Type='string' />
22: <RemoveFolder Id='CleanupApplicationFolder' On='uninstall' />
23: </Component>
24: </Feature>
25: </Product>
26:
27: <Fragment>
28: <Media Id='1' />
29: <Directory Id='TARGETDIR' Name='SourceDir'>
30: <Directory Id="LocalAppDataFolder" Name="AppData">
31: <Directory Id='ApplicationFolder' Name='Remember Property' />
32: </Directory>
33: </Directory>
34: </Fragment>
35: </Wix>
That’s a fully working installation, so I’ll comment on the interesting parts of the code:
- line 6: Note that I used a per-user install for this example because it saved me UAC prompts during testing. If your install is per-machine use HKLM instead of HKCU for the various registry roots we’ll discuss next.
- line 8: I’m using the new WiX v3.5 language simplifications with the MajorUpgrade element and the absent Component/@Id,@Guid attributes. If you’re using WiX v3.0 you’ll have to type more stuff.
- line 10: This is the Property that we’ll remember. In this case, I have no default value but you could add one if your scenario required it. You can have as many remembered Properties as you want, you just add more registry keys.
- line 11: This is the registry search that will reload our Property on repair, upgrade and uninstall scenarios. On the initial install the registry key will be missing so the Property will use its default value (which in my case is nothing).
- line 18: This is the registry key that remembers our Property. Put the registry key in a place that is logical for you. Again, remember to make your Root=‘HKLM’ when doing a per-machine install.
- line 22: I added the RemoveFolder element to make ICE64 happy.
- line 27: Notice that I pulled the directory tree into a separate Fragment. I did this just to keep the rest of the code a bit more tidy. I also added the Media element to make ICE71 happy even though there are no files to install.
Cool. Now let’s walk through our install, repair, uninstall scenarios and see the simple solution work. I’ll show you the command I executed then the important pieces of the log files that demonstrate how it works. First install and it’s log file:
msiexec /l*v i.txt /i demo.msi REMEMBERME=1
MSI (c) (A0:3C) [21:31:02:897]: Command Line: REMEMBERME=1 CURRENTDIRECTORY=...
MSI (c) (A0:3C) [21:31:02:898]: PROPERTY CHANGE: Adding REMEMBERME property. Its value is '1'.
MSI (c) (A0:3C) [21:31:02:918]: Switching to server: REMEMBERME="1" TARGETDIR=...
MSI (s) (90:F0) [21:31:02:945]: Command Line: REMEMBERME=1 TARGETDIR=...
Property(S): REMEMBERME = 1
Property(C): REMEMBERME = 1
Now, repair and it’s log file (Note: that I only had to pass REINSTALLMODE=“u” because we only need to repair our per-user registry keys) :
msiexec /l*v r.txt /i demo.msi REINSTALL=ALL REINSTALLMODE=u
Action start 21:35:52: AppSearch.
AppSearch: Property: REMEMBERME, Signature: RememberProperty
MSI (c) (C0:A0) [21:35:52:722]: PROPERTY CHANGE: Adding REMEMBERME property. Its value is '1'.
MSI (c) (C0:A0) [21:35:52:727]: Switching to server: REMEMBERME="1" TARGETDIR=...
MSI (s) (90:90) [21:35:52:742]: Command Line: REMEMBERME=1 TARGETDIR=...
Property(S): REMEMBERME = 1
Property(C): REMEMBERME = 1
Notice how the REMEMBERME Property was set to 1 after the AppSearch. That’s where the RegistrySearch element was executed and set the Property. Uninstall works exactly the same so I’ll skip that and move on to where our simple solutoin breaks down. What happens if you want to change the value during a repair? It goes something like this:
msiexec /l*v r.txt /i demo.msi REINSTALL=ALL REINSTALLMODE=u REMEMBERME=2
Action start 22:57:23: AppSearch.
AppSearch: Property: REMEMBERME, Signature: RememberProperty
MSI (c) (E4:A0) [22:57:23:378]: PROPERTY CHANGE: Modifying REMEMBERME property. Its current value is '2'. Its new value: '1'.
MSI (c) (E4:A0) [22:57:23:383]: Switching to server: REMEMBERME="1" TARGETDIR=...
MSI (s) (88:A8) [22:57:23:398]: Command Line: REMEMBERME=1 TARGETDIR=...
Property(S): REMEMBERME = 1
Property(C): REMEMBERME = 1
Do you see how the AppSearch that solved our problem creates a new one? It overwrites our attempt to set REMEMBERME to “2” with the remembered value “1”. That brings us to our more complex but complete solution.
The Complete Solution
The complete solution builds on the simple solution. All we need to do is add a couple custom actions to save the Property value if it set from the command line. I had hoped to use the SetProperty element introduced in WiX v3.0 to make the solution pretty clean but that does not support the “firstSequence” bit for custom actions. So I’ve had to write out the custom actions completely.
1: <Fragment>
2: <CustomAction Id='SaveCmdLineValue' Property='CMDLINE_REMEMBERME'
3: Value='[REMEMBERME]' Execute='firstSequence' />
4: <CustomAction Id='SetFromCmdLineValue' Property='REMEMBERME'
5: Value='[CMDLINE_REMEMBERME]'
6: Execute='firstSequence' />
6:
7: <InstallUISequence>
8: <Custom Action='SaveCmdLineValue' Before='AppSearch' />
9: <Custom Action='SetFromCmdLineValue' After='AppSearch'>
10: CMDLINE_REMEMBERME
11: </Custom>
12: </InstallUISequence>
13: <InstallExecuteSequence>
14: <Custom Action='SaveCmdLineValue' Before='AppSearch' />
15: <Custom Action='SetFromCmdLineValue' After='AppSearch'>
16: CMDLINE_REMEMBERME
17: </Custom>
18: </InstallExecuteSequence>
19: </Fragment>
There is nothing special here. We squirrel away the REMEMBERME value from the command line in the CMDLINE_REMEMBERME Property. Later we set the REMEMBERME Property to the CMDLINE_REMEMBERME value if one was provided. Since I put this in a Fragment the only thing remaining is to add a reference from the Product element. I added the following just above the close Product element.
25: <CustomActionRef Id='SaveCmdLineValue' />
Try it out yourself and follow the changes to the REMEMBERME Property in a verbose log file. In the future (WiX v4.0, perhaps), we might add something to the WiX language to make this much easier to write. In the meantime, tuck this solution in a Fragment for when you need it later.