Click here to Skip to main content
Click here to Skip to main content
Go to top

Creating Custom Action for WIX Written in Managed Code without Votive

, 31 Jan 2011
Rate this:
Please Sign up or sign in to vote.
Creating Custom Action for WIX written in managed code without Votive

Table of Contents

Introduction

In this article, I introduce a complete example of how to create a custom action for WIX installer written in C#. The Votive is not presented here and therefore the example is applicable to all versions of Visual Studio including the express edition. I also introduce how to integrate the custom action into an installer and the build script to build everything.

A Word about Votive

"What is Votive" can be found here. In spite of the fact that the Votive is a very nice plug-in, you can find a lot of restrictions when you are trying to use it. Unfortunately, Votive is currently available just for VS 2005/2008 and because it’s a plug-in, it can’t be used in express editions. I gave up with this plug-in a long time ago and therefore it’s not presented in this article either.

This article also serves the purpose of creating a custom action without the Votive.

Implementation

The implementation itself is not so difficult. However, there are some tricky steps which should be accomplished in order to achieve the goal.

I created the library called MyCustomAction. You can see it in the source code. The library contains a single class with a single static method. The code can be seen in Listing 1.

[CustomAction]
public static ActionResult MySimpleAction(Session session)
{
	try
	{
		File.AppendAllText(@"c:\tmp\time.txt", ";
			Installation: " + DateTime.Now.ToString());
	}
	catch (Exception)
	{
		return ActionResult.Failure;
	}
	return ActionResult.Success;
}
Listing 1 – A Custom Action written in C#.

As you can see, there is nothing complicated in the code. The important thing is to mark the method by CustomAction attribute, return the ActionResult and expect the Session. In order to have those types available, you will have to reference the library Microsoft.Deployment.WindowsInstaller.dll. This library is available in %ProgramFiles%\Windows Installer XML v3\SDK. Make it Copy Local True in properties. Then you can write your custom C# code into this function as you like. As a test, I prepared some simple code to just write a time stamp into a file called time.txt in the tmp directory on C drive.

Assuming you have your C# code finished, you can alter the build process to create an assembly which should be acceptable for WIX. To do that, follow those steps:

  1. Right click on the project and choose Unload Project.

    1.jpg

    Figure 1 - Unloading Project
  2. Right click on the unloaded project and choose Edit.

    2.jpg

    Figure 2 - Editing Project
  3. In the first PropertyGroup element, change the TargetFrameworkVersion to the version which is most convenient for you. It is the version of .NET Framework. At this time, the highest possible version is v3.5. But it may be different if you use a higher version of WIX than I do. I'm using WIX 3.0.

  4. Into the first PropertyGroup element, add the element from Listing 2. It is the path to the WIX targets where the post-build action for creating the final version of DLL is defined which should be acceptable by WIX.

    <WixCATargetsPath Condition=" '$(WixCATargetsPath)' == '' ">
    $(MSBuildExtensionsPath)\Microsoft\WiX\v3.0\Wix.CA.targets</WixCATargetsPath>
    Listing 2 – Declaration of WixCATargetsPath variable.
  5. To the end of the file, right before the closing Project tag, add the following import element:

    <Import Project="$(WixCATargetsPath)" />
    Listing 3 – Importing the WIX CA targets.

    This will achieve that the targets will be loaded.

  6. In the ItemGroup element, find the Reference element with Include="Microsoft.Deployment.WindowsInstaller". As a sub element, put <Private>True</Private> in order to be sure that the assembly will always be copied to the bin folder.

  7. Close the file and right click on the project -> ReloadProject.

    3.jpg

    Figure 3 - Reloading Project

The result of the changes should be similar to Listing 10.

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build"
	xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
    <ProductVersion>8.0.30703</ProductVersion>
    <SchemaVersion>2.0</SchemaVersion>
    <ProjectGuid>{B7AF9993-2C2E-4C03-98F2-109A8B6557BE}</ProjectGuid>
    <OutputType>Library</OutputType>
    <AppDesignerFolder>Properties</AppDesignerFolder>
    <RootNamespace>MyCustomAction</RootNamespace>
    <AssemblyName>MyCustomAction</AssemblyName>
    <TargetFrameworkVersion>v3.5</TargetFrameworkVersion>
    <FileAlignment>512</FileAlignment>
    <WixCATargetsPath Condition=" '$(WixCATargetsPath)' == '' ">
	$(MSBuildExtensionsPath)\Microsoft\WiX\v3.0\Wix.CA.targets</WixCATargetsPath>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
    <DebugSymbols>true</DebugSymbols>
    <DebugType>full</DebugType>
    <Optimize>false</Optimize>
    <OutputPath>bin\Debug\</OutputPath>
    <DefineConstants>DEBUG;TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
    <DebugType>pdbonly</DebugType>
    <Optimize>true</Optimize>
    <OutputPath>bin\Release\</OutputPath>
    <DefineConstants>TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
  </PropertyGroup>
  <ItemGroup>
    <Reference Include="Microsoft.Deployment.WindowsInstaller,
	Version=3.0.0.0, Culture=neutral, PublicKeyToken=ce35f76fcda82bad,
	processorArchitecture=MSIL">
      <SpecificVersion>False</SpecificVersion>
      <HintPath>..\..\..\..\Program Files\
	Windows Installer XML v3\SDK\Microsoft.Deployment.WindowsInstaller.dll</HintPath>
      <Private>True</Private>
    </Reference>
    <Reference Include="System" />
    <Reference Include="System.Core" />
    <Reference Include="System.Xml.Linq" />
    <Reference Include="System.Data.DataSetExtensions" />
    <Reference Include="System.Data" />
    <Reference Include="System.Xml" />
  </ItemGroup>
  <ItemGroup>
    <Compile Include="SimpleCustomAction.cs" />
    <Compile Include="Properties\AssemblyInfo.cs" />
  </ItemGroup>
  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
  <Import Project="$(WixCATargetsPath)" />
  <!-- To modify your build process, add your task inside one of the targets
	below and uncomment it.
       	Other similar extension points exist, see Microsoft.Common.targets.
  <Target Name="BeforeBuild">
  </Target>
  <Target Name="AfterBuild">
  </Target>
  -->
</Project>
Listing 10 – Complete csproj of the class library project containing the custom action.

Well, you can try to build the project using Visual Studio or directly using msbuild. If it fails, you did something wrong in the steps above. If it succeeds, look into the bin directory. You should see 3 assemblies. The most important assembly we are expecting to see there is the assembly with the name your_assembly.CA.dll.

4.jpg

Figure 4 – Build result of the class library containing the custom action.

This assembly is actually NOT the .NET managed assembly but unmanaged assembly (specifically C++ in this case) created by WIX target in post-build action. This assembly is exposing a DLL entry point with the name of your static function. You can use for example the http://www.dependencywalker.com/ to discover it. By this, your custom action written in C# is done. In the following part of the article, I will describe how to integrate your custom action to the installer.

Adding Custom Action to the Installer

I’d like to split this chapter into two parts. In the first part is shown just simple executing of the custom action. In the second part is shown the example with some parameters passed from WIX to C# code.

Adding Dummy Custom Action to the Installer

The WIX has several types of custom action calls. We need the Type 1: calling a function from a dynamic-link library. This consists of 3 steps.

  1. Add a link to the DLL.
    <Binary Id="myAction"
    SourceFile="..\MyCustomAction\bin\Release\MyCustomAction.CA.dll" />
    Listing 4 – Adding link to the library.
  2. Specify the custom action.
    <CustomAction Id="myActionId"
      BinaryKey="myAction"
      DllEntry="MySimpleAction"
      Execute="deferred"
      Return="check" />
    Listing 5 – Specifying custom action.
  3. Specify in which step of installation the custom action should be executed.
     <InstallExecuteSequence>
        <Custom Action="myActionId" Before="InstallFinalize" />
    </InstallExecuteSequence>
    Listing 6 – Calling custom action.

The entire code could be seen in Listing 7.

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

	<Product Id="{C98462B7-1458-4199-AEA0-2066C3F4D2D1}"
			 Name="WixCustomActionTest.Setup"
			 Language="1033"
			 Version="1.0.0.0"
			 Manufacturer="WixCustomAction.Setup"
			 UpgradeCode="{15F2F252-9FCD-4C3B-AC29-070C624610B9}">

		<Package InstallerVersion="200" Compressed="yes" />

		<Property Id="ALLUSERS" Value="1" />

		<Binary Id="myAction"
		SourceFile="..\MyCustomAction\bin\Release\MyCustomAction.CA.dll" />

		<Media Id="1" Cabinet="MyWeb.cab" EmbedCab="yes" />

		<!-- Configurable install location -->
		<Property Id="WIXUI_INSTALLDIR" Value="INSTALLLOCATION" />

		<!-- Creating directories -->
		<Directory Id="TARGETDIR" Name="SourceDir">
			<!-- Install stuff into program files folder. -->
			<Directory Id="ProgramFilesFolder">
				<Directory Id="INSTALLLOCATION"
					Name="WixCustomAction">
					<Component Id="SomeContent" Guid="">
						<!-- This file is here just
						for having some content
						in the installer. -->
						<File Id="BuildFile" KeyPath="yes"
						Source="Build.build" Vital="yes" />
					</Component>
				</Directory>
			</Directory>
		</Directory>

		<!-- Complete feature which will be installed. -->
		<Feature Id="Complete"
             Title="WixCustomAction - WixCustomAction awesome test"
             Level="1"
             Display="expand">
			<ComponentRef Id="SomeContent" />
		</Feature>

		<CustomAction Id="myActionId"
					  BinaryKey="myAction"
					  DllEntry="MySimpleAction"
					  Execute="deferred"
					  Return="check" />

		<InstallExecuteSequence>
			<Custom Action="myActionId" Before="InstallFinalize" />
		</InstallExecuteSequence>

		<UIRef Id="WixUI_Minimal" />

	</Product>
</Wix>
Listing 7. - Product.wxs.

Using Conditions in Custom Action Call

Conditions in the context of Custom Actions are written as the inner text into the Custom attribute. If the condition is true, the action is performed and vice versa. The condition could be composed by other conditions using AND, OR operators. As the input to the condition are properties and/or static values. Conditions in Custom Actions are in the most cases composed only from properties. These properties you can find in [2] especially in the part “Installation Status Properties”. So if you want to call the Custom Action from Listing 6 only when the application takes installation, then specify the condition as in Listing 10.

<Custom Action="myActionId" Before="InstallFinalize">NOT Installed</Custom>
Listing 10 – A Custom Action executed only when application installation.

To be more clear about that, I’m introducing a table explaining what values the properties contain in which stage. I got it from [3].

Property Name Install Uninstall Repair Modify Upgrade Link to documentation
Installed False True False True True Show
REINSTALL True False True False False Show
UPGRADINGPRODUCTCODE True False True True True Show

Example Extended by Input Parameters

When writing a Custom Action, you usually need to pass some parameters from WIX to C#. This could be done by session argument especially CustomActionDataCollection. For more information discover the code from Listing 11. It is just a “Hello world” example of displaying values passed from WIX to C#.

[CustomAction]
public static ActionResult MySimpleAction(Session session)
{
	try
	{
		session.Message(InstallMessage.Warning,
			new Record(new string[]
			{
				string.Format("INSTALLLOCATION{0}", 
				session.CustomActionData["INSTALLLOCATION"])
			}));

		session.Message(InstallMessage.Warning,
			new Record(new string[]
			{
				string.Format("Another Value{0}", 
				session.CustomActionData["AnotherValue"])
			}));
	}
	catch (Exception exception)
	{
		session.Log(exception.ToString());
		return ActionResult.Failure;
	}
	return ActionResult.Success;
}
See Listing 11 for altering the C# code.

However, it’s not that simple. There has to be another Custom Action (Type 51) to set up WIX properties into CustomActionDataCollection on WIX side. How to do it you can see in Listing 12.

<CustomAction Id="SetCustomActionDataValue" Return="check"
                 Property="myActionId" Value="INSTALLLOCATION=[INSTALLLOCATION];
                 AnotherValue='Just a value'" />
Listing 12 – Custom Action type 51 to set the CustomActionDataCollection.

You have to also specify when this action should be executed. The most reasonable time is just right before your C# custom action which could be achieved by the code from Listing 14.

<InstallExecuteSequence>
    <Custom Action="SetCustomActionDataValue" Before="myActionId">NOT Installed</Custom>
    <Custom Action="myActionId" Before="InstallFinalize">NOT Installed</Custom>
</InstallExecuteSequence>
Listing 14 – Specifying execution of SetCustomActionDataValue.

As in the precedent example, it is good to introduce the entire WIX code to see the differences comparing to Listing 7.

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

	<Product Id="{C98462B7-1458-4199-AEA0-2066C3F4D2D1}"
			 Name="WixCustomActionTest.Setup"
			 Language="1033"
			 Version="1.0.0.0"
			 Manufacturer="WixCustomAction.Setup"
			 UpgradeCode="{15F2F252-9FCD-4C3B-AC29-070C624610B9}">

		<Package InstallerVersion="200" Compressed="yes" />

		<Property Id="ALLUSERS" Value="1" />

		<Binary Id="myAction"
		SourceFile="..\MyCustomAction\bin\Release\MyCustomAction.CA.dll" />

		<Media Id="1" Cabinet="MyWeb.cab" EmbedCab="yes" />

		<!-- Configurable install location -->
		<Property Id="WIXUI_INSTALLDIR" Value="INSTALLLOCATION" />

		<!-- Creating directories -->
		<Directory Id="TARGETDIR" Name="SourceDir">
			<!-- Install stuff into program files folder. -->
			<Directory Id="ProgramFilesFolder">
				<Directory Id="INSTALLLOCATION"
				Name="WixCustomAction">
					<Component Id="SomeContent" Guid="">
						<!-- This file is here just for
						having some content 
						in the installer. -->
						<File Id="BuildFile"
						KeyPath="yes" Source="
						Build.build" Vital="yes" />
					</Component>
				</Directory>
			</Directory>
		</Directory>

		<!-- Complete feature which will be installed. -->
		<Feature Id="Complete"
             Title="WixCustomAction - WixCustomAction awesome test"
             Level="1"
             Display="expand">
			<ComponentRef Id="SomeContent" />
		</Feature>

		<CustomAction Id="myActionId"
					  BinaryKey="myAction"
					  DllEntry="MySimpleAction"
					  Execute="deferred"
					  Return="check" />

    <CustomAction Id="SetCustomActionDataValue" Return="check"
                 Property="myActionId" Value="
                 INSTALLLOCATION=[INSTALLLOCATION];AnotherValue='Just a value'" />

		<InstallExecuteSequence>
      <Custom Action="SetCustomActionDataValue"
      Before="myActionId">NOT Installed</Custom>
			<Custom Action="myActionId" Before="
			InstallFinalize">NOT Installed</Custom>
		</InstallExecuteSequence>

		<UIRef Id="WixUI_Minimal" />

	</Product>

</Wix>
Listing 13 – Product.wxs from extended example.

Building Installer

As you noticed, the Product.wxs is standalone with no integration to Visual Studio solution. To build an installer, we have to create a build script. Basically, we need two commands to do that: call candle.exe and then light.exe. For that purpose, I created a target called CreateInstaller which could be seen in Listing 8.

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="3.5" DefaultTargets="Build"
	xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

	<!-- Define some variables -->
	<PropertyGroup>
		<MsiOut>bin\Release\CustomActionTest.msi</MsiOut>
		<ProjectName>WixCustomAction</ProjectName>
	</PropertyGroup>

	<!-- The list of WIX input files -->
	<ItemGroup>
		<WixCode Include="Product.wxs" />
	</ItemGroup>

	<!-- The list of WIX after candle files -->
	<ItemGroup>
		<WixObject Include="Product.wixobj" />
	</ItemGroup>

	<!-- Define default target with name 'Build' -->
	<Target Name="Build">
		<!-- Compile whole solution in release mode -->
		<MSBuild
			Projects="..\WixCustomActionExample.sln"
			Targets="Build"
			Properties="Configuration=Release" />
	</Target>

	<!-- Define creating installer in another target -->
	<Target Name="CreateInstaller">

		<!-- At last create an installer -->
		<Exec
			Command='"$(WIX)bin\candle" @(WixCode, &apos; &apos;)'
			ContinueOnError="false"
			WorkingDirectory="." />
		<Exec
			Command='"$(WIX)bin\light" -ext WixUIExtension -out
				$(MsiOut) @(WixObject, &apos; &apos;)'
			ContinueOnError="false"
			WorkingDirectory="." />

		<!-- A message at the end -->
		<Message Text="Install package has been created." />
	</Target>

</Project>
Listing 8 – Build.build the build script to build the installer.

The build script from Listing 8 also contains the target for building the solution in release mode.

Open the Visual Studio Command prompt or cmd if you have msbuild.exe available there. Go to the folder where the build script is located and execute the command from Listing 9.

Msbuild /t:Build;CreateInstaller Build.build
Listing 9 – Building an installer.

If everything goes well, you should have the msi package in the bin folder.

Conclusion

Writing a custom action in managed code is a very comfortable way of adding a custom activity to the installer. It’s good to understand the way it’s working in order to be able to discover malicious errors. I hope this article brings a light into this area and was useful for you. Please don’t hesitate to share some thoughts and votes Wink | ;) . Thanks for reading.

