Real-World Example: WiX/MSI Application Installer

Creating an installer that does not suck is hard. The tools available are expensive, inadequate, overly complicated and/or poorly documented (pick any combination). WiX is one of the better choices. It is free and it is used for some Microsoft products. But WiX is just a wrapper around MSI and as such is unnecessarily difficult to use. In an effort to make life better for fellow developers I am publishing the full source code of uberAgent’s installer here.

uberAgent setup

What it Does

The WiX installer presented here can be compiled into two separate MSI packages: one for 32-bit and the other for 64-bit. The unfortunate fact that both are required is due to Windows Installer’s inability to write to the 64-bit Program Files directory from a 32-bit MSI package.

Features:

  • Puts an executable and a config file in %ProgramFiles%
  • Installs and starts a Windows system service
  • Custom dialog asks for a server name and adds that to the config file
  • Install location and custom server name are persisted across upgrades
  • Install location and server name can be specified on the msiexec command line
  • Displays a license agreement in RTF format
  • Easily localizable (including the license agreement), all relevant strings in per-language WXL files
  • Uses the upgrade logic introduced with WiX 3.5
  • Prevents multiple installations of the same product (same or different versions)
  • Checks for a minimum required OS version
  • Checks to prevent installing the 32-bit version on a 64-bit OS and vice versa

The source code is heavily commented. Please study if carefully. It consists of the following files:

  • Product.wxs: Main installer file
  • WixUI_HK.wxs: Custom UI sequence definition, required since we are adding dialogs
  • Product_en-us.wxl: Localization, English strings
  • LicenseAgreementDlg_HK.wxs: Custom dialog displaying an RTF license agreement
  • ServerDlg.wxs: Custom dialog asking for server name(s)

Directory Structure

The following files and directories are referenced by the installer:

  • images\app.ico: icon used in Programs and Features
  • images\BannerTop.bmp: Top banner for the installer UI
  • images\Dialog.bmp: Full background image for installer UI
  • Eula-en.rtf: English EULA

Compiling

In order to build this installer you need to install WiX. I am using it with and compiling from Visual Studio 2013. I have tested it with WiX v3.8.

The MajorUpgrade option AllowSameVersionUpgrades=”yes” unfortunately produces the validation error ICE61 which you need to suppress in the project settings.

Source Code

Product.wxs

