Creating WiX Custom Actions in C# and Passing Parameters

I’m currently working on a project where I’m creating a custom HTTP module for a customer.  The module is to be used with an existing commercial product.  Since the custom HTTP module is working with an existing product, the process of installing it would need to make significant changes to the web.config file of the existing product, plus be able to remove those changes during a subsequent uninstall.

I hadn’t worked with Windows Installer XML (WiX) projects before so this was a perfect opportunity to get up to speed. 

Creating a Setup Project and C# Custom Action Project

Once you install the WiX toolset you’ll be able to create a new Setup Project from Visual Studio as shown below.

image

After creating the setup project you’ll have a Product.wxs file that defines your setup.  I won’t go into details about the general process of authoring this file as this is covered in detail in the online WiX tutorial.  Once we’ve created our setup project, we’ll need to add a C# Custom Action Project to our solution (you can see this project template option in the above screenshot as well).  Within the custom action project you’ll have a new CustomActions class that looks like the following:

  1: public class CustomActions
  2: {
  3:     [CustomAction]
  4:     public static ActionResult CustomAction1(Session session)
  5:     {
  6:         session.Log("Begin CustomAction1");
  7:  
  8:         return ActionResult.Success;
  9:     }
  10: }

This is where you’ll write the code that you want to be executed when the custom action method is called.  Any method that you want to expose to the setup project must be a public static method that is annotated with the CustomAction attribute and has the following method signature.

  1: public static ActionResult ActionMethod(Session session)

The Session parameter passed to the method is the object that controls the installation process.  Among other things, the Session object allows you to write output to the installation log file and gives you access to global properties for passing data (we’ll use this later).

So in my particular instance I needed to write a custom action that would perform various updates to the existing product’s web.config file as part of the install.  WiX provides what’s referred to as a Standard Custom Action library for manipulating XML files as part of an installation.  However, after reviewing the on-line content and some examples this proved to be far too cumbersome for my needs.

So my custom action method ultimately looked similar to the following:

  1: [CustomAction]
  2: public static ActionResult ConfigurEwsFilter(Session session)
  3: {
  4:     try
  5:     {
  6:         session.Log("Begin Configure EWS Filter Custom Action");
  7:  
  8:         // TODO: Make changes to config file
  9:  
  10:         session.Log("End Configure EWS Filter Custom Action");
  11:     }
  12:     catch (Exception ex)
  13:     {
  14:         session.Log("ERROR in custom action ConfigureEwsFilter {0}", 
  15:                     ex.ToString());
  16:         return ActionResult.Failure;
  17:     }
  18:  
  19:     return ActionResult.Success;
  20: }

The code snippet shows what could be argued as a standard boilerplate approach for custom actions.  Logging the entry and exit of the custom action, performing the configuration step necessary and handling an unhandled exceptions to make sure they are logged to the output correctly.

Compiling the custom action project will generate two DLLs.  The additional DLL, $(TargetName).CA.dll, provides a format that is callable from the MSI engine.  Therefore, this is the DLL that we want to reference from the *.wxs file in the setup project.  To do that, we add a Binary element within the Product element that was generated in the *.wxs when the setup project was created.

  1: <Product Id="*" Name="SetupProject1" 
  2:          Language="1033" Version="1.0.0.0" 
  3:          Manufacturer="" 
  4:          UpgradeCode="d206ba77-ddbe-41a3-893f-324f55242bef">
  5:  
  6:   <Binary Id="EwsAction.CA.dll"
  7:           src="..\EwsAction\bin\$(var.Configuration)\EwsAction.CA.dll" />
  8:  
  9: </Product>

In the snippet above, I’m referencing the *.CA.dll relative to the setup project (which is in the same solution).  I’m also indicating that I want to base the version of the *.CA.dll on the current build configuration of the setup project using the $(var.Configuration) syntax.  That way, if I build a debug version of the setup project I get a debug version of the custom action and a similar result for a release build.