References

History

  • 02. December. 2010: Initial publication
  • 28. January. 2011: Added example with passing data from WIX to C# Custom Action

License

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

Share

About the Author

Petr Pechovic
Software Developer (Senior)
Czech Republic Czech Republic

Comments and Discussions

 
Questionhelp needed Pinmembernikhil12343-Mar-14 0:15 
GeneralMy vote of 5 Pinmemberknluasdf14-Jul-13 23:10 
QuestionYour method of passing propertie values to CA WRONG! Pinmemberserovodorod201112-Jun-13 5:10 
May be access to properties must be in this way:
session["PROPERTY_NAME"], but not session.CustomActionData["PROPERTY_NAME"]?
See this article WiX Tricks[^]
SuggestionAdditional paths for csproj PinmemberJoshua Hoexter8-Apr-13 10:16 
QuestionYou save my life Pinmemberquizerm6-Jan-13 20:53 
GeneralVery Useful Pinmemberatul_dev21-Nov-12 22:03 
GeneralRe: Very Useful PinmemberPetr Pechovic11-Dec-12 5:04 
QuestionPěkný: Nice and thanks a lot Pinmemberrajen shrestha30-Oct-12 18:58 
AnswerRe: Pěkný: Nice and thanks a lot PinmemberPetr Pechovic11-Dec-12 5:03 
GeneralMy vote of 5 Pinmemberkevinb9930-Oct-12 8:43 
GeneralRe: My vote of 5 PinmemberPetr Pechovic11-Dec-12 5:03 
Questionvery useful! PinmemberSouthmountain31-Aug-12 9:12 
AnswerRe: very useful! PinmemberPetr Pechovic11-Dec-12 5:04 
Questionnice PinmemberCIDev19-Apr-12 4:00 
AnswerRe: nice PinmemberPetr Pechovic19-Apr-12 5:12 
Question<Import ... invalid elemet PinmemberRoss Brigoli17-Jan-12 23:34 
AnswerRe: <Import ... invalid elemet PinmemberPetr Pechovic18-Jan-12 1:09 
GeneralRe: <Import ... invalid elemet Pinmemberpr362611-Dec-12 4:53 
GeneralRe: <Import ... invalid elemet PinmemberPetr Pechovic11-Dec-12 5:00 
GeneralRe: <Import ... invalid elemet Pinmembermarkus300020-Mar-13 6:01 
GeneralRe: <Import ... invalid elemet PinmemberMember 327877428-Oct-13 2:09 
QuestionVery helpful - Several values just in one line Pinmemberallancm28-Aug-11 22:06 
AnswerRe: Very helpful - Several values just in one line PinmemberPetr Pechovic29-Aug-11 0:05 
GeneralMy vote of 5 Pinmembernenfa10-Jan-11 3:05 
GeneralRe: My vote of 5 PinmemberPetr Pechovic10-Jan-11 3:58 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web02 | 2.8.140916.1 | Last Updated 31 Jan 2011
Article Copyright 2010 by Petr Pechovic
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid