Table of Contents
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.
"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.
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:
Right click on the project and choose Unload Project.
Figure 1 - Unloading Project
Right click on the unloaded project and choose Edit.
Figure 2 - Editing Project
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.
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.
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.
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.
Close the file and right click on the project -> ReloadProject
.
Figure 3 - Reloading Project
The result of the changes should be similar to Listing 10.
="1.0"="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)" />
</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.
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.
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.
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.
- Add a link to the DLL.
<Binary Id="myAction"
SourceFile="..\MyCustomAction\bin\Release\MyCustomAction.CA.dll" />
Listing 4 – Adding link to the library.
- Specify the custom action.
<CustomAction Id="myActionId"
BinaryKey="myAction"
DllEntry="MySimpleAction"
Execute="deferred"
Return="check" />
Listing 5 – Specifying custom action.
- 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.
="1.0"="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" />
<Property Id="WIXUI_INSTALLDIR" Value="INSTALLLOCATION" />
<Directory Id="TARGETDIR" Name="SourceDir">
<Directory Id="ProgramFilesFolder">
<Directory Id="INSTALLLOCATION"
Name="WixCustomAction">
<Component Id="SomeContent" Guid="">
<File Id="BuildFile" KeyPath="yes"
Source="Build.build" Vital="yes" />
</Component>
</Directory>
</Directory>
</Directory>
<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.
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 |
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.
="1.0"="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" />
<Property Id="WIXUI_INSTALLDIR" Value="INSTALLLOCATION" />
<Directory Id="TARGETDIR" Name="SourceDir">
<Directory Id="ProgramFilesFolder">
<Directory Id="INSTALLLOCATION"
Name="WixCustomAction">
<Component Id="SomeContent" Guid="">
<File Id="BuildFile"
KeyPath="yes" Source="
Build.build" Vital="yes" />
</Component>
</Directory>
</Directory>
</Directory>
<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.
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.
="1.0"="utf-8"
<Project ToolsVersion="3.5" DefaultTargets="Build"
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<MsiOut>bin\Release\CustomActionTest.msi</MsiOut>
<ProjectName>WixCustomAction</ProjectName>
</PropertyGroup>
<ItemGroup>
<WixCode Include="Product.wxs" />
</ItemGroup>
<ItemGroup>
<WixObject Include="Product.wixobj" />
</ItemGroup>
<Target Name="Build">
<MSBuild
Projects="..\WixCustomActionExample.sln"
Targets="Build"
Properties="Configuration=Release" />
</Target>
<Target Name="CreateInstaller">
<Exec
Command='"$(WIX)bin\candle" @(WixCode, ' ')'
ContinueOnError="false"
WorkingDirectory="." />
<Exec
Command='"$(WIX)bin\light" -ext WixUIExtension -out
$(MsiOut) @(WixObject, ' ')'
ContinueOnError="false"
WorkingDirectory="." />
<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.
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 ;). Thanks for reading.
- 02. December. 2010: Initial publication
- 28. January. 2011: Added example with passing data from WIX to C# Custom Action