Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / HTML

A Multiple and Selective Configuration Installer for Business Applications Using Visual Studio and WiX Toolset

4.87/5 (8 votes)
14 Oct 2014CPOL12 min read 36.5K   1.2K  
A sample shows approaches of creating WiX installers and a bootstrapper bundle for the deployment of business applications with multiple and selective configurations.

Introduction

Most business application deployments are specific to configurations based on different environments. The MSBuild tool has options of directly editing the configuration files using AfterBuild tasks. This, however, is not suitable for the deployment with multiple and selective configurations. The optimal approach for such deployment may have these requirements:

  • Setting up configurations for various deployment environments, such as local debug, dev, uat, and prod in the Visual Studio projects.
  • Transforming XML items and values in the environment-specific configuration files.
  • Providing editing features on the installer UI for configuration items that need to be manually changed during the installation.
  • Enabling batch build to generate individual MSI package files for the multiple configurations.
  • Bundling multiple MSI packages into one output MSI package file with selective configuration options.

The article and downloaded source show a sample installer application implementing the requrements. You may need a basic knowledge and practices of the WiX Toolset to better understand the coding scenarios and run the sample application.

There are many manual editing tasks on the Visual Studio solution and project files in the sample application. If you try the same in your own applications, make sure to back up the original files before doing any editing work.

Installer Demo

The sample installer has been tested on machines with windows XP, Windows 7, Windows 8/8.1, Windows Server 2003, and Windows Server 2008 R2. When running the sample installer by executing the single consolidated MSI package file, SM.MultiConfigApp.SetupAll.msi, the first window of the WiX bootstrapper application will be shown:

Image 1

Clicking a radio button for desired configuration/environment will go to the WiX installer Welcome window.The parent bootstrapper window is still runing behind it.

Image 2

Clicking the Next button will display the Destination Folder dialog.

Image 3

Then there is the editable Databased Connection Configurations dialog.

Image 4

The progress bar dialog will be displayed based on the duration of the installation processes. If there is no error, the Completion dialog is shown.

Image 5

Click the Finish button will return to the bootstrapper window with the setup successful message.

Image 6

Installer Structures

There are two Visual Studio solutions and a standalone MSI package folder for the installer.

Image 7

  1. SM.MultiConfigApp solution contains a simple Window Form project as the target application and a WiX regular setup project.

  2. SM.MultiConfigApp.SetupAll solution contains three projects:

    • SM.MultiConfigApp.Bundle: a WiX bootstrapper project that burns multiple MSI files into a single output EXE file. It also sets the UI for configuration mode selections.

    • SM.MultiConfigApp.SetupAll: a WiX regular project that wraps the bundle and generates a single output MSI file. Many companies need MSI package files for application deployments due to policies or regulations.

    • SM.RemoveMsiWrapper: a console/window application project that automatically removes the bundle wrapper installation after the target application installation is completed or canceled. Thus, the installation simulates the process as if a real MSI, not the wrapper, is executed.

  3. MsiPackge: a folder that holds the output MSI package files from both solutions. It provides the shared source file location for burning the multiple individually configured MSI files.

Configuration Modes

The Visual Studio has a feature for setting the configurations and platforms using the Configuration Manager under the Build menu. For all projects in the sample application, I left the default Debug mode and added dev, uat, and prod as shown below. All names of added configuration modes are in lowercase for easy identification and MSI file name incorporation.

Image 8

