Allowing The User To Select The Install Folder

This article is one in my series focusing on what I learned creating a Windows Installer using the WiX toolkit’s "Managed Bootstrapper Application" (Custom .NET UI) framework. Each post builds upon the previous posts, so I would suggest you at least skim through the earlier articles in the series.

Here’s an index into the articles in this series:

During an installation, it's often expected that the end user can change the installation folder from the default value. Obviously, you'll need some UI controls to allow the user to select the destination folder they want to use, but what do you do with it once you have it?

Changing the install folder location for your installation has two main tasks:

  • Overriding the folder path at install
  • Determining the override used at modify/uninstall/upgrade time

I'll walk through both in this article.

Overriding the Install Folder Path

Let's look at how the default install folder is set. In your individual MSI WiX projects, you'll have a Directory fragment defined that will look something like this:

  <Fragment>
    <Directory Id="TARGETDIR" Name="SourceDir">
      <Directory Id="ProgramFilesFolder">
        <Directory Id="MYINSTALLFOLDER" Name="Wrightfully Blog Examples">

          <!-- ... subfolders -->   
          <Directory Id="dirLicenses" Name="Licenses" />
          <Directory Id="dirdoc" Name="doc" />
        </Directory>
      </Directory>
    </Directory>
  </Fragment>

Notice that some of the Ids are in ALLCAPS. This means they are exposed as variables that can be set by the WiX Engine's Engine.StringVariables array. The override will replace that node in the structure and all child folders will branch off the new path. Overrides will not affect any Directory nodes defined above or beside it in the xml structure -- only it and it's children.

So, if I want to override the install folder defined in my Directory fragment, I'd do something like this:

var pathOverride = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles),
                           "My Alternate Location");

Engine.StringVariables["MYINSTALLFOLDER"] = pathOverride;

Then, when the actual MSIs install, they will substitute the value for MYINSTALLFOLDER to use my new value at install time. But first, you have to tell WiX to pass the MYINSTALLFOLDER along to the MSI, which you do in the <MsiPackage> declaration of your <Bundle>, like this:

  <MsiPackage Compressed="yes" SourceFile="$(var.MyMis.TargetPath)">
      <MsiProperty Name="MYINSTALLFOLDER" Value="[MYINSTALLFOLDER]"/>
  </MsiPackage>

Determining Override Used in Later Runs

Unfortunately, if you override the folder at install time, that's not persisted by the installation system, so you don't know what the value was set to during later runs -- such as when the user clicks Modify/Uninstall in Add/Remove Programs, or when the user goes to upgrade with a newer version of the installer.

So, in order to correctly re-override the value during future runs, we need to store it somewhere and then go grab that value when we run. And we do this with a registry key. You could do this directly in the Bootstrapper Application (managed code), but I prefer to use the WiX functionality to manage the registry key and ensure it's removed when the software is uninstalled, etc. So in an MSI Feature that will always be installed with my software, I put this in my product definition:

<Feature Id="feat_Documentation" Title="Documentation" Level="1" Display="hidden">
    <ComponentGroupRef Id="compgroup_Docs" />

    <Component Directory='MYINSTALLFOLDER'>
        <RegistryValue Root='HKLM' Key='SOFTWARE\\Wrightfully\\BlogInstaller'
                   Name='MyInstallFolder' Value='[MYINSTALLFOLDER]'
                   Type='string'></RegistryValue>
    </Component>
</Feature>

This will create the registry key and populate it with the value of MYINSTALLFOLDER. Then, in my bootstrapper app, I can read the values using this code:

var existingInstallLocation =
    Registry.GetValue(@"HKEY_LOCAL_MACHINE\\SOFTWARE\\Wrightfully\\BlogInstaller",
                    "MyInstallFolder", "").ToString();
if (!string.IsNullOrEmpty(existingInstallLocation))
{
     Engine.StringVariables["MYINSTALLFOLDER"] = existingInstallLocation;
}

If the registry value exists (is not an empty string), then I use that value for [MYINSTALLFOLDER]. Otherwise, I use the default value.

TIP: Since the default value for MYINSTALLFOLDER isn't known by the Bootstrapper App (it's defined in each MSI's Directory fragment), I just always set it to a default value at startup of my bootstrapper (or to the override found in the registry) -- which means the value in the Directory fragments is never actually used.

ANOTHER TIP: Make sure you do some general validation on the user-entered values to make sure it's a valid path. Otherwise, the MSI installs will blow up, and you definitely don't want to deal with that.