Along with the Binary element, we’ll also need to add a CustomAction element to reference the Binary element’s Id and indicate the entry point that we want to execute.  This should look similar to the following:

  1: <CustomAction Id="ConfigurEwsFilter" 
  2:               Return="check" 
  3:               Execute="immediate" 
  4:               BinaryKey="EwsAction.CA.dll" 
  5:               DllEntry="ConfigurEwsFilter" />

In the above example, notice that the BinaryKey attribute matches the Id attribute of the Binary element we previously added.  Also note that the DllEntry attribute references the method name that was decorated with the CustomAction attribute in the C# code.

Adding the Binary and CustomAction elements by themselves however still does not result in the custom action actually being executed.  To include the custom action in the installation process we need to add an entry to the InstallExecuteSequence as shown.

  1: <InstallExecuteSequence>
  2:   <Custom Action="ConfigurEwsFilter" Before="InstallFinalize"  />
  3: </InstallExecuteSequence>

Note here that we’re referencing the Id attribute of the CustomAction element while indicating that we want this action to execute prior to the standard InstallFinalize action of the installation.

Passing Parameters

The last issue we need to resolve is retrieving the location of the existing product’s web.config file and passing it to the custom action.  In my case I can use a standard RegistrySearch action to read the install path of the existing product from the registry.  The web.config should exist within a well known folder path within the install path.  Since the RegistrySearch action will store the root install path to a property, I need a separate CustomAction to build out the full path to the web.config file.  As a consequence, I’ll also need another entry in the InstallExecuteSequence to execute this new custom action.  These 3 additions are shown below.

  1: <Property Id="PRODUCTINSTALLFOLDER">
  2:   <RegistrySearch Id='ProductInstallDir' 
  3:                   Type='raw' 
  4:                   Root='HKLM' 
  5:                   Key='SOFTWARE\Manufacturer\ExistingProduct\Setup' 
  6:                   Name='MsiInstallPath' 
  7:                   Win64='yes'/>
  8: </Property>
  9: <CustomAction Id='AssignConfigFile' 
  10:               Property='CONFIGFILE' 
  11:               Value='[PRODUCTINSTALLFOLDER]pathtoconfigfile\web.config' />
  12: <InstallExecuteSequence>
  13:   <Custom Action="AssignConfigFile" After="CostFinalize" />
  14:   <Custom Action="ConfigurEwsFilter" Before="InstallFinalize"  />
  15: </InstallExecuteSequence>

The result of the above is that the web.config file’s path should now be in the global property named CONFIGFILE.  With the Session object discussed earlier, I can now access this path from within my custom action code using syntax similar to the following:

  1: session.Log("Session value for CONFIGFILE = '{0}'", 
  2:             session["CONFIGFILE"]);

Now within my custom action method I can open the web.config file and make the necessary edits for my custom HTTP module.

Since the web.config edits are performed by a custom action, they won’t automatically be removed during an uninstall.  In my next blog post I’ll discuss creating a separate custom action to remove the web.config edits and have it run only during the uninstall of the module.

Comments

  • Anonymous
    January 07, 2013
    This article is so good. I have one problem: what is the pathtoconfigfile?  Is it a predefined function?

  • Anonymous
    January 24, 2013
    No, my notation wasn't terribly good in this case.  pathtoconfigfile is meant to be a placeholder in the event there are subfolders between the [PRODUCTINSTALLFOLDER] and the actual location of the configuration file.

  • Anonymous
    June 24, 2013
    I have a problem to install the Setup on XP. It fails to start the custom action. On W7 it works. How can I run a custom Action on XP? .net 4.0 is installed. The log tells only that the msi startet. Then the setup make a rollback. without the custom action the setup does not fail.

  • Anonymous
    September 16, 2014
    The comment has been removed

  • Anonymous
    December 03, 2014
    Hi James I'm trying to implement passing parameters from Wix to C# Custom Action, but I am facing problem. I have asked the question here stackoverflow.com/.../wix-custom-action-implementation-for-writing-installfolder-in-text . Can you help me in where I am facing the problem?

  • Anonymous
    February 26, 2016
    The comment has been removed