The default Release mode is removed from the list after the desired configuration modes have been added for the build. This can be done by making changs in both solution and project files.

  1. Open the *.sln file with any text editor, then search and delete any line having the "Release|" in any GlobalSection.

    XML
    GlobalSection(SolutionConfigurationPlatforms) = preSolution
        - - -		
        Release|Any CPU = Release|Any CPU
        Release|Mixed Platforms = Release|Mixed Platforms
        Release|x86 = Release|x86
        - - -	
    EndGlobalSection
    GlobalSection(ProjectConfigurationPlatforms) = postSolution
        - - -	
        {5BF1BA2D-CEF2-4A8C-8669-96F894D5A04F}.Release|Any CPU.ActiveCfg = Release|Any CPU
        {5BF1BA2D-CEF2-4A8C-8669-96F894D5A04F}.Release|Any CPU.Build.0 = Release|Any CPU
        {5BF1BA2D-CEF2-4A8C-8669-96F894D5A04F}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
        {5BF1BA2D-CEF2-4A8C-8669-96F894D5A04F}.Release|Mixed Platforms.Build.0 = Release|Any CPU
        {5BF1BA2D-CEF2-4A8C-8669-96F894D5A04F}.Release|x86.ActiveCfg = Release|Any CPU
        - - -	
    EndGlobalSection            
  2. Open the *.csproj file with any text editor (or unload the project and select context menu command Edit <project_name> from Visual Studio Solution Explorer), then search and delete any entire PropertyGroup node having the "Release|" in it.

    XML
    <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
        <PlatformTarget>AnyCPU</PlatformTarget>
        <DebugType>pdbonly</DebugType>
        <Optimize>true</Optimize>
        <OutputPath>bin\Release\</OutputPath>
        <DefineConstants>TRACE</DefineConstants>
        <ErrorReport>prompt</ErrorReport>
        <WarningLevel>4</WarningLevel>
    </PropertyGroup>        

Configuration Files

Each additional configuration mode requires a corresponding configuration file which is dependent of the base configuration file, the App.config in our case. New files can be added to the project with the naming convention App.<configuration_name>[configuration_name].config, such as the App.prod.config. To attach these files to the App.config as child ones, open the project file (SM.MultiConfigApp.csproj in our case) and manually update the ItemGroup node with these added configuration files.

The ItemGroup node before updating:

XML
<ItemGroup>
   <None Include="App.config" />
   <None Include="App.dev.config" />
   <None Include="App.prod.config" />
   <None Include="App.uat.config" />    
</ItemGroup>

The ItemGroup node after updating:

XML
<ItemGroup>
   <None Include="App.config">
      <SubType>Designer</SubType>
   </None>
   <None Include="App.dev.config">
      <DependentUpon>App.config</DependentUpon>
      <SubType>Designer</SubType>
   </None>
   <None Include="App.prod.config">
      <DependentUpon>App.config</DependentUpon>
      <SubType>Designer</SubType>
   </None>
   <None Include="App.uat.config">
      <DependentUpon>App.config</DependentUpon>
      <SubType>Designer</SubType>
   </None>    
</ItemGroup>

The base and dependent configuration files should appear as a group in the Visual Studio Solution Explorer:

Image 9

Transforming Configuration Items

By using the TranformXml task from MSBuild Extensions, the App.config file in the sample application contains all needed items for the base local debug environment. Any environment-specific config file only keeps different items for the environment. The build process will populate the applicaiton configuration file for a particular enviroment installation with the same base items but dynamically update items that are defined from the environment-specific file .

To enable the TranformXml task, following XML code needs to be added to the project file (SM.MultiConfigApp.csproj in our case), under the root Project node.

XML
<UsingTask TaskName="TransformXml" AssemblyFile="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v12.0\Web\Microsoft.Web.Publishing.Tasks.dll" />
<Target Name="AfterCompile" Condition="exists('App.$(Configuration).config')">
    <!-- Generates the transformed App.config in the intermediate directory -->
    <TransformXml Source="App.config" Destination="$(IntermediateOutputPath)$(TargetFileName).config" Transform="App.$(Configuration).config" />
    <!-- Forces the build process to use the transformed configuration file -->
    <ItemGroup>
      <AppConfigWithTargetPath Remove="App.config" />
      <AppConfigWithTargetPath Include="$(IntermediateOutputPath)$(TargetFileName).config">
        <TargetPath>$(TargetFileName).config</TargetPath>
      </AppConfigWithTargetPath>
    </ItemGroup>
</Target>

Where the v12.0 indicates the project is created with Visual Studio 2013. If you use Visual Studio 2012 or 2010, change this to v11.0 or v10.0, respectively.

We then need to add Transform attributes and the desired values to the nodes in the environment-specific configuration files.

The example of the base configuration file, App.config, for the local debug environment:

