Click here to Skip to main content
15,886,110 members
Articles / Web Development / HTML

Writing a Wix Extension to Read XML Files

Rate me:
Please Sign up or sign in to vote.
4.61/5 (7 votes)
16 Jan 2015CPOL18 min read 34K   687   9   8
Goes through how to write a compiler extension for Wix, using the read world example of an XML search extension to read values out of XML files

Introduction

Wix by default includes extensions that write out values into XML files. However the corresponding read functionality is missing for whatever reason. Therefore I wrote my own extension to do this. Since I was in an article writing mood at the time, I wrote up the steps I went through. Hopefully, it serves as a useful way of demonstrating how to write Wix add-ins, and also producing something useful at the end.

Using the Wix.XmlSearch Extension

The extension extends the Property Wix element by adding a child sub element with the name XmlSearch - this is similar to the way that RegistrySearch and FileSearch work. The syntax looks like below:

XML
<Wix xmlns:xmlsearch="http://schemas.rottedfrog.co.uk/wix/XmlSearch">
...
<Property Id="Monkey" Value="Blah">
<!--The Value will be overwritten if the XmlSearch succeeds-->
  <xmlsearch:XmlSearch Id="CheckForMonkeys" 
  File="[INSTALLDIR]Primates.xml" 
  XPath="/Primates/Primate[\[]@Type=&quot;Monkey&quot;[\]]" 
  Condition="Not GORILLA" />
</Property>

The attributes are as follows:

  • The Id is a unique identifier for the XmlSearch entry.
  • The File is the path of the file to search - property values are allowed.
  • XPath is the path to the attribute or element in the XML file to read the value from.
  • Condition is an optional attribute - if present, then the XmlSearch is skipped if the Condition is not met.

First of all, the most important thing you need to have is the wix source code. There is no documentation for a lot of this, therefore you *have* to look at the wix source in order to find what you want.

The wix help has a brief introduction, and a useful example of a preprocessor extension.

Parsing Elements

  • local name
  • context values (look in wix source)

Assumptions

I'm going to assume a working knowledge of Wix and MSI technology in general. Specifically, you should not be fazed when I start talking about custom tables and custom actions.

I'll assume that you have VS2013 installed, along with Wix >= 3.9 and Votive. You can develop Wix extensions with earlier version (including the express editions), without a great deal of tweaking, but for brevity and to make my life easier, I'll assume everyone has the same setup as me.

Architecture

At a high level, the architecture is straightforward: we are going to extend the wix schema to add a new element "XmlSearch" which can be a child of any Property. This will take as attributes, the file name and XPath for the value to extract, looking somewhat like the below:

XML
<ext:XmlSearch Id="search1" File="[SomeFolder]HelloWorld.xml" 
XPath="/configuration/appSettings/add[\[]@key='hello'[\]]" />

Under the hood, this is going to add a custom table to the MSI, also called "XmlSearch" which contains all the details from the XmlSearch element, and a custom action, imaginatively named XmlSearch, which will read that table and populate the properties with the values from the XML File.

Step 1: Creating an Extension Project

We'll start by creating a C# Class Library project, then adding a reference to Wix.dll (this is in C:\Program Files (x86)\WiX Toolset v3.9\bin on my machine, adjust as necessary for yours).

That gives you access to almost everything you'll need for our extension.

Next, we need to create the extension class.

In Wix, all extensions derive from WixExtension. A single wix extension can extend multiple different tools, for example the preprocessor, compiler, decompiler and harvester. For this tutorial, we will extend the compiler.

To create our wix extension, create a class called WixXmlSearchExtension and derive from Microsoft.Tools.WindowsInstallerXml.WixExtension.