<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
 
   <!--
   Unattended setup: The following variables can be set:
   -  INSTALLDIR: Full path to the installation directory
   -  SERVERS: Splunk servers to send data to
   -->
 
   <!--
      ====================================================================================
      Defines & Variables
   -->
 
   <!-- Full version number to display -->
   <?define VersionNumber="!(bind.FileVersion.uberAgentExe)" ?>
 
   <!--
   Upgrade code HAS to be the same for all updates.
   Once you've chosen it don't change it.
   -->
   <?define UpgradeCode="YOUR-GUID-HERE" ?>
 
   <!-- The URL for add/remove programs -->
   <?define InfoURL="https://helgeklein.com/" ?>
 
   <!-- 32-bit / 64-bit variables -->
   <?if $(var.Platform) = x64 ?>
      <?define Win64 = "yes" ?>
      <?define PlatformProgramFilesFolder = "ProgramFiles64Folder" ?>
      <?define uberAgentExeSourcePath = "$(var.ProjectDir)..\uberAgent\x64\Release\uberAgent.exe" ?>
   <?else ?>
      <?define Win64 = "no" ?>
      <?define PlatformProgramFilesFolder = "ProgramFilesFolder" ?>
      <?define uberAgentExeSourcePath = "$(var.ProjectDir)..\uberAgent\Win32\Release\uberAgent.exe" ?>
   <?endif ?>
 
   <!--
      ====================================================================================
      Package start
   -->
 
   <!-- The upgrade code must never change as long as the product lives! -->
   <!-- Product IDs must be autogenerated (*) or else major upgrades will not work -->
   <Product Id="*" Name="!(loc.ApplicationName)" Language="!(loc.Language)" Version="$(var.VersionNumber)" Manufacturer="!(loc.ManufacturerFullName)" UpgradeCode="$(var.UpgradeCode)" >
 
      <!-- Package IDs are valid for a single package version only - they are autogenerated by WiX -->
      <!-- Let's require Windows Installer 4.0 (included in Vista) -->
      <!-- And ALWAYS install per machine!!! -->
      <Package Id="*" InstallerVersion="400" Compressed="yes" InstallScope="perMachine"  Description="!(loc.ProductDescription)" Comments="!(loc.Comments) $(var.VersionNumber)" />
 
      <!-- License agreement text: dummy. Real text is set in WXS file -->
      <WixVariable Id="WixUILicenseRtf" Value="dummy" />
 
      <!-- UI customization -->
      <WixVariable Id="WixUIBannerBmp" Value="images\BannerTop.bmp" />
      <WixVariable Id="WixUIDialogBmp" Value="images\Dialog.bmp" />
 
      <!-- Define icons (ID should not be longer than 18 chars and must end with ".exe") -->
      <Icon Id="Icon.exe" SourceFile="images\app.ico" />
 
      <!-- Set properties for add/remove programs -->
      <Property Id="ARPPRODUCTICON" Value="Icon.exe" />
      <Property Id="ARPHELPLINK" Value="$(var.InfoURL)" />
      <Property Id="ARPNOREPAIR" Value="yes" Secure="yes" />      <!-- Remove repair -->
      <Property Id="ARPNOMODIFY" Value="yes" Secure="yes" />      <!-- Remove modify -->
 
      <!-- Upgrade logic -->
      <!-- AllowSameVersionUpgrades -> Always upgrade, never allow two versions to be installed next to each other -->
      <!-- AllowSameVersionUpgrades causes ICE61 which must be ignored -->
      <MajorUpgrade DowngradeErrorMessage="!(loc.NewerInstalled)" AllowSameVersionUpgrades="yes" />
 
      <!-- This is the main installer sequence run when the product is actually installed -->
      <InstallExecuteSequence>
 
         <!-- Determine the install location after the install path has been validated by the installer -->
         <Custom Action="SetARPINSTALLLOCATION" After="InstallValidate"></Custom>
 
      </InstallExecuteSequence>
 
      <!-- Set up ARPINSTALLLOCATION property (http://blogs.technet.com/b/alexshev/archive/2008/02/09/from-msi-to-wix-part-2.aspx) -->
      <CustomAction Id="SetARPINSTALLLOCATION" Property="ARPINSTALLLOCATION" Value="[INSTALLDIR]" />
 
      <!-- 
         Launch conditions
 
         1. Check minimum OS version 
            If not, the installation is aborted.
            By doing the (Installed OR ...) property means that this condition will only be evaluated if the app is being installed and not on uninstall or changing
 
            Note: Under a Product element, a condition becomes a LaunchCondition entry. 
      -->
      <Condition Message="!(loc.OS2Old)">
         <![CDATA[Installed OR (VersionNT >= 600)]]>
      </Condition>
 
      <!-- 
         2. Check OS bitness
            Unfortunately 32-bit MSI packages cannot write to 64-bit ProgramFiles directory. That is the only reason we need separate MSIs for 32-bit and 64-bit.
      -->
      <?if $(var.Platform) = x64 ?>
         <Condition Message="!(loc.x86VersionRequired)">
            <![CDATA[VersionNT64]]>
         </Condition>
      <?endif?>
      <?if $(var.Platform) = x86 ?>
         <Condition Message="!(loc.x64VersionRequired)">
            <![CDATA[NOT VersionNT64]]>
         </Condition>
      <?endif?>
      <!-- 
         Launch conditions end
      -->
 
      <!-- Save the command line value INSTALLDIR and restore it later in the sequence or it will be overwritten by the value saved to the registry during an upgrade -->
      <!-- http://robmensching.com/blog/posts/2010/5/2/the-wix-toolsets-remember-property-pattern/ -->
      <CustomAction Id='SaveCmdLineValueINSTALLDIR' Property='CMDLINE_INSTALLDIR' Value='[INSTALLDIR]' Execute='firstSequence' />
      <CustomAction Id='SetFromCmdLineValueINSTALLDIR' Property='INSTALLDIR' Value='[CMDLINE_INSTALLDIR]' Execute='firstSequence' />
      <InstallUISequence>
         <Custom Action='SaveCmdLineValueINSTALLDIR' Before='AppSearch' />
         <Custom Action='SetFromCmdLineValueINSTALLDIR' After='AppSearch'>
            CMDLINE_INSTALLDIR
         </Custom>
      </InstallUISequence>
      <InstallExecuteSequence>
         <Custom Action='SaveCmdLineValueINSTALLDIR' Before='AppSearch' />
         <Custom Action='SetFromCmdLineValueINSTALLDIR' After='AppSearch'>
            CMDLINE_INSTALLDIR
         </Custom>
      </InstallExecuteSequence>
 
      <!-- Save the command line value SERVERS and restore it later in the sequence or it will be overwritten by the value saved to the registry during an upgrade -->
      <!-- http://robmensching.com/blog/posts/2010/5/2/the-wix-toolsets-remember-property-pattern/ -->
      <CustomAction Id='SaveCmdLineValueSERVERS' Property='CMDLINE_SERVERS' Value='[SERVERS]' Execute='firstSequence' />
      <CustomAction Id='SetFromCmdLineValueSERVERS' Property='SERVERS' Value='[CMDLINE_SERVERS]' Execute='firstSequence' />
      <InstallUISequence>
         <Custom Action='SaveCmdLineValueSERVERS' Before='AppSearch' />
         <Custom Action='SetFromCmdLineValueSERVERS' After='AppSearch'>
            CMDLINE_SERVERS
         </Custom>
      </InstallUISequence>
      <InstallExecuteSequence>
         <Custom Action='SaveCmdLineValueSERVERS' Before='AppSearch' />
         <Custom Action='SetFromCmdLineValueSERVERS' After='AppSearch'>
            CMDLINE_SERVERS
         </Custom>
      </InstallExecuteSequence>
 
      <!-- Determine the directory of a previous installation (if one exists). If not INSTALLDIR stays empty -->
      <Property Id="INSTALLDIR">
         <RegistrySearch Id="DetermineInstallLocation" Type="raw" Root="HKLM" Key="Software\!(loc.ManufacturerName)\InstalledProducts\!(loc.ApplicationName)" Name="InstallLocation" />
      </Property>
      <!-- Determine the servers of a previous installation -->
      <Property Id="SERVERS">
         <RegistrySearch Id="DetermineServers" Type="raw" Root="HKLM" Key="Software\!(loc.ManufacturerName)\InstalledProducts\!(loc.ApplicationName)" Name="Servers" />
      </Property>
      <!-- Set default value if registry search comes up empty -->
      <SetProperty Before='InstallInitialize' Sequence='execute' Id='SERVERS' Value='localhost:19500'>NOT SERVERS</SetProperty>
 
      <!--
         ====================================================================================
         Start to build directory structure
      -->
 
      <!-- We do not have more than one medium (Floppy, CD, ...). Everything in one file. -->
      <Media Id="1" Cabinet="media1.cab" EmbedCab="yes" />
 
      <!-- Outermost folder (kind of virtual). Fixed entry. -->
      <Directory Id="TARGETDIR" Name="SourceDir">
 
         <!-- We start building our directory structure here -->
         <!-- "ProgramFilesFolder" is a variable containing the absolute path. -->
         <!-- For a list of folder variables, see: http://msdn.microsoft.com/en-us/library/aa372057%28VS.85%29.aspx -->
         <Directory Id="$(var.PlatformProgramFilesFolder)">
 
            <!-- All folders from here on are relative to their parent. -->
 
            <Directory Id="ProgramFilesHK" Name="!(loc.ManufacturerName)">
 
               <!-- INSTALLDIR is a property name. We need it later for the UI (to be able to change the install dir. -->
               <Directory Id="INSTALLDIR" Name="!(loc.ApplicationName)">
 
                  <!-- Define components, the building blocks of MSIs. -->
                  <!-- Rule: A component should only contain items that belong together so strongly that they always need to be installed or removed together. -->
                  <!-- If this means a single file, then your components will contain a single file each. This is not only normal but exactly what you're -->
                  <!-- to do. Don't be afraid, Windows Installer can efficiently handle thousands of components or more, if needed. -->
 
                  <!-- Installation directory as a component so it can be emptied during uninstall (by default files added by someone other than Windows Installer are not removed) -->
                  <Component Id="INSTALLDIR_comp" Guid="YOUR-GUID-HERE">
                     <CreateFolder />
                     <RemoveFile Id="RemoveFilesFromAppDirectory" Name="*.*" On="uninstall" />
                  </Component>
 
                  <!-- Main program file -->
                  <Component Id="uberAgent.exe_comp" Guid="*" Win64="$(var.Win64)">
 
                     <File Source="$(var.uberAgentExeSourcePath)" Id="uberAgentExe" KeyPath="yes" />
 
                     <ServiceInstall Id="ServiceInstaller" Account="LocalSystem" Description="!(loc.ServiceDescription)" DisplayName="!(loc.ServiceDisplayName)" ErrorControl="normal"
                                     LoadOrderGroup="NetworkProvider" Name="uberAgentSvc" Start="auto" Type="ownProcess" Vital="yes" />
                     <ServiceControl Id="ServiceControl" Name="uberAgentSvc" Start="install" Stop="both" Remove="uninstall" />
 
                  </Component>
 
                  <!-- Configuration file -->
                  <Component Id="uberAgent.conf_comp" Guid="YOUR-GUID-HERE">
 
                     <File Source="$(var.ProjectDir)..\uberAgent\Config file\Standalone mode\uberAgent.conf" Id="uberAgentConf" KeyPath="yes" />
 
                     <IniFile Id="ConfigFile" Action="addLine" Directory="INSTALLDIR" Name="uberAgent.conf" Section="Receiver" Key="Servers" Value="[SERVERS]" />
 
                  </Component>
 
               </Directory>
            </Directory>
         </Directory>
 
         <!-- Registry entries -->
         <Component Id="RegValInstallLocation_comp" Guid="YOUR-GUID-HERE">
            <!-- Do NOT use the application's default registry key here, because THIS key will be removed on uninstall
                 (important when installing a newer version, because that is uninstall followed by install) -->
            <RegistryKey Root="HKLM" Key="Software\!(loc.ManufacturerName)\InstalledProducts\!(loc.ApplicationName)">
               <RegistryValue Name="InstallLocation" Value="[INSTALLDIR]" Type="string" KeyPath="yes" />
               <RegistryValue Name="Servers" Value="[SERVERS]" Type="string" />
            </RegistryKey>
         </Component>
 
      </Directory>
 
      <!--
         End of directory structure
         ====================================================================================
      -->
 
      <!-- Features define which parts of the application can be installed in a custom installation -->
      <Feature Id="Complete" Title="!(loc.ApplicationName)" Description="!(loc.FeatureCompleteDescription)" Display="expand" Level="1" ConfigurableDirectory="INSTALLDIR">
 
         <!-- A feature block for the main (GUI) program and all its dependencies -->
         <Feature Id="MainProgram" Title="!(loc.FeatureMainProgramTitle)" Description="!(loc.FeatureMainProgramDescription)" Level="1">
            <ComponentRef Id="INSTALLDIR_comp" />
            <ComponentRef Id="uberAgent.exe_comp" />
            <ComponentRef Id="uberAgent.conf_comp" />
 
            <!-- Registry entries -->
            <ComponentRef Id="RegValInstallLocation_comp" />
         </Feature>
 
      </Feature>
 
      <UI>
         <!-- Define the installer UI -->
         <UIRef Id="WixUI_HK" />
      </UI>
 
      <Property Id="WIXUI_INSTALLDIR" Value="INSTALLDIR" />
      <Property Id="WIXUI_SERVERS" Value="SERVERS" />
 
   </Product>
 
</Wix>

LicenseAgreementDlg_HK.wxs

<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
   <Fragment>
      <UI>
         <Dialog Id="LicenseAgreementDlg_HK" Width="370" Height="270" Title="!(loc.LicenseAgreementDlg_Title)">
            <Control Id="LicenseAcceptedCheckBox" Type="CheckBox" X="20" Y="207" Width="330" Height="18" CheckBoxValue="1" Property="LicenseAccepted" Text="!(loc.LicenseAgreementDlgLicenseAcceptedCheckBox)" />
            <Control Id="Back" Type="PushButton" X="180" Y="243" Width="56" Height="17" Text="!(loc.WixUIBack)" />
            <Control Id="Next" Type="PushButton" X="236" Y="243" Width="56" Height="17" Default="yes" Text="!(loc.WixUINext)">
               <Publish Event="SpawnWaitDialog" Value="WaitForCostingDlg">CostingComplete = 1</Publish>
               <Condition Action="disable"><![CDATA[LicenseAccepted <> "1"]]></Condition>
               <Condition Action="enable">LicenseAccepted = "1"</Condition>
            </Control>
            <Control Id="Cancel" Type="PushButton" X="304" Y="243" Width="56" Height="17" Cancel="yes" Text="!(loc.WixUICancel)">
               <Publish Event="SpawnDialog" Value="CancelDlg">1</Publish>
            </Control>
            <Control Id="BannerBitmap" Type="Bitmap" X="0" Y="0" Width="370" Height="44" TabSkip="no" Text="!(loc.LicenseAgreementDlgBannerBitmap)" />
            <Control Id="LicenseText" Type="ScrollableText" X="20" Y="60" Width="330" Height="140" Sunken="yes" TabSkip="no">
            <!-- This is the original line -->
            <!--<Text SourceFile="!(wix.WixUILicenseRtf=$(var.LicenseRtf))" />-->
            <!-- To enable EULA localization we change it to this: -->
            <Text SourceFile="$(var.ProjectDir)\!(loc.LicenseRtf)" />
            <!-- In each of the localization files (wxl) put a line like this:
            <String Id="LicenseRtf" Overridable="yes">EULA_en-us.rtf</String>-->
            </Control>
            <Control Id="Print" Type="PushButton" X="112" Y="243" Width="56" Height="17" Text="!(loc.WixUIPrint)">
               <Publish Event="DoAction" Value="WixUIPrintEula">1</Publish>
            </Control>
            <Control Id="BannerLine" Type="Line" X="0" Y="44" Width="370" Height="0" />
            <Control Id="BottomLine" Type="Line" X="0" Y="234" Width="370" Height="0" />
            <Control Id="Description" Type="Text" X="25" Y="23" Width="340" Height="15" Transparent="yes" NoPrefix="yes" Text="!(loc.LicenseAgreementDlgDescription)" />
            <Control Id="Title" Type="Text" X="15" Y="6" Width="200" Height="15" Transparent="yes" NoPrefix="yes" Text="!(loc.LicenseAgreementDlgTitle)" />
         </Dialog>
      </UI>
   </Fragment>
</Wix>

Product_en-us.wxl

<?xml version="1.0" encoding="utf-8"?>
<WixLocalization Culture="en-us" Codepage="1252" xmlns="http://schemas.microsoft.com/wix/2006/localization">
   <String Id="Language">1033</String>         <!-- Supported language and codepage codes can be found here: http://www.tramontana.co.hu/wix/lesson2.php#2.4 -->
 
   <String Id="ApplicationName">uberAgent</String>
   <String Id="ManufacturerName">Helge Klein</String>
   <String Id="ManufacturerFullName">Helge Klein GmbH</String>
   <String Id="ProductDescription">Monitoring agent</String>
   <String Id="Comments">Installs uberAgent</String>
 
   <String Id="LicenseRtf" Overridable="yes">Eula-en.rtf</String>
 
   <String Id="OS2Old">This product requires at least Windows Vista / Server 2008.</String>
   <String Id="NewerInstalled">The same or a newer version of this product is already installed.</String>
   <String Id="x64VersionRequired">You need to install the 64-bit version of this product on 64-bit Windows.</String>
   <String Id="x86VersionRequired">You need to install the 32-bit version of this product on 32-bit Windows.</String>
 
   <String Id="FeatureCompleteDescription">The complete package.</String>
   <String Id="FeatureMainProgramDescription">The main version including all dependencies.</String>
   <String Id="FeatureMainProgramTitle">Main program</String>
 
   <String Id="ServiceDisplayName">uberAgent</String>
   <String Id="ServiceDescription">Monitors the system's performance, applications and user experience.</String>
 
   <String Id="ServerDlgTitle">{\WixUI_Font_Title}Receiver Configuration</String>
   <String Id="ServerDlgDescription">Target servers for uberAgent's data</String>
   <String Id="ServerDlgServerLabel">Specify to which Splunk Indexers or Forwarders uberAgent should send its data.&#13;&#10;If multiple targets are specified, uberAgent load-balances between them.&#13;&#10;&#13;&#10;Format:&#13;&#10;    server1:port1, server2:port2, ...&#13;&#10;Default:&#13;&#10;    localhost:19500&#13;&#10;    (requires locally installed Universal Forwarder)</String>
</WixLocalization>

ServerDlg.wxs

<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
   <Fragment>
      <UI>
         <Dialog Id="ServerDlg" Width="370" Height="270" Title="!(loc.InstallDirDlg_Title)">
            <Control Id="Next" Type="PushButton" X="236" Y="243" Width="56" Height="17" Default="yes" Text="!(loc.WixUINext)" />
            <Control Id="Back" Type="PushButton" X="180" Y="243" Width="56" Height="17" Text="!(loc.WixUIBack)" />
            <Control Id="Cancel" Type="PushButton" X="304" Y="243" Width="56" Height="17" Cancel="yes" Text="!(loc.WixUICancel)">
               <Publish Event="SpawnDialog" Value="CancelDlg">1</Publish>
            </Control>
 
            <Control Id="Description" Type="Text" X="25" Y="23" Width="280" Height="15" Transparent="yes" NoPrefix="yes" Text="!(loc.ServerDlgDescription)" />
            <Control Id="Title" Type="Text" X="15" Y="6" Width="200" Height="15" Transparent="yes" NoPrefix="yes" Text="!(loc.ServerDlgTitle)" />
            <Control Id="BannerBitmap" Type="Bitmap" X="0" Y="0" Width="370" Height="44" TabSkip="no" Text="!(loc.InstallDirDlgBannerBitmap)" />
            <Control Id="BannerLine" Type="Line" X="0" Y="44" Width="370" Height="0" />
            <Control Id="BottomLine" Type="Line" X="0" Y="234" Width="370" Height="0" />
 
            <Control Id="ServerLabel" Type="Text" X="20" Y="60" Width="290" Height="90" NoPrefix="yes" Text="!(loc.ServerDlgServerLabel)" />
            <Control Id="Server" Type="Edit" X="20" Y="150" Width="320" Height="18" Property="WIXUI_SERVERS" Indirect="yes" />
         </Dialog>
      </UI>
   </Fragment>
</Wix>

WixUI_HK.wxs

<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
   <Fragment>
 
      <UI Id="WixUI_HK">
         <TextStyle Id="WixUI_Font_Normal" FaceName="Tahoma" Size="8" />
         <TextStyle Id="WixUI_Font_Bigger" FaceName="Tahoma" Size="12" />
         <TextStyle Id="WixUI_Font_Title" FaceName="Tahoma" Size="9" Bold="yes" />
 
         <Property Id="DefaultUIFont" Value="WixUI_Font_Normal" />
         <Property Id="WixUI_Mode" Value="InstallDir" />
 
         <DialogRef Id="BrowseDlg" />
         <DialogRef Id="DiskCostDlg" />
         <DialogRef Id="ErrorDlg" />
         <DialogRef Id="FatalError" />
         <DialogRef Id="FilesInUse" />
         <DialogRef Id="MsiRMFilesInUse" />
         <DialogRef Id="PrepareDlg" />
         <DialogRef Id="ProgressDlg" />
         <DialogRef Id="ResumeDlg" />
         <DialogRef Id="UserExit" />
 
         <!--   Make sure to include custom dialogs in the installer database via a DialogRef command, 
               especially if they are not included explicitly in the publish chain below -->
         <DialogRef Id="LicenseAgreementDlg_HK"/>
         <DialogRef Id="ServerDlg" />
 
         <Publish Dialog="BrowseDlg" Control="OK" Event="DoAction" Value="WixUIValidatePath" Order="3">1</Publish>
         <Publish Dialog="BrowseDlg" Control="OK" Event="SpawnDialog" Value="InvalidDirDlg" Order="4"><![CDATA[WIXUI_INSTALLDIR_VALID<>"1"]]></Publish>
 
         <Publish Dialog="ExitDialog" Control="Finish" Event="EndDialog" Value="Return" Order="999">1</Publish>
 
         <Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="LicenseAgreementDlg_HK">NOT Installed</Publish>
         <Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg">Installed AND PATCH</Publish>
 
         <Publish Dialog="LicenseAgreementDlg_HK" Control="Back" Event="NewDialog" Value="WelcomeDlg">1</Publish>
         <Publish Dialog="LicenseAgreementDlg_HK" Control="Next" Event="NewDialog" Value="InstallDirDlg">LicenseAccepted = "1"</Publish>
 
         <Publish Dialog="InstallDirDlg" Control="Back" Event="NewDialog" Value="LicenseAgreementDlg_HK">1</Publish>
         <Publish Dialog="InstallDirDlg" Control="Next" Event="SetTargetPath" Value="[WIXUI_INSTALLDIR]" Order="1">1</Publish>
         <Publish Dialog="InstallDirDlg" Control="Next" Event="DoAction" Value="WixUIValidatePath" Order="2">NOT WIXUI_DONTVALIDATEPATH</Publish>
         <Publish Dialog="InstallDirDlg" Control="Next" Event="SpawnDialog" Value="InvalidDirDlg" Order="3"><![CDATA[NOT WIXUI_DONTVALIDATEPATH AND WIXUI_INSTALLDIR_VALID<>"1"]]></Publish>
         <Publish Dialog="InstallDirDlg" Control="Next" Event="NewDialog" Value="ServerDlg" Order="4">WIXUI_DONTVALIDATEPATH OR WIXUI_INSTALLDIR_VALID="1"</Publish>
         <Publish Dialog="InstallDirDlg" Control="ChangeFolder" Property="_BrowseProperty" Value="[WIXUI_INSTALLDIR]" Order="1">1</Publish>
         <Publish Dialog="InstallDirDlg" Control="ChangeFolder" Event="SpawnDialog" Value="BrowseDlg" Order="2">1</Publish>
 
         <Publish Dialog="ServerDlg" Control="Back" Event="NewDialog" Value="InstallDirDlg">1</Publish>
         <Publish Dialog="ServerDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg">1</Publish>
 
         <Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="ServerDlg" Order="1">NOT Installed</Publish>
         <Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="MaintenanceTypeDlg" Order="2">Installed</Publish>
 
         <Publish Dialog="MaintenanceWelcomeDlg" Control="Next" Event="NewDialog" Value="MaintenanceTypeDlg">1</Publish>
 
         <Publish Dialog="MaintenanceTypeDlg" Control="RepairButton" Event="NewDialog" Value="VerifyReadyDlg">1</Publish>
         <Publish Dialog="MaintenanceTypeDlg" Control="RemoveButton" Event="NewDialog" Value="VerifyReadyDlg">1</Publish>
         <Publish Dialog="MaintenanceTypeDlg" Control="Back" Event="NewDialog" Value="MaintenanceWelcomeDlg">1</Publish>
      </UI>
 
      <UIRef Id="WixUI_Common" />
   </Fragment>
</Wix>

, ,

13 Responses to Real-World Example: WiX/MSI Application Installer

  1. Sean Seymour December 23, 2014 at 00:28 #

    This is great, thanks so much for sharing it. Having a full, real world example of a working WiX installer is invaluable. Sometimes tutorials just don’t cut it.

  2. Chris J March 25, 2015 at 15:31 #

    Wow, fantastic. Thanks for sharing this; this has just saved hours of blundering around in the dark, and maybe my sanity too…

  3. Thomas Freudenberg April 1, 2015 at 16:16 #

    Great solution. However, do you know how to skip the InstallDirDlg dialog on upgrades? If the application is once installed, I don’t want to bother the user with asking for the install directory again.

  4. Doug May 22, 2015 at 10:49 #

    Thank you so much for publishing this. +1 for Sean Seymours comment

  5. Gab August 21, 2015 at 14:31 #

    Thank you very much, this was very useful.

  6. Diwakar Padmaraja January 6, 2016 at 15:42 #

    Thanks, Helge. Great to see the full WiX Installer source example. Its very helpful. I have used it as a reference for some of our desktop setup/install kits.

  7. Juvenalcr February 4, 2016 at 06:17 #

    Hello.

    It is cool but when i execute it thrown Unresolved reference to symbol ‘Directory:INSTALLDIR’ in section ‘Product:*’ error

    so My question is why thrown me this mistake?

  8. Juvenal February 4, 2016 at 08:16 #

    hello i whould like to know why thrown me the next error

    Error 9 Item has already been added. Key in dictionary: ‘ServerDlg/BannerLine’ Key being added: ‘ServerDlg/BannerLine’ light.exe 0
    1 Setup

  9. Scott Ferguson February 5, 2016 at 04:08 #

    I can’t say just how much I appreciate you doing this! Thank you very much indeed!!

    Its great to be able to reference a real-world example after I have created a few of my own to see how others are using it. I like your way better! :-)

  10. George Mauer August 4, 2016 at 00:29 #

    Hi, sorry to be a bother, but any chance of putting a working version on github or something? This is a very frustrating way to browser source code.

  11. JB Dufour September 30, 2016 at 21:09 #

    Thanks for the very clear and complete example

  12. Abdul Hameed January 11, 2017 at 14:25 #

    Sir how i can customize VerifyReadyDlg like i want to show a directory information or any message in VerifyReadyDlg. Any help will be appreciated.

  13. granadaCoder January 12, 2017 at 20:45 #

    First. Thank you. This example got me over the hump.

    I have a question when computing the “INSTALLDIR”.

    Where you have the default install directory as:

    C:Program Files (x86)Helge KleinuberAgent

    I’m trying to get:

    C:Program Files (x86)Helge Klein{22222222-2222-2222-2222-222222222222}

    Where “22222222-2222-2222-2222-222222222222” is the ProductCode.

    Why am I doing this?

    I tried this:

    But that gives me (in the GUI)

    C:Program Files (x86)AppsCCT[ProductCode]

    aka, no replacement happening

    The strange thing is that the registry will work.

    example: (note the three times I add “[ProductCode]” to the key name(s))

Leave a Reply