XML
<configuration>
  <connectionStrings>
    <add name="AppDBConnection" connectionString="Data Source=AppDbSource;Initial Catalog=AppDb;Persist Security Info=True;User ID=Debug;Password=debug" providerName="System.Data.SqlClient"/>
  </connectionStrings>
  <appSettings>
    <add key="Environment" value="debug"/>       
    <add key="EmailFrom" value="debug.mail@mytest.com"/>
    <!--Not transformed-->
    <add key="EmailTo" value="appadmin@mytest.com"/>
  </appSettings>   
</configuration>

The example of the configuration file for the prod environment showing the transformed items:

XML
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
  <connectionStrings>
    <add name="AppDBConnection" connectionString="Data Source=AppDbSource;Initial Catalog=AppDb;Persist Security Info=True;User ID=prod;Password=prod" providerName="System.Data.SqlClient" xdt:Transform="SetAttributes(connectionString)" xdt:Locator="Match(name)"/>
  </connectionStrings>
  <appSettings>
    <add key="Environment" value="prod" xdt:Transform="SetAttributes" xdt:Locator="Match(key)" />
    <add key="EmailFrom" value="prod.mail@mytest.com" xdt:Transform="SetAttributes" xdt:Locator="Match(key)" />
  </appSettings>
</configuration>

Editing Configuration Values during Setup

The implementation of a regular WiX installer project is not the focus of this article. Readers can refer to documents of WiX installers everywhere for details. I here only discuss the on-installation configuration update feature used in the sample application.

The configured items should mostly be done within the deployed target application for a designated environment. What if some sensitive configuration values can only be changed by the deployment team during the setup? The password in a database connection string for the production is such an example. The WiX installer provides the feature and UI for modifying nodes and values in XML files after copied to the destination locations. The following steps are used in the downloaded sample installer for editing a connection string based on the WiX UI inputs.

  1. In the App.prod.config file, values of the Data Source, User ID, and Password in the original connection string can be anything or just empty but editable during the setup process.

    XML
    <connectionStrings>
        <add name="AppDBConnection" connectionString="Data Source=AppDbSource;Initial Catalog=AppDb;Persist Security Info=True;User ID=tbd;Password=****" providerName="System.Data.SqlClient" xdt:Transform="SetAttributes(connectionString)" xdt:Locator="Match(name)"/>
    </connectionStrings>
  2. The default values of the Data Source, User ID, and Password are set using the WiX installer variables in the Variables.wxi file.

    XML
    <?define DbDataSource = "AppDbSource" ?>
    - - - 
    <?if $(var.Configuration) = prod?>  
    <?define DbUserId = "prod" ?>
    <?define DbPassword = "" ?>
    <?endif?>
  3. All variables are accessed via properties set in the Product.wxs file.

    XML
    <Property Id="DB_DATASOURCE" Secure="yes">$(var.DbDataSource)</Property>
    <Property Id="DB_USERID" Secure="yes">$(var.DbUserId)</Property>
    <Property Id="DB_PASSWORD" Secure="yes">$(var.DbPassword)</Property>
  4. Adding a dialog for input fields of the Data Source, User ID, and Password values. See details in the Dialogs.wxs file. The editable text boxes take the values via the Property attribute of the Control nodes.

    XML
    <Control Id="DbDataSourceEdit" Type="Edit" Property="DB_DATASOURCE" />
    <Control Id="DbUserIdEdit" Type="Edit" Property="DB_USERID" />
    <Control Id="DbPasswordEdit" Type="Edit" Property="DB_PASSWORD" />
  5. Setting output XML configuration attribute values that will be replaced by the UI inputs.

    XML
    <util:XmlConfig Id="AppDbConnString"
    File="[INSTALLFOLDER]$(var.SM.MultiConfigApp.TargetName)$(var.SM.MultiConfigApp.TargetExt).config"
    Action="create" ElementPath="/configuration/connectionStrings/add[\[]@name='AppDBConnection'[\]]"
    Name="connectionString"
    Node="value"
    Value="Data Source=[DB_DATASOURCE]; Initial Catalog=AppDB; Persist Security Info=True; User ID=[DB_USERID]; Password=[DB_PASSWORD]"
    On="install"
    Sequence="1" />

