With the new .NET framework API providing such wonderful functionality, everybody is programming in C# or VB.NET except setup developers. There was quite some commotion in the WIX users list on writing managed custom actions. It was finally decided that it is generally harmful to write managed code custom actions as they would depend on .NET Framework and having dependencies for setup is plain bad design. But IMHO, we can use managed code custom actions if we are 100% sure that it would be present on the target platform or if it is a prerequisite and a part of your LaunchConditions.
Writing managed code custom actions can be tricky. The easiest way is to implement the System.Configuration.Install.Installer class. This Installer class has four methods which can be executed during the MSI thread. The Installer class was only meant for developers to do some dirty stuff. But it is far from perfect to be used as a part of the MSI thread. Some of my strong reasons for not using the Installer class are as below.
- InstallUtilLib.DLL is not completely silent. It still pops up an ugly message box with it fails.
- The methods do not have access to the MSI thread. So we would not be able to use properties or write to the MSI log file.
We are then left with only one choice. Writing DLLs with C++ which export functions which use managed code. One of the several places that this could be used is handling XML configuration data. Windows Installer supports writing properties to both INI and Registry files but we do not have such facility for XML files. I guess this should be on the TODO list for the next version of Windows Installer <smile/>. But until then we would have to do it ourselves. There is a neat task in NAnt named <xmlpoke>. This custom action is programmed in pretty much the same way without namespace support. Given the path to the XML file and the XPath expression, we would be able to replace the value with our own using the installer. And it should also support properties.
I start off by creating a class library project in VS.NET 2003 and start editing the ProjectName.CPP file. Note that you would have to create a Module Definition file to support exporting the DLL functions that you write as symbols. Now for the fun stuff.
Lets first write functions that can write to the log file.
UINT WriteToLog(MSIHANDLE hMSI, CHAR *strMessage)
{
MSIHANDLE hrec=MsiCreateRecord(1);
MsiRecordSetString(hrec,0, strMessage);
MsiProcessMessage(hMSI,INSTALLMESSAGE_INFO,hrec);
MsiCloseHandle(hrec);
return ERROR_SUCCESS;
}
The above function is a pretty simple function that just writes any string of characters to the log file. While this function is good enough, we will write another function that accepts the managed String type.
void WriteToLog1(MSIHANDLE hMSI, String* strMessage)
{
CHAR* strMessageArr={0};
strMessageArr=(char*)(void*)Marshal::StringToHGlobalAnsi(strMessage);
WriteToLog(hMSI,strMessageArr);
}
This functions accepts the managed String* and Marshals it to a character array and then writes it to the log file. Now that we have the logging functions ready, lets concentrate on the actual juice. The idea here is that we use a deferred custom action to poke values into the XML file. The custom action expects data in a '|' delimited list in the following format.
[XMLFILEPATH]|[XPATHEXPRESSION]|[NEWVALUE]
A immediate type 51 custom action sets the CustomActionData property in the above format.
extern
"C" __declspec(dllexport) UINT XmlPoke(MSIHANDLE hMSI)
{
TCHAR propval[261]={0};
DWORD len=261;
MsiGetProperty(hMSI,TEXT("CustomActionData"),propval,&len);
WriteToLog(hMSI,propval);
String *str=new String(propval);
String *delims="|";
Char delim[]=delims->ToCharArray();
String* split[]=str->Split(delim);
FileInfo* fi=new FileInfo(split[0]);
if(fi->Exists){
try{
XmlDocument* xdoc=new XmlDocument();
xdoc->Load(split[0]);
XmlNodeList * nl=xdoc->SelectNodes(split[1]);
IEnumerator * nodesenum=nl->GetEnumerator();
WriteToLog1(hMSI,split[1]);
while(nodesenum->MoveNext())
{
XmlNode* xNode=__try_cast<XmlNode*>(nodesenum->Current);
String* oldValue1=xNode->InnerXml;
WriteToLog1(hMSI,String::Concat(oldValue1, new String(" was the old value")));
xNode->InnerXml=split[2];
String* NewValue="New Value is ";
WriteToLog1(hMSI,String::Concat(NewValue,split[2]));
}
xdoc->Save(split[0]);
}
catch(Exception* exc){
WriteToLog1(hMSI,exc->Message);
}
}
else
{
WriteToLog(hMSI,"Error: The file does not exist");
}
return ERROR_SUCCESS;
}
The above function reads the CustomActionData property and splits it into an array. I assume that setup developers are good and they always give good data. So I have only gone for rudimentary error handling. The custom action never fails unless the error is catastrophic and is not handled by the try-catch block. It tries find if the XML file exists and if it does, it loads the XML file. I then use the SelectNodes() function to select the list of nodes as per the XPath expression. And finally I set the InnerXml value of the function to the new value. I also log these values as and when required. Thats it... as simple as it seems.
Dont forget to have the following header files and namespaces on top of the file. You would also have to include reference to System.Xml.Dll.
#include
"stdafx.h"
#include "windows.h" //Required by MSI.h
#include "XmlTasksManaged.h"
//The MSI Stuff
#include "Msi.h"
#include "MsiQuery.h"
//.NET Stuff goes here
using namespace System;
using namespace System::IO; //For the FileInfo Object
using namespace System::Collections; //For the IEnumerator
using namespace System::Xml; //The XML Stuff
using namespace System::Runtime::InteropServices; //For the Marshal object
As mentioned earlier the Namespace support is still not included. It is not a very difficult functionality to add. We can pass it to the CustomActionData property. We can then cut and slice the CustomActionData property as we please. <smile/>