One thing I found frustrating while working on one of my projects here during the Hackathon was that WiX does not have an easy to use File Chooser dialog to utilize within your installer. My goal was to be able to choose a config file during the installation, but also be able to specify the config file on the command line when running the resulting .msi headless. The path to this config file then needed to be fed to another executable that was being launched from within the installer. This short tutorial will cover three main things:

  1. Creating a Custom Action using C#
  2. Creating a custom dialog
  3. Utilizing a Property that can be set from the command line -or- the Custom Action and then pass the value of that property to an application that is launched from within the installer.

 

Solution Setup

If you want to follow along I created this example using Visual Studio 2010 and WiX 3.5. We start by creating a Setup Project. If you have WiX installed correctly you should be able to find this under the Windows Installer XML project templates.

 

Now the first thing you are going to want to do when developing a WiX installer is prevent the .msi from creating a restore point every time you run it otherwise it takes forever for your installer to actually get started doing its work. We do this by defining the MSIFASTINSTALL property and setting the value so that it only does File Costing and skips other checks and prevents the creation of a restore point.

<Property Id="MSIFASTINSTALL" Value="3" />

There are two other projects we want to create and add to our solution now. A new Class Library project called CustomAction which will be precisely what it says it is, our Custom Action that we will invoke as part of our dialog. For clarity I would rename Class1.cs to CustomAction.cs after you create this project. The other project we will create will simply be a new Console Application that will just echo out its command line arguments and wait for user input so we can test our work.

Add a reference to the CustomAction project to the Microsoft.Deployment.WindowsInstaller library, which should be available in your WiX installation directory. One thing we'll need to change in the Custom Action project will require that we unload the project and edit the .csproj file directly.  Add the following code to the correct PropertyGroup for your environment:

 

<WixCATargetsPath Condition=" '$(WixCATargetsPath)' == '' ">$(MSBuildExtensionsPath)\Microsoft\WiX\v3.5\Wix.CA.targets</WixCATargetsPath>

 

Also make sure you add the following Import directly above the closing Project tag at the bottom of the file:

<Import Project="$(WixCATargetsPath)" />

 

EchoApp

Our sample EchoApp will simply contain the following main method:

        static void Main(string[] args)
        {
            if (args.Count() > 0)
            {
                foreach (var arg in args)
                {
                    Console.WriteLine(arg);
                }
            }
            else
            {
                Console.WriteLine("No args detected.");
            }
            Console.WriteLine("Press any key to exit...");
            Console.ReadKey();
        }

 

Running EchoApp From The Installer

In order to get the EchoApp to execute as part of our installer we need to first make sure we include the build output of the EchoApp project as part of the installer. I prefer to do this in an automated fashion so that as your application changes and new artifacts and dependent assemblies start getting pulled in you don't need to hand edit your ComponentGroup. To do this we will be using heat.exe to harvest the output of our EchoApp project. By utilizing a Pre-Build Event in our ExampleSetup project we can do this with very little effort.

"$(WIX)bin\heat.exe" dir "$(ProjectDir)..\EchoApp\bin\$(ConfigurationName)" -sfrag -srd -sreg -svb6 -ag -dr "INSTALLLOCATION" -cg "ComponentGroup" -template fragment -o "$(ProjectDir)ComponentGroup.wxs" -var var.EchoAppDir

You can read up on heat.exe for explanations of the different arguments I have made use of. The only one I really wanted to point out is the -var var.EchoAppDir at the end of the line. What this does is tells heat that when it generates the list of artifacts for your component group to use that variable as the path to where the source item is located. In order for this to work properly we'll need to define EchoAppDir as a preprocessor variable. Open up the properties of the ExampleSetup project and go to the Build tab and define the path as a preprocessor variable.

EchoAppDir=..\EchoApp\bin\Debug\

If you build your EchoApp and then rebuild your ExampleApp project your Pre-Build Event should generate a file in your ProjectDir called ComponentGroup.wxs. Go ahead and add this file to your ExampleSetup project. Open this file up and find the File Id for the EchoApp.exe. This is the Id you will be using to reference the executable as part of the custom action within your installer. There are three things we need to do to set everything up to get the EchoApp run as part of the installer. First the generated ComponentGroup needs to be added as part of a Feature so we can reference it properly. Next we need to define the CustomAction itself. And finally we need to specify when the CustomAction is going to be invoked. The following is an example Product.wxs file that does these three things:

 

<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
	<Product Id="4020bfc1-d5c4-4de7-985b-6eb89ef150fc" Name="ExampleSetup" Language="1033" Version="1.0.0.0" Manufacturer="ExampleSetup" UpgradeCode="06b74d8e-9291-4f62-9ab6-43f89e408a5b">
		<Package InstallerVersion="200" Compressed="yes" />

    <Property Id="MSIFASTINSTALL" Value="3" />
    
		<Media Id="1" Cabinet="media1.cab" EmbedCab="yes" />

    <CustomAction Id="RunEchoApp" FileKey="fil147FF0AEBC7C22FF62CAE93B3D41AD58" ExeCommand='Hello World!' Execute="deferred" />

    <Directory Id="TARGETDIR" Name="SourceDir">
			<Directory Id="ProgramFilesFolder">
				<Directory Id="INSTALLLOCATION" Name="ExampleSetup">					
				</Directory>
			</Directory>
		</Directory>

    <InstallExecuteSequence>
      <Custom Action="RunEchoApp" Before="InstallFinalize" >(NOT REMOVE)</Custom>
    </InstallExecuteSequence>
    
		<Feature Id="ProductFeature" Title="ExampleSetup" Level="1">
      <ComponentGroupRef Id="ComponentGroup" />
			<ComponentGroupRef Id="Product.Generated" />
		</Feature>
	</Product>
</Wix>

At a high level what we did here is define a CustomAction named RunEchoApp and set it up to run only during an install before the InstallFinalize phase of the install process. You should be able to build and run the installer now and see that the EchoApp is indeed run as part of the installer. As you can see the ExeCommand for our type of CustomAction is actually what will be passed in as arguments to the EchoApp.

 

The Pass Through

While specifying and argument directly is nice, what we really are looking for is to handle arguments passed directly into the MSI to be passed down through to the EchoApp. We do this through the use of Properties. Let's define a Property called MESSAGE and pass it through to the EchoApp:

    <Property Id="MESSAGE" />
    <CustomAction Id="RunEchoApp"
                  FileKey="fil147FF0AEBC7C22FF62CAE93B3D41AD58"
                  ExeCommand='[MESSAGE]'
                  Execute="deferred" />

Now if you were to build the installer and just double-click it to run it you would see the "No args detected." message.  Run it from the command line passing it args however and you will see that our MESSAGE gets through!

msiexec.exe /i C:\Projects\ExampleSetup\ExampleSetup\bin\Debug\ExampleSetup.msi MESSAGE="ECHO!"

 

Adding the UI

While it is nice to be able to automate your installers, the whole point of using something like WiX is to be able to provide a rich installer package for your end user. In order to be able to add a custom UI to our ExampleSetup project we will need to add a reference to the WiX library called WiXUIExtension. And to keep things separate let's add a new file to our setup project called InstallerUi.wxs as a place to dump our UI definition. Since our main goal here is to utilize a OpenFileDialog I renamed our MESSAGE property to FILEPATH. As a base for the InstallerUi.wxs file we can use the following code:

 

<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
  <Fragment>

    <WixVariable Id="WixUICostingPopupOptOut" Value="1" Overridable="yes" />

    <Binary Id="CustomAction.CA.dll" src="..\CustomAction\bin\$(var.Configuration)\CustomAction.CA.dll" />
    <CustomAction Id="OpenFileChooser" Return="check" Execute="immediate" BinaryKey="CustomAction.CA.dll" DllEntry="OpenFileChooser" />

    <UI Id="ExampleSetupUI">
      <Property Id="DefaultUIFont" Value="WixUI_Font_Normal" />
      <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="WIXUI_INSTALLDIR" Value="TARGETDIR" />

      <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" />

      <Publish Dialog="ExitDialog" Control="Finish" Event="EndDialog" Value="Return" Order="999">1</Publish>
      <Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="FileBrowseDlg">1</Publish>

      <Publish Dialog="FileBrowseDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg" Order="1">1</Publish>
      <Publish Dialog="FileBrowseDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg" Order="1">1</Publish>
      <Publish Dialog="FileBrowseDlg" Control="Cancel" Event="SpawnDialog" Value="CancelDlg" Order="1">1</Publish>
      <Publish Dialog="FileBrowseDlg" Control="btnChange" Event="DoAction" Value="OpenFileChooser" Order="1">1</Publish>
      <Publish Dialog="FileBrowseDlg" Control="btnChange" Property="FILEPATH" Value="[FILEPATH]" Order="2" >1</Publish>

      <Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="FileBrowseDlg" Order="1">1</Publish>

      <Dialog Id="FileBrowseDlg" Width="370" Height="270" Title="File Browser Example Dialog" NoMinimize="yes">
        <Control Id="Next" Type="PushButton" X="236" Y="244" Width="56" Height="17" Default="yes" Text="Next" />
        <Control Id="Back" Type="PushButton" X="176" Y="244" Width="56" Height="17" Cancel="yes" Text="Back"/>
        <Control Id="Cancel" Type="PushButton" X="304" Y="243" Width="56" Height="17" Cancel="yes" Text="Cancel"/>
        <Control Id="ComboLabel" Transparent="yes" Type="Text" X="25" Y="58" Width="300" Height="10" TabSkip="no" Text="Specify the location for Environment Switcher file:" />
        <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 Type="Edit" Id="txtFilePath" Width="283" Height="15" X="30" Y="82" Property="FILEPATH" Text="[FILEPATH]" />
        <Control Type="PushButton" Id="btnChange" Width="56" Height="17" X="30" Y="100" Text="Browse" />
        <Control Type="Text" Id="Title" Transparent="yes"  Width="300" Height="15" X="11" Y="11" Text="Example Setup" />
        <Control Id="BannerBitmap" Type="Bitmap" X="0" Y="0" Width="370" Height="44" TabSkip="no" Text="!(loc.BrowseDlgBannerBitmap)"/>             
      </Dialog>

    </UI>

    <UIRef Id="WixUI_Common" />
  </Fragment>