Common Location for Output MSI Files

Output MSI files are usually generated in the folders with configuration names under the bin folder. This is inconvenient for the file access, especially by the bootstrapper application for bundling files into one MSI package. The individual MSI file generated for specific environments can be copied to a common location by adding the Copy task into the AfterBuild Target under the root Project node in the project file.

XML
<Target Name="AfterBuild">
    <Copy SourceFiles="$(OutputPath)$(OutputName).msi" 
        DestinationFiles="..\..\MsiPackage\$(OutputName).msi" />  
</Target> 

The MsiPackage folder is parallel to the solution root folder and shown in the previous screenshot.

Batch Build

For building the Visual Studio solutions with multiple configurations, the batch build option is an easy and efficient way to generate output files for all server-deployed applications. This can be done by simply click the Batch Build… from the Build menu in the Visual Studio, and then check the build configurations for project listed on the Batch Build dialog window.

 Image 10

When you open the SM.MultiConfigApp solution from the downloaded source in the Visual Studio and click the Build or Rebuild button on the Batch Build dialog window, three MSI files for the dev, uat, and prod configurations, respectively, will be in the MsiPackage folder. These files can directly be used if you only need to send individual MSI files for deploying the application to each environment, or can be taken by WiX bootstrapper application to generate a bundle output installer described in the following sections.

Using WiX Extended Bootstrapper Application

To minimize coding efforts, I do not create my own managed WiX bootstrapper application. The WiX Extended Bootstrapper Application (WiX Extended BA) basically meets my needs. The bootstrapper sits on the top of real installers and provides radio button selection options. The drawback is that I cannot find installation completion and cancelling event handlers available for calling by managed C# code to perform some custom actions. For using the WiX Extended BA in the sample application, following steps are needed.

  • Add a reference to WixBalExtensionExt.dll in the Visual Studio project and add the namespace reference into the Bundle.wxs file as instructed in the documents.

  • Copy and rename the Bundle4Theme.xml to SMBundleTheme.xml. Configure the XML node and values for the radio buttons in both SMBundleTheme.xml and Bundle.wxs files.

  • Include all MSI package files for various configurations under the Chain node in the Bundle.wxs file for required operations.

  • Bind the individual MSI packages to the radio buttons through the InstallCondition attribute. For example:

    XML
    InstallCondition="RadioButton_prod"
    

The code of WiX bootstrapper bundles is quite standard and not listed here. You can see the files in the SM.MultiConfigApp.Bundle project and documents elsewhere for details.

Wrapper Installer for Bundle Output

The WiX bootstrapper project always outputs an EXE file that cannot directly be converted into an MSI file. If an MSI file is needed, an additional installer project can be created as a wrapper for the bundle EXE file. The SM.MultiConfigApp.SetupAll project shows this wrapper activity. Executing the wrapper MSI file actually installs the bundle EXE file to a temporary directory and then starts a custom action to run the EXE file.

XML
<!--Set install destination directory-->
<Directory Id="TempFolder">
   <Directory Id="INSTALLFOLDER" Name="~_tmpdir"></Directory>
</Directory>

<!--Set files to be installed-->
<Component Id="BootStrapperComponent" Guid="{AAC1A55F-7C74-4C27-8665-E274D9CDFD83}">
   <File Id="File1" Source="$(var.SM.MultiConfigApp.Bundle.TargetPath)" />
</Component>

<!--Set custom action-->
<CustomAction Id="RunBsExe" FileKey="File1" Return="asyncNoWait" Execute="deferred" ExeCommand="" HideTarget="no" Impersonate="no" />

<!--Set CA running sequence-->
<InstallExecuteSequence>      
   <Custom Action="RunBsExe" After="InstallFiles">NOT Installed</Custom>     
</InstallExecuteSequence>

Removal of Wrapper Installer

The MSI wrapper creates an additional installation on the target machine that may not be acceptable for a normal deployment scenario. To automatically uninstall the wrapper, another application, SM.RemoveMsiWrapper, is added into the solution. The main task of it is to call the msiexec.exe and silently uninstall the wrapper application, SM.MultiConfigApp.SetupAll, based on the wrapper’s GUID.

C#
Process proc = new Process();
proc.StartInfo = new ProcessStartInfo("msiexec.exe", "/x " + WRAPPER_GUID + " /qn " + logParam);
proc.Start();   

Such an SM.RemoveMsiWrapper project needs to address the below issues:

  1. Running unattendantly without any UI windows. This is resolved by creating the Console application project and then setting the Output Type to the Windows Application on the project’s Properties > Application page.

    Note that setting the "proc.StartInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden" will only hide the UI for the application running under the process (the msiexec.exe in our case), not the console window for the SM.RemoveMsiWrapper application itself. The msiexec.exe uninstaller is actually quiet and no UI by using the parameter "/qn".

  2. Using a static GUID for the wrapper application so that the uninstaller can easily find and remove it. This is done by setting the Product Id of the SM.MultiConfigApp.SetupAll project to an explicit GUID value in the Product.wxs file:

    XML
    <Product Id="{5029EB25-4652-4F70-9CFD-91F7F81858E9}" 
             Name="SM.MultiConfigApp.SetupAll.Wrapper"
             - - -  >         
       - - -
    </Product>
  3. Executing the uninstaller. There should have been no problem if the complete and cancel handlers from the WiX Extended BA are available for calling by managed code. Fortunately, the WiX Extended BA always creates a log file in the user’s standard temporary folder during the execution. In the sample application, this log file is used to detect the actions of the installation completion or cancellation at the bootstrap level. Readers can look into the code in the RemoveMsiWrapper.Program.cs file for the processing logic but here are the outlines:

    • The RemoveMsiWrapper application must start to run immediately before the WiX Extended BA, SM.MultiConfigApp.Bundle. Thus it can find the latest log file created by the wrapper with a particular timestamp. This is specified by the execution sequence in the wrapper application SM.MultiConfigApp.SetupAll:

      XML
      <InstallExecuteSequence>
      <!--Schedule removing MSI wrapper app to run first-->
      <Custom Action="RemoveWrap" After="InstallFiles">NOT Installed</Custom>
      <Custom Action="RunBsExe" After="RemoveWrap">NOT Installed</Custom>     
      </InstallExecuteSequence>
    • The RemoveMsiWrapper application will keep running and checking the values in the log file with particular time intervals. The log file access intervals and the total allowed application running time can be pre-set. In the sample application, the log file access checking occurs every 2 seconds and it allows application to run for 90 minutes. A business application installer usually runs shortly and impossible to exceeds this limit.

    • The log file stream of the WiX Extended BA is actually held there during the WiX Extended BA execution. Any access to the file by the RemoveMsiWrapper application will be denied until the WiX Extended BA closes the log file at the time either completing or canceling the installation. When the RemoveMsiWrapper application opens the log file and has found the text "exit code: ", it then initiates the wrapper uninstaller.

    • The operations of the wrapper uninstaller are also logged to a text file by specifying the value in the "/l*" parameter of the msiexec.exe. Thus the wrapper removal process is tracked although nothing about the wrapper can be seen during the target application installation.

  4. Wrapper uninstaller permissions due to the UAC. This causes the issue of unable to "Run as administrator" silently on machines with the Vista and Window Server 2008 and above. Consequently, the wrapper fails to be uninstalled. The sample application uses the app.manifest approach to resolve the issue.

    • Add the app.manifest file into the RemoveMsiWrapper project by selecting the Project > Add > Add New Item > Visual C# Items > Application Manifest File.

    • Edit two lines of the app.manifest file as shown below.

      XML
      <assemblyIdentity version="1.0.0.0" name="SM.RemoveMsiWrapper"/>
        - - -
      <requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
        - - - 
    •  On the project’s Properties page, set the Application > Icon and manifest > Manifest to the app.manifest file.

    Now the wrapper uninstaller will automatically and silently run as the administrator if the target application installer is executed by a user who is in the local admin group.

Summary

There are challenges of making a single MSI based Windows installer with multiple and selective configurations for a business application. The article and sample installer provide the practical solution for the topic. In addition to the multiple configurations, the approach can also be extended to installers with multiple platforms or other multiple and selective item scenarios.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)