The last thing we need to do is tell WiX which class in the class library is the WixExtension. This is done with an assembly attribute. Open up AssemblyInfo.cs and add [assembly: AssemblyDefaultWixExtension(typeof(Wix.XmlSearch.XmlSearchWixExtension))] to the file (you'll need to use the namespace Microsoft.Tools.WindowsInstallerXml for this to compile).

Step 2: Extending the Compiler

Our next goal is to extend the compiler so that it parses XMLSearch elements and puts entries into a custom table. We'll start by adding a compiler extension.

Create another new class, and derive this one from CompilerExtension. There are a couple of methods that need to be overridden to make this class work, we'll come back to them in a little bit.

We need to describe our new element to WiX, and we do this is a very standard manner - using xsd. This isn't an xsd tutorial, so I've pasted the xsd we will use below (in the source code, I have added documentation elements for each part - they are removed here for conciseness and clarity).

XML
<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:html="http://www.w3.org/1999/xhtml" 
xmlns:wix="http://schemas.microsoft.com/wix/2006/wi" 
xmlns:xs="http://www.w3.org/2001/XMLSchema" 
xmlns:xse="http://schemas.microsoft.com/wix/2005/XmlSchemaExtension" 
targetNamespace="http://schemas.rottedfrog.co.uk/wix/XmlSearch" 
xmlns="http://schemas.rottedfrog.co.uk/wix/XmlSearch">
  <xs:element name="XmlSearch">
    <xs:annotation>
      <xs:appinfo>
        <xse:parent namespace="http://schemas.microsoft.com/wix/2006/wi" ref="Property" />
      </xs:appinfo>
    </xs:annotation>
    <xs:complexType>
      <xs:attribute name="Id" use="required" type="xs:string" />
      <xs:attribute name="XPath" use="required" type="xs:string" />
      <xs:attribute name="File" use="required" type="xs:string" />
      <xs:attribute name="SelectionLanguage">
        <xs:simpleType>
          <xs:restriction base="xs:NMTOKEN">
            <xs:enumeration value="XPath" />
            <xs:enumeration value="XSLPattern" />
          </xs:restriction>
        </xs:simpleType>
      </xs:attribute>
    </xs:complexType>
  </xs:element>
</xs:schema>

The one important point in the schema is the annotation: xse:parent - it indicates to wix that the element is expected as a child of the Property element.

Now we have our schema we need to associate it with our extension. Firstly, we'll mark it as an embedded resource. Then we'll override the Schema property on our compiler extension and just loading the schema and returning it. With this, our class should look something like this:

C#
public class XmlSearchCompilerExtension : CompilerExtension
{
  XmlSchema _schema;

  public XmlSearchCompilerExtension()
  {
    _schema = XmlSchema.Read(System.Reflection.Assembly.GetExecutingAssembly().
    GetManifestResourceStream("Wix.XmlSearch.Resources.XmlSearch.xsd"), null);
  }

  public override System.Xml.Schema.XmlSchema Schema
  {
    get { return _schema; }
  }
}

Next, we need to add the specification for our custom table. We do this in XML, using an undocumented schema, supplied with the wix source code (look in WiX\src\wix\Xsd). Let's go through the XML for our table here.

XML
<?xml version="1.0" encoding="utf-8"?>
<tableDefinitions xmlns="http://schemas.microsoft.com/wix/2006/tables">
    <tableDefinition name="XmlSearch">
        <columnDefinition name="Id" type="string" 
        length="72" primaryKey="yes" modularize="column"
                category="identifier" 
                description="Primary key, non-localized token in table."/>
        <columnDefinition name="File" type="localized" 
        length="255" modularize="none"
                category="formatted" 
                description="Name of file to look in."/>
        <columnDefinition name="Property" type="string" 
        length="72" nullable="yes" 
        localizable="yes" modularize="property"
                category="property" 
                description="Property to store the value of the discovered xml node."/>
        <columnDefinition name="XPath" 
        type="string" length="0" modularize="none"
                category="formatted" 
                description="The XPath expression to the node 
                with the value that should be stored in the property."/>
        <columnDefinition name="Condition" 
        type="string" length="255" 
        modularize="condition" nullable="yes"
                category="condition" 
                description="Optional property that sets a condition 
                on when the xml value should be retrieved."/>
    </tableDefinition>
</tableDefinitions>

Each custom table is represented by a tableDefinition element, and each column within the table by columnDefinition elements. name and type are self explanatory, length is the length of the field in the MSI database - this can be up to 255. primaryKey specified which fields form the primary key - they must be the first fields in the list. modularize is used when creating merge modules - it specifies how/if the module id is appended to the field. category indicates what sort of data is stored in the field - this allows the ICE checks to validate the data in the column. We want to be able to use property values in the filename and xpath fields, so they are given a category of formatted. All the valid values for type, modularize and category are specified in the schema.

Writing the Custom Action

At this point, we will leave our add-in for a while, and start work on our custom action.

I always write my custom actions in C++, although I am aware of a variety of articles about how to do it in C#. I dislike the idea of .NET custom actions for several reasons:

  1. They introduce a dependency on a version of the .NET framework, which may not be installed (even .NET 2 is not default for some of the clients I've worked with recently).
  2. Spinning up the .NET runtime for a single custom action seems expensive and costly
  3. C++ is "closer to the metal" when dealing with MSI. C# relies on lots of messy Pinvoke signatures.
  4. I like C++.

If you've not written a C++ custom action before, I go through all the boiler plate required to set one up from scratch, and the C++ is not too difficult to follow, even for someone who doesn't use C++ regularly. I use my own simple framework for interacting with MSI when using custom actions, which I plan to cover in a subsequent article. It's fairly straightforward to see what is going on.

So to start, let's create a new blank C++ empty project in VS and add a new file called main.cpp. Then add the following to main.cpp:

C++
#include "msihelper.h"

UINT XmlSearch(MSIHANDLE hInstall)
{

}

This is the basic structure for a custom action. First, we include msihelper is a small framework that I wrote to wrap the MSI functions in a more C++ like way. It also includes the windows.h, msi.h and msiquery.h which are the headers we need - if you prefer the virgin approach. Every custom action has the same function signature - it takes a handle to the MSI package, and returns an unsigned integer, where 0 indicates success and any other value indicates a failure.

Next, we will set up the project options so that this compiles. For those of you who are C++ experts, this will seem patronizing, but it's not aimed at you, it's aimed at the people who don't do this sort of thing very regularly, so that they can get the setup right without having to spend ages trying to work out the right configurations and can just concentrate on the code instead, which is far more interesting.

Open the project options, and choose all configurations from the combo box at the top of the dialog - we want our changes to apply to both debug and release versions. Next go to Configuration Properties > General. Where it says configuration type, choose DLL. You may also want to change Character Set to "Use Unicode Character Set" - this sets some preprocessor definitions to enable Windows to use the unicode versions of functions by default. Now choose Linker > Input and add msi.lib to the list of additional dependencies. Finally go to the C/C++ > Code Generation page. You'll need to set this next option separately for Debug and Release Builds. For Debug, Runtime Library should be set to Multi-threaded Debug, and for Release it should be set to Multi-threaded. This means that the C++ library functions are compiled directly into the DLL rather that requiring a DLL to be installed before-hand. The side effect is that the DLL becomes bigger, but generally the code bloat is not large for simple custom action DLLs, and the benefits of reducing dependencies of the install are obvious.

The last bit of boilerplate code is adding a module definition file to the project. A module definition (.def) file is used to tell the compiler what functions are being exported (made public) from the DLL. There are other ways you can do this using the __declspec directive, but a .def file is the most quirk free method, so I have chosen that one to use. Our def file looks like this:

C++
LIBRARY "XmlSearch"

;Exported Custom Actions

EXPORTS
  XmlSearch

It's very simple - the LIBRARY statement just tells the linker we are creating a DLL, and EXPORTS starts a list of exported functions. The functions are exported with C-style function name decoration, rather than C++ name mangling. If you wish to add more custom actions to the DLL, just add their names to the bottom of the list.

Now onto the custom action proper. We will approach this by using a SQL statement to extract the rows from custom XmlSearch table we created earlier, then iterate through each row, and assign the value of the discovered node to the specified property. Very straightforward.

The one hiccup is that we need XML support. There currently is no native XML support in C++, which is very annoying, so I've plunked for using MSXML. I wanted to use to use MSXML or to use a statically linked library, pugixml, which is quick, low memory and very efficient. In the end, I went for MSXML, solely, because it is what WiX uses for its XmlFile and XmlConfig custom actions, and I felt that consistency with those was more important than a minimal dependency DLL. Notice that I've also included another header file, msihelper.h and also msihelper.cpp in the project as well. These provide object oriented wrappers around the most common custom action features in MSI. You can just get them as part of the source code download above.

So if we add the code to iterate through the SQL statement, it looks like the following:

C++
#include <windows.h>
#include <Msi.h>
#include <MsiQuery.h>
#include "msihelper.h" 

// Used for MSXML
#include <comutil.h>
#include <comdef.h>
#include <MsXml2.h>

const wchar_t *xmlSearchQuery = L"SELECT `Id`, 
`Property`, `File`, `XPath`, `Condition` FROM `XmlSearch`";

extern "C" UINT __stdcall XmlSearch(MSIHANDLE hMsi)
{
  MsiSession session(hMsi);

  try
  {
    CoInitialize(nullptr);
    MsiView view = session.OpenView(xmlSearchQuery);
    FindXmlValue f(session);
    for_each(begin(view), end(view), f);
    return f.errcode;
  }
  catch (msi_exception &err)
  {
    session.LogMessage(__FUNCTIONW__, err.what().c_str());
    return err.number();
  }
}

First of all, we define the xmlSearch query. All MSI queries are expressed in SQL. The syntax is basic and doesn't support a lot of common operations - check the MSDN documentation for more about the Windows installer query syntax. Notice the angled single quotation marks. These are the escape markers. Using this also you to use reserved words and some disallowed characters in the field names. The CoInitialize instruction is required before we can call any of the XML functions. The remainder of the code is looking through each row returned by the query, and executing the function object FindXmlValue, which does all the real work.

At this point is worth discussing error handling. I've wrapped my errors up in msi_exception and xml_exception class, and using the MsiLogging to write the error messages into the log. Custom Actions have to be robust - if they break, they can prevent your application from being installed, therefore it is important that you consider what could make your custom action break and handle any and all errors that may occur. Not only that, but decide whether they are errors that actually stop the installation from continuing or whether they are recoverable. You must be sure that any failures are logged. in the Msi log as well, so that you can easily find out where any failures are taking place. I will replace the error handling code with placeholder comments for brevity in the following code samples. Please check the download for the full code.

First, the part that does all the boring stuff - validating all the fields in the table. I also check for existence of the file as well. One thing to bear in mind, is that we are writing a search operation - not finding the file or the value in the file is not an error therefore shouldn't be marked as failure. However invalid fields are a failure condition and must be treated as such. Also worth noting that I have defined a smart pointer for accessing the XML interfaces using some handy COM macros. They are similar to shared_ptr, but use COM reference counting, and can also be implicitly cast down to the interface they represent. They have the same name as the interface they wrap, with the suffix Ptr.

C++
_COM_SMARTPTR_TYPEDEF(IXMLDOMDocument2, IID_IXMLDOMDocument2);
struct FindXmlValue
{
  mutable unsigned int errcode;
  mutable MsiSession &_session;

  FindXmlValue(MsiSession &session)
    : errcode(0), _session(session)
  { }

  bool FileExists(const wstring &file)
  { return GetFileAttributes(file.c_str()) != INVALID_FILE_ATTRIBUTES; }

  void operator()(MsiRecord &record) const
  {
    try
    {
      wstring id(record[1].GetString());
      wstring prop(record[2].GetString());
      wstring file(record[3].GetFormattedString());
      wstring xpath(record[4].GetFormattedString());
      wstring condition = record[5].GetString();
      if (!id.length())
      {
        _session.LogMessage(L"XmlSearch", 
        L"Missing Id value in XmlSearch Table");
        errcode = ERROR_INVALID_FIELD;
      }
        // else Check property is not empty
        // else Check XPath is not empty
        // else Check file is not empty
      else if ( !_session.EvaluateCondition(condition.c_str()))
      { _session.LogMessage(L"XmlSearch", 
      L"Condition %s evaluated as false; skipping XmlSearch", id.c_str()); }
      else if (FileExists(file))
      { 
        _session.Properties[prop] = GetXmlValue(file, xpath); 
        _session.LogMessage(L"XmlSearch", 
        L"(%s) Set value of %s to '%s'", id.c_str(), 
        prop.c_str(), static_cast<wstring>(_session.Properties[prop]).c_str());
      }
      else
      { _session.LogMessage(L"XmlSearch", 
      L"(%s) Could not find file '%s'", id.c_str(), file.c_str());}
    }
    catch (xml_exception &ex)
    { 
      _session.LogMessage(L"XmlSearch", ex.what().c_str()); 
      if (ex.fatal() && !errcode)
      { errcode = ex.number(); }
    }
    catch (msi_exception &ex)
    {
      _session.LogMessage(L"XmlSearch", ex.what().c_str());
      if (!errcode)
      { errcode = ex.number(); }
    }
  }
}; 

It's worth noting for people not used to using functor classes, in the above code that the keyword mutable means that the class-level variable is modifiable, even when the object is marked as constant.

The actual meat of our custom action is in the GetXmlValue method. We create an instance of the DomDocument class, which is available from MSXML 3.0 onwards, load the requested XML document, and then select the first node that matches the xpath expression specified and return the value of the node (if it is an attribute) or the text of the node (if it is an element). As before, the important part is the error handling.

C++
wstring GetXmlValue(const wstring &file, const wstring &xpath, bool usexpath) const
{
  IXMLDOMDocument2Ptr doc;
  VARIANT_BOOL loaded = VARIANT_FALSE;

  doc.CreateInstance(__uuidof(DOMDocument)); // Check for success
  doc->load(_variant_t(file.c_str()), &loaded); // Check for success
  
  if (!loaded)
  { throw xml_exception(wstring(L"Could not load file: ") + file); }
  
  if (usexpath)
  { 
    if (FAILED(doc->setProperty(_bstr_t(L"SelectionLanguage"), 
    _variant_t(L"XPath"))))
    { _session.LogMessage(L"XmlSearch", 
    L"Warning: Could not set selection language to XPath, trying with default selection language"); }
  }

  IXMLDOMNode *node = nullptr;
  doc->selectSingleNode(_bstr_t(xpath.c_str()), &node); 
  if (!node)
  { throw xml_exception(wstring(L"Could not find node matching xpath \"") + 
  xpath + L"\" in file " + file + L"."); }
    
  _bstr_t result;
  DOMNodeType type;
  node->get_nodeType(&type);
  if (type == NODE_ATTRIBUTE)
  { 
    variant_t v;
    hr = node->get_nodeValue(&v); // Check for success
    result = v; }
  }
  else
  { 
    BSTR text;
    node->get_text(&text); // Check for success
    result.Attach(text);
  }
  node->Release();
  return wstring(result);
}

Now with that done, it's worth talking about how to test the custom action. Writing unit tests for a custom action is quite difficult, as they are supposed to run as part of an install. I create a skeleton msi (wix is brilliant for this), with the minimum of code, maybe only installing a couple of files and registry entries.

Load a package during setup and teardown of the tests, and manually add the table entries that we need. Unit testing custom actions was a bind before doing this, and this makes it very testable, albeit with a bit of boilerplate and setup. However you can use the same skeleton MSI for testing any custom action. As for unit testing frameworks, if you are using VS2012 or VS2013, the one in there is actually quite good. If not, then I would recommend using boost::test, which is very good, or whatever your favourite test framework is. Focus your testing on making sure the error handling works. Most of the time, testing the custom action works when all the input is valid is fairly trivial, given there is no UI.

Linking the Custom Action to Your Add-in

Linking the custom action in requires a fragment. We embed this fragment in a WixLib along with the binary DLL. Here is our fragment:

XML
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
  <Fragment>
    <Binary Id="XmlSearchCa" 
    SourceFile="$(var.XmlSearch.CustomAction.TargetPath)" />
    <CustomAction Id="XmlSearch" 
    BinaryKey="XmlSearchCa" DllEntry="XmlSearch" 
    Execute="firstSequence" Return="check" 
    SuppressModularization="yes" />
    <InstallExecuteSequence>
      <Custom Action="XmlSearch" After="AppSearch" />
    </InstallExecuteSequence>
    <InstallUISequence>
      <Custom Action="XmlSearch" After="AppSearch" />
    </InstallUISequence>
  </Fragment>
</Wix>

This is a fairly standard link up for a custom action: we embed the DLL in the binary table, set up a CustomAction that calls the function in our DLL, and then add our custom action to the InstallUI and InstallExecute sequences so that it gets called at appropriate times.

By default, wix does not bundle binary files inside a wixlib. However, we want it embedded inside our wixlib so that we can bundle the whole thing as a single Wix extension assembly. Fortunately, wix has an option that allows us to do this. In the properties for the wix library project file, tick "Bind Files into the Library file" (equivalent to the lit command line option -bf). This will bundle the binary files into the lib.

The last step is getting our add-in to inject the fragment from the wixlib into our install. This is done by inserting a reference to the custom action - this is done in the core code that parses our XmlSearch elements, which is in the core of the extension, which we will cover next.

The Compiler Extension

The compiler extension brings everything together. It is the part that parses our custom XML and writes out the required entries in the wixobj so that everything gets bound together. Implementing the Compiler extension involves overriding two ParseElement overloads:

C#
public override void ParseElement(SourceLineNumberCollection sourceLineNumbers, 
System.Xml.XmlElement parentElement, System.Xml.XmlElement element, params string[] contextValues)
public override ComponentKeypathType ParseElement(SourceLineNumberCollection sourceLineNumbers, 
System.Xml.XmlElement parentElement, System.Xml.XmlElement element, 
ref string keyPath, params string[] contextValues)

In our case, we aren't interested in the keypath field, so we simply handle both cases in this by forwarding the non keyPath overload to the keyPath one with a null keyPath specified:

C#
    public override void ParseElement(SourceLineNumberCollection sourceLineNumbers, 
System.Xml.XmlElement parentElement, System.Xml.XmlElement element, params string[] contextValues)
    {
      string keyPath = null;
      ParseElement(sourceLineNumbers, parentElement, element, ref keyPath, contextValues);
    }

This leaves us with one function to implement. The implementation is fairly straightforward - we just until the XmlSearch element is found, and then check that it has an inner property element. If so then we parse the element, otherwise we raise an error. This should only pass us elements for the schema we registered at the beginning. If we get anything unexpected, we raise another error.

C#
    public override ComponentKeypathType ParseElement
(SourceLineNumberCollection sourceLineNumbers, System.Xml.XmlElement parentElement, 
System.Xml.XmlElement element, ref string keyPath, params string[] contextValues)
    {
      ComponentKeypathType keyType = ComponentKeypathType.None;
      switch (element.LocalName)
      {
        case "XmlSearch":
          switch (parentElement.LocalName)
            {
            case "Property":
              ParseXmlSearchElement(sourceLineNumbers, parentElement, element);
              break;
            default:
              this.Core.UnsupportedExtensionElement(parentElement, element);
              break;
            }
          break;
        default:
          this.Core.UnexpectedElement(parentElement, element);
          break;
      }
      return keyType;
    }

We raise errors by calling methods of Core - these give us standard Wix error messages, which in our case happen to be exactly what we need. However if you need to do custom messages, it's much more of a hassle. By design, all error messages are stored as localised format strings in resource files. In WiX itself, these are created in XML files, and a tool is used to generate both a resource file and wrapper classes around them. Unless you really like the message generation tool, the way to do it is to create resource strings and derive classes from WixErrorEventArgs. You need to use the SourceLineNumberCollection if you want to report line numbers for the elements that relate to your error messages. Calling them is the simple matter of throwing a WixException or calling Core.OnMessage(). Warnings and verbose logging is done in a similar way - but derive your classes from WixWarningEventArgs or WixVerboseEventArgs and use Core.OnMessage() to send them to the console.

Finally, we come to ParseXmlSearchElement. This is parsing our element and adding a row into our custom XmlSearch table. This is straightforward XML manipulation and error handling we discussed for the previous method. The only part of real interest is adding a row to the table, which is shown below:

C#
Row r = Core.CreateRow(sourceLineNumbers, "XmlSearch");
r[0] = id;
r[1] = file;
r[2] = property;
r[3] = xPath;
r[4] = condition;
this.Core.CreateWixSimpleReferenceRow
(sourceLineNumbers, "CustomAction", "XmlSearch");

We are using Core.CreateRow to create a row in the XmlSearch table, and populate it with the elements - the indexer gives us access to the cells from each column. The last line is important - CreateWixSimpleReferenceRow - this creates a reference to the XmlSearch custom action defined in our wixlib, and this is what makes wix include the fragment in our final generated binary.

Final Thoughts

This is by no means a complete reference to building a wix extension, but hopefully it will give you some insight into what is involved. The key deficiencies that I can see are:

  1. It doesn't integrate with the CCPSearch action
  2. Blank properties give a warning (unless marked as secure). Really it should be warningless where an xmlsearch element is specified. I suspect there is no way of changing this behaviour through an extension.

Any questions or thoughts on improving what I have written would be appreciated. Happy coding people!

Points of Interest

This was the first time I used Windows Installer SQL statements. They turned out to have lots of quirks and pitfalls and it's very sparsely documented, so I'll probably write another article on it when I have time.

You need to carefully manage the build order of your projects - there were a few times that I built the extension only to find that it had included an old version of the wixlib. This is easily rectified by making sure that the project dependencies for your solution are set correctly.

I had one weird error the first time I tested the custom action in release configuration. It turned out that it was due to the order that certain class level variables were declared in the MsiView class was not the same as the order they needed to be instantiated in. This is a classic C++ bug - read Scott Meyers Effective C++ for the reasons why this didn't work.

I found a few votive quirks that I needed to be careful of when working on this:

  1. Votive doesn't support referencing extensions by project - only by referencing the DLL. This makes sense (how do you know if what you are referencing should be installed input or is an extension), but may catch out the unwary.
  2. When you reference an extension DLL in a WiX project, it's held open by Visual Studio, which sometimes results in you being unable to build your extension if it's in the same solution.
  3. If you remove a reference to a custom action DLL, it deletes the DLL. Very bad!
  4. When rebuilding an install, not all the files produced in the obj folder are cleaned up first. We've had some awkward build quirks down to it not rebuilding certain files or cleaning them up when told.

License

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


Written By
Technical Lead
United Kingdom United Kingdom

A youthful, enthusiastic (albeit dead) amphibian coder writing software for a financial information firm based in the UK.



Website may come in the future, if I have time...


Comments and Discussions

 
QuestionPorting to new compiler and toolset? Pin
M-Schiller6-Aug-20 2:38
M-Schiller6-Aug-20 2:38 
QuestionHow use it in a bundle? Pin
marygrace24-Nov-15 22:51
marygrace24-Nov-15 22:51 
GeneralRe: How use it in a bundle? Pin
Rotted Frog9-Jun-16 5:15
Rotted Frog9-Jun-16 5:15 
QuestionSeveral Errors Pin
jeffdavis87035-Mar-15 7:02
jeffdavis87035-Mar-15 7:02 
BugLeaked MSIHANDLE in log Pin
dudelsack22-Jan-15 21:51
dudelsack22-Jan-15 21:51 
GeneralRe: Leaked MSIHANDLE in log Pin
Rotted Frog23-Jan-15 21:15
Rotted Frog23-Jan-15 21:15 
GeneralMy vote of 4 Pin
KarstenK16-Jan-15 2:34
mveKarstenK16-Jan-15 2:34 
GeneralRe: My vote of 4 Pin
Rotted Frog16-Jan-15 21:09
Rotted Frog16-Jan-15 21:09 

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

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