</Wix>

That's a lot of code to swallow all at once. The main thing I want to draw your attention to is where we have added the Binary reference for our Custom Action through the built CustomAction.CA.dll that was built from the custom build target we had added earlier. The very next line defines the CustomAction itself and defines the entry point into the DLL that we will be using. Aside from that the only other interesting thing is the hook into the Browse button and the Publishing of the events to refresh the Property FILEPATH when the dialog closes.

We can then add this UI to our Product definition in Product.wxs using the UIRef tag:

<UIRef Id="ExampleInstallerUI" />

 

WiX and C# Custom Actions

If you have tried to build the CustomAction prior to this point you will notice that it doesn't build yet. The first thing is to create a CustomAction.config file in order to pass in some config we will need to make the installer happy at runtime when it goes to load our CustomAction. Make sure you the set build action on this file to Content or your custom action will not run! Add the following code to your new config file:

 

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <startup useLegacyV2RuntimeActivationPolicy="true">
    <supportedRuntime version="v4.0" />
  </startup>
</configuration>

Next we will actually define the behavior of the custom action itself. You will need to add a reference to System.Windows.Forms in order to get the OpenFileDialog. Here is the code I set up for displaying the dialog and taking the result and populating our FILEPATH property with the chosen file path:

 

using System.Threading;
using System.Windows.Forms;
using Microsoft.Deployment.WindowsInstaller;

namespace CustomAction
{
    public class CustomAction
    {
        [CustomAction]
        public static ActionResult OpenFileChooser(Session session)
        {

            session.Log("Begin OpenFileChooser Custom Action");

            var task = new Thread(() => GetFile(session));
            task.SetApartmentState(ApartmentState.STA);
            task.Start();
            task.Join();

            session.Log("End OpenFileChooser Custom Action");

            return ActionResult.Success;
        }

        private static void GetFile(Session session)
        {
            var fileDialog = new OpenFileDialog { Filter = "Config File (*.xml)|*.xml" };
            if (fileDialog.ShowDialog() == DialogResult.OK)
            {
                session["FILEPATH"] = fileDialog.FileName;
            }
        }
    }
}

All this code does is present the user with the dialog (within it's own thread for reasons I won't go into here) and then set the property FILEPATH if the user chooses a file. One thing to note here is that regardless of what the user does we always return success. The reasoning here is that if you return a non-success status from your Custom Action for any reason the installer will interpret it as a failure and bail out of the installation. In our case the non-selection or premature closing of the file chooser dialog is not a failure.

Putting It All Together

You should now be able to build your CustomAction project and then rebuild your installer. When you run the installer you should be presented with the Welcome screen and next should bring you to our custom dialog we created (defined by FileBrowseDlg above). Clicking the Browse button should pop up your standard File Chooser dialog and if you select a file the file path should end up in the Edit box. Once you begin the installation our EchoApp will run and your chosen file will be passed in as an argument. Make sure you wrap the [FILEPATH] in quotes properly in your RunEchoApp CustomAction otherwise paths with whitespace in them will split your path up into multiple args.

 

In Closing

While this is a very basic example of what you can do using WiX I hope you can see that by utilizing the power of managed code within your Custom Actions you can easily overcome any shortcomings of the WiX framework itself. Feel free to comment or ask questions!

 

/sleep