Extending WiX by creating a custom WiX Extension


Extending WiX

WiX facilitates plenty of use cases out of the box. However, there are tasks that just don’t come built into Windows Installer by default. Thankfully, the WiX team and others have extended WiX to cover a range of extra functionality such as installing websites, creating users, and editing XML files. In this chapter, we will cover how you can join in the fun by building your own extensions. This allows you to craft custom WiX elements that bind to custom actions to perform complex tasks, but stay consistent with the WiX declarative, XML style.

We will explore the following topics:

  • Hooking into the WiX extension model using classes from the Microsoft.Tools.WindowsInstallerXml namespace
  • Defining an XML schema for new WiX elements
  • Parsing those elements when they’re used in a WiX project and storing the result in the MSI database
  • Associating the elements with custom actions to be run at install time

Building a custom WiX extension

You’ve been exposed to several of the WiX extensions already. WixUIExtension adds a premade user interface. WixNetFxExtension gives you information about the version of .NET that’s installed. WixUtilExtension provides a number of elements for jobs such as adding users, editing XML files, and setting Internet shortcuts.

There are also other extensions that we haven’t covered, including WixSqlExtension that can set up an MSSQL database, WixIIsExtension for adding websites, app pools and virtual directories to IIS, and WixDifxAppExtension for installing Windows drivers. For more information about these extensions, check out the WiX documentation at http://wix.sourceforge.net/manual-wix3/schema_index.htm. In this chapter, you will learn to make your own extension and bend WiX to your will for fortune and glory.

To get started, let’s define what an extension is and what it would take to make one.

Setting the stage

A WiX extension is a .NET assembly that, when added to a WiX Setup project, provides new XML elements for additional functionality. As such we will need to create a new C# class library project and reference wix.dll from the WiX bin directory. For the example in this chapter I will name the project AwesomeExtension.

add-ref-1

Extending the CompilerExtension class

The first thing we’ll do is add a new class to our project and call it AwesomeCompiler. This class should extend the CompilerExtension class. Be sure to add a using statement targeting the Microsoft.Tools.WindowsInstallerXml namespace. We will immediately override a property called Schema that returns an instance of XmlSchema:

namespace AwesomeExtension
{
   using System.Reflection;
   using System.Xml;
   using System.Xml.Schema;
   using Microsoft.Tools.WindowsInstallerXml;

   public class AwesomeCompiler : CompilerExtension
   {
      private XmlSchema schema;

      public AwesomeCompiler()
      {
         this.schema =  
            CompilerExtension.LoadXmlSchemaHelper(
               Assembly.GetExecutingAssembly(),  
               "AwesomeExtension.AwesomeSchema.xsd");
      }

      public override XmlSchema Schema
      {
         get 
         {
            return this.schema; 
         }
      }
   }
}

The Schema property returns an object of type XmlSchema, which is an in-memory representation of an XSD file. The XSD file, which we’ll add to our project soon, will define the syntax for our custom XML elements such as the names of the elements, the attributes they can have, and where they can be placed in an XML document relative to other elements.

We are setting this property in the class’s constructor using a static method called LoadXmlSchemaHelper from the base class. The first parameter to this method is the currently executing assembly and the second is the name of the XSD file. We will be embedding the XSD file in the class library so to reference it you should prefix the file’s name with the project’s default namespace, such as AwesomeExtension.AwesomeSchema.xsd. This is assuming you place the XSD in the root folder of the project. If you decide to place it in a subfolder, include the name of that folder in the string. For example, if you place it in a folder called Schemas, you would reference AwesomeExtension.Schemas.AwesomeSchema.xsd. You can know for sure what to use by compiling your project and opening the outputted assembly with ILDASM, the .NET disassembler. There you can see the names of all embedded resources by looking inside the manifest.

In the next section, we’ll see how to make an XML schema and embed it in our assembly.

Adding an XML schema

Visual Studio comes with a template for adding an XSD file to your project. Right-click on your project and select Add | New Item | Data | XML Schema. In this example, I’ll be adding it to the root folder of the project and calling it AwesomeSchema.xsd.

create-schema-1

Once you have it, right-click on the file in Solution Explorer and select Properties. Change Build Action to Embedded Resource. Next, right click on the file again and choose View Code. You should see a schema element with various XML namespaces defined. I would modify it to use the XmlSchemaExtension namespace for defining parent-child relationships of elements. Also, change the domain name to use something other than tempuri.org.

<?xml version="1.0" encoding="utf-8"?>
<xs:schema 
   elementFormDefault="qualified"
   targetNamespace="http://www.mydomain.com/AwesomeSchema"
   xmlns="http://www.mydomain.com/AwesomeSchema"
   xmlns:xs="http://www.w3.org/2001/XMLSchema"
   xmlns:xse=
"http://schemas.microsoft.com/wix/2005/XmlSchemaExtension">
</xs:schema>

The next step is to define our custom WiX elements. These will go inside the schema element. Add the following markup:

<?xml version="1.0" encoding="utf-8"?>
<xs:schema ...>

  <xs:annotation>
    <xs:documentation>
      The schema for the Awesome WiX Extension
    </xs:documentation>
  </xs:annotation>

  <xs:element name="SuperElement">
    <xs:annotation>
      <xs:appinfo>
        <xse:parent namespace="http://schemas.microsoft.com/wix/2006/wi" ref="Product" />
        <xse:parent namespace="http://schemas.microsoft.com/wix/2006/wi" ref="Fragment" />
      </xs:appinfo>
      <xs:documentation>
        A custom element for declaring level of awesomeness.
      </xs:documentation>
    </xs:annotation>

    <xs:complexType>
      <xs:attribute name="Id"
                    use="required"
                    type="xs:string">
        <xs:annotation>
          <xs:documentation>The ID for the element.</xs:documentation>
        </xs:annotation>
      </xs:attribute>

      <xs:attribute name="Type" use="required">
        <xs:annotation>
          <xs:documentation>The type of awesomeness: Super, TotallySuper or RockStar.</xs:documentation>
        </xs:annotation>
        <xs:simpleType>
          <xs:restriction base="xs:string">
            <xs:enumeration value="Super" />
            <xs:enumeration value="TotallySuper" />
            <xs:enumeration value="RockStar" />
          </xs:restriction>
        </xs:simpleType>
      </xs:attribute>
    </xs:complexType>
  </xs:element>

</xs:schema>

Use the annotation and documentation elements to give helpful descriptions to your elements and attributes. We use them in various places in this example including at the top of the document to convey the purpose of the schema.

<xs:annotation>
    <xs:documentation>
      The schema for the Awesome WiX Extension
    </xs:documentation>
  </xs:annotation>

Use the appinfo and parent elements to define the WiX elements yours should be placed within. For example, a SuperElement should only be placed within a Product or Fragment element, as shown:

<xs:appinfo>
   <xse:parent namespace="http://schemas.microsoft.com/wix/2006/wi" ref="Product" />

   <xse:parent namespace="http://schemas.microsoft.com/wix/2006/wi" ref="Fragment" />
</xs:appinfo>

When defining attributes for your elements you can stick to the simple data types such as strings, shown here for the Id attribute:

<xs:attribute name="Id" 
              use="required"
              type="xs:string"/>

Alternatively, you can also build complex types such as enumerated values. In the example shown, we are defining an attribute called Type that can be set to Super, TotallySuper, or RockStar:

<xs:attribute name="Type" use="required">
   <xs:simpleType>
      <xs:restriction base="xs:string">
         <xs:enumeration value="Super" />
         <xs:enumeration value="TotallySuper" />
         <xs:enumeration value="RockStar" />
      </xs:restriction>
   </xs:simpleType>
</xs:attribute>

Now that we have our schema, let’s jump back to our AwesomeCompiler class and add a method for parsing our SuperElement element when someone uses it in a WiX project.

Parsing custom elements

Next, we need to add logic to our AwesomeCompiler class to parse our new element when it’s used. Override the ParseElement method from the base class:

public override void ParseElement(
   SourceLineNumberCollection sourceLineNumbers,
   XmlElement parentElement,
   XmlElement element,
   params string[] contextValues)
   {
      switch (parentElement.LocalName)
      {
         case "Product":
         case "Fragment":
            switch (element.LocalName)
            {
               case "SuperElement":
                  this.ParseSuperElement(element);
                  break;
               default:
                  this.Core.UnexpectedElement(
                     parentElement, 
                     element);
                  break;
            }
            break;
         default:
            this.Core.UnexpectedElement(
               parentElement, 
               element);
            break;
   }
}

When someone uses our extension in their WiX project and then compiles it, Candle will call the ParseElement method. Our override parses only SuperElements that are children to Product or Fragment elements. In your own extension, feel free to add more case statements for all of the elements you’ve defined in your schema. We’ll be calling a method called ParseSuperElement to handle the specific parsing logic. You should call the UnexpectedElement method if the element passed in isn’t recognized so that Candle will throw an error.

The ParseSuperElement method takes an XmlNode object—our SuperElement. The following is the code to add:

private void ParseSuperElement(XmlNode node)
{
   SourceLineNumberCollection sourceLineNumber =
      Preprocessor.GetSourceLineNumbers(node);

   string superElementId = null;
   string superElementType = null;

   foreach (XmlAttribute attribute in node.Attributes)
   {
      if (attribute.NamespaceURI.Length == 0 ||
          attribute.NamespaceURI == this.schema.TargetNamespace)
      {
         switch (attribute.LocalName)
         {
            case "Id":
               superElementId = this.Core.GetAttributeIdentifierValue(
                  sourceLineNumber,
                  attribute);
               break;
            case "Type":
               superElementType =
                  this.Core.GetAttributeValue(
                     sourceLineNumber,
                     attribute);
               break;
            default:
               this.Core.UnexpectedAttribute(
                  sourceLineNumber,
                  attribute);
               break;
          }
      }
      else
      {
          this.Core.UnsupportedExtensionAttribute(
             sourceLineNumber,
             attribute);
      }
   }

   if (string.IsNullOrEmpty(superElementId))
   {
        this.Core.OnMessage(
           WixErrors.ExpectedAttribute(
              sourceLineNumber,
              node.Name,
              "Id"));
    }

    if (string.IsNullOrEmpty(superElementType))
    {
         this.Core.OnMessage(
            WixErrors.ExpectedAttribute(
               sourceLineNumber,
               node.Name,
               "Type"));
     }

     if (!this.Core.EncounteredError)
     {
        Row superElementRow =
             this.Core.CreateRow(
                sourceLineNumber,
                "SuperElementTable");

         superElementRow[0] = superElementId;
         superElementRow[1] = superElementType;
      }
}

All parsing methods follow a similar structure. First, use the Preprocessor.GetSourceLineNumbers method to get a SourceLineNumberCollection object. This will be used in various places throughout the rest of the function.

private void ParseSuperElement(XmlNode node)
{
   SourceLineNumberCollection sourceLineNumber =
      Preprocessor.GetSourceLineNumbers(node);
}

Next, loop through each item in the node object’s Attributes collection to get each attribute that was set on our SuperElement. There are quite a few specialized functions for retrieving the value of different types of attributes, but the simplest is GetAttributeValue, which returns the value as a string. I’m also using GetAttributeIdentifierValue, which does some additional validation checks to make sure the Id attribute contains valid characters for an ID.

Call the UnexpectedAttribute method as a fallback in case an unrecognized attribute is used on our SuperElement. You should also call the UnsupportedExtensionAttribute method if an attribute is prefixed with a namespace that isn’t our schema’s target namespace.

foreach (XmlAttribute attribute in node.Attributes)
{
      if (attribute.NamespaceURI.Length == 0 ||
          attribute.NamespaceURI == this.schema.TargetNamespace)
      {
         switch (attribute.LocalName)
         {
            case "Id":
               superElementId = 
                this.Core.GetAttributeIdentifierValue(
                  sourceLineNumber,
                  attribute);
               break;
            case "Type":
               superElementType =
                  this.Core.GetAttributeValue(
                     sourceLineNumber,
                     attribute);
               break;
            default:
               this.Core.UnexpectedAttribute(
                  sourceLineNumber,
                  attribute);
               break;

          }
      }
      else
      {
          this.Core.UnsupportedExtensionAttribute(
             sourceLineNumber,
             attribute);
      }
}

After extracting each attribute’s value, we will do some validation to make sure all required attributes have been set. You can use the OnMessage method to have Candle display an error if the mandatory attribute is missing, as shown in the following code:

if (string.IsNullOrEmpty(superElementId))
{
   this.Core.OnMessage(
      WixErrors.ExpectedAttribute(
         sourceLineNumber,
         node.Name,
         "Id"));
}

if (string.IsNullOrEmpty(superElementType))
{
   this.Core.OnMessage(
      WixErrors.ExpectedAttribute(
         sourceLineNumber,
         node.Name,
         "Type"));
}

Finally, use the CreateRow method to add the data from the element to the MSI database. Given the name of the table you want to create and the source line number where the element was parsed, you’ll get a Row object that you can treat as an array. Each index in the array is associated with column in the row. In this example, we’re creating a row in a table called SuperElementTable and setting the first column to the element’s Id attribute and the second to its Type attribute, given as follows:

if (!this.Core.EncounteredError)
{
   Row superElementRow =
      this.Core.CreateRow(
         sourceLineNumber,
         "SuperElementTable");

   superElementRow[0] = superElementId;
   superElementRow[1] = superElementType;
}

Next, let’s dig into how to declare the structure of this table.

Creating a new MSI table

Assuming the WiX compiler can successfully parse our element, we need to define how that data is going to be stored in the MSI. This part is actually pretty simple. We just need to add an XML file that establishes the structure of a table in the MSI called SuperElementTable. Each SuperElement that’s used will be added as a row in this table.

Add an XML file to your class library project and call it TableDefinitions.xml. As we did for the XSD, set the Build Action option of this file to Embedded Resource. The root element will be called tableDefinitions and should reference the XML namespace http://schemas.microsoft.com/wix/2006/tables. Add the following markup:

<?xml version="1.0" encoding="utf-8" ?>
<tableDefinitions
   xmlns="http://schemas.microsoft.com/wix/2006/tables">

  <tableDefinition
     name="SuperElementTable"
     createSymbols="yes">

  </tableDefinition>

</tableDefinitions>

Inside the tableDefinitions element, we’ve added a tableDefinition element with a name attribute set to SuperElementTable. That’s what we’re calling the new table in the MSI. The createSymbols attribute should be set to yes. Next, add a columnDefinition element for each attribute defined by SuperElement. These will define the columns in the table. They are shown as follows:

Next, add a columnDefinition element for each attribute defined by SuperElement. These will define the columns in the table. They are shown as follows:

<?xml version="1.0" encoding="utf-8" ?>
<tableDefinitions
   xmlns="http://schemas.microsoft.com/wix/2006/tables">

  <tableDefinition
     name="SuperElementTable"
     createSymbols="yes">

    <columnDefinition
         name="Id"
         type="string"
         length="72"
         primaryKey="yes"
         category="identifier"
         description="Primary key for this element" />

    <columnDefinition
       name="Type"
       length="72"
       type="string"
       category="formatted"
       nullable="no"
       description="Type of SuperElement" />

  </tableDefinition>

</tableDefinitions>

Here we are adding columns for the Id and Type attributes. Marking the Id attribute as a primaryKey means that it can’t be duplicated by another row and that it will serve as the primary key on the table.

You can also mark a column as a foreign key by setting the keyTable attribute to the name of another table and keyColumn attribute to the number, counting from left to right, of the column on that table to reference. The WiX linker will do some validation on the foreign keys including checking that the referenced column exists and that the foreign key isn’t also a primary key. If you run into an error about modularization types, just make sure that your columnDefintion element sets an attribute called modularize to the same as it is on the referenced column. The error message will tell you what it has to be.

The type attribute can be set to one of the values described in the following table:

Column Type Meaning
string Column is a string.
localized Column is a localizable string.
number Column is a number.
Column Type Column is a binary stream.
preserved Column is a string that is preserved in transforms.

The category attribute can be set to a valid column data type. You can find a list in the section titled Column Data Types (Windows) in the MSI SDK documentation that came with WiX. A list can also be found at:

http://msdn.microsoft.com/en-us/library/windows/desktop/aa367869(v=vs.85).aspx

In our example, Id has a Category attribute of identifier, meaning that it can only contain ASCII characters, underscores, and periods. Type has a Category attribute of formatted, meaning that it can accept a WiX property value as well as a string. One last useful attribute: if you want to allow nulls in a column, set the nullable attribute to yes.

You can get a good idea about the syntax to use in your table definitions by looking at those defined by WiX. Download the source code and check out the src\wix\Data\tables.xml file.

Extending the WixExtension class

The next step is to add a class that extends the WixExtension class. The purpose of this class is to return an instance of our AwesomeCompiler and also our table definitions. Add a new class to your project and call it AwesomeWixExtension. It should override the CompilerExtension property to return an instance of our AwesomeCompiler class:

namespace AwesomeExtension
{
    Using System.Reflection;    
    using Microsoft.Tools.WindowsInstallerXml;

    public class AwesomeWixExtension : WixExtension
    {
        private CompilerExtension compilerExtension;

        public override CompilerExtension CompilerExtension
        {
            get
            {
                if (this.compilerExtension == null)
                {
                    this.compilerExtension =
                       new AwesomeCompiler();
                }

                return this.compilerExtension;
            }
        }

    }
}

Next, override the TableDefinitions property and use the LoadTableDefinitionHelper method to get the table definitions from our XML file:

private TableDefinitionCollection tableDefinitions;

public override TableDefinitionCollection TableDefinitions
{
   get
   {
      if (this.tableDefinitions == null)
      {
         this.tableDefinitions =
            WixExtension.LoadTableDefinitionHelper(
               Assembly.GetExecutingAssembly(),                      
               "AwesomeExtension.TableDefinitions.xml");
      }

      return this.tableDefinitions;
   }
}

One last thing to do: open your project’s AssemblyInfo.cs file and add the following attribute:

[assembly: AssemblyDefaultWixExtension(typeof(
   AwesomeExtension.AwesomeWixExtension))]

This will mark our new AwesomeWixExtension class as the default WiX extension in the assembly.

Using the extension in a WiX project

At this point, you could use your extension in a WiX project to get a feel for what it will do. So far, we’ve added code to parse SuperElement and store it in the MSI. Later on we will tie a custom action to SuperElement so that when someone uses it the action will be run during the installation. To use the extension, follow these steps:

  1. Copy the extension assembly and its dependencies to a WiX project.
  2. Add the extension as a project reference.
  3. Use the custom XML namespace in the WiX project’s Wix element.
  4. Add SuperElement to the markup and compile.

Step 1 is to copy the output from our AwesomeExtension to a folder in a WiX project. I’ll assume you’ve created a simple WiX project. A common strategy is to add a lib folder to the WiX project and copy the output files there:

add-extension-ref-1

Then, add AwesomeExtension.dll as a project reference:

add-extension-ref-2

You can accomplish the same thing from the command line by passing the –ext flag to Candle and Light, followed by the path to the assembly.

Now that we have a reference to the extension we can include our custom XML namespace. Change the Wix element in your main WiX source file so that it contains http://www.mydomain.com/AwesomeSchema.

<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi" xmlns:awesome="http://www.mydomain.com/AwesomeSchema">

You’re then able to use SuperElement, like so:

<awesome:SuperElement Id="super1" Type="Super" />

This element can be placed within the Product element or a Fragment element. Compile the WiX project and use Orca.exe to see that the custom table has been added:

orca-1

You should see a row in the table containing the data from SuperElement. If you’d like to see Visual Studio IntelliSense when typing the custom element, copy the AwesomeSchema.xsd file we created into Visual Studio’s Xml/Schemas folder. If using Visual Studio 2010, it can be found at C:\Program Files (x86)\Microsoft Visual Studio 10.0\Xml\Schemas. Close and re-open Visual Studio and you should see the parameter information pop up as you type:

using-the-extension-1

Tying a custom action to the custom element

Having a table full of rows in the MSI gets us halfway. The other half is defining a custom action that reads from that table and does something useful with the data at install time. The extensions that come with WiX already do all sorts of handy things, such as create SQL databases and tables, edit XML files, add users and set permissions, add websites to IIS, and so on. Your extension will add to this list, allowing an installer to do new, uncharted things! However, in this example we’ll keep it simple and only display a message box for each row in the SuperElementTable table.

This is going to be a three-step process. First, we’ll define the custom action in C#. Then, we’ll embed the custom action DLL inside a WiX library (.wixlib). Finally, we’ll embed that library inside our extension. In the same Visual Studio solution where we defined our extension, add a new project using the C# Custom Action Project template. Call it SuperElementActions.

Now, if we were going to perform our custom action during the immediate phase of the UI or Execute sequence, things would be pretty simple. Let’s assume, however, that we are making an action that changes the end users’ computer and so should only be run during the deferred stage of the Execute sequence. This gives us an extra challenge: we can’t read the MSI database during the deferred stage.

The trick is to read the database during the immediate phase, store the results in a property, and then read that property during the deferred phase. In short, we need two custom actions. The first will read the database and looks like the following code snippet:

namespace SuperElementActions
{
   using System;   
   using System.Collections.Generic;
   using Microsoft.Deployment.WindowsInstaller;

   public class CustomActions
   {
      [CustomAction]
      public static ActionResult ShowMessageImmediate(Session session)
      {    
         Database db = session.Database;                     

         try
         {
            View view = db.OpenView("SELECT `Id`, `Type` FROM `SuperElementTable`");
            view.Execute();

            CustomActionData data = new CustomActionData();

            foreach (Record row in view)
            {
               data[row["Id"].ToString()] = row["Type"].ToString();
            }

            session["ShowMessageDeferred"] = data.ToString();

            return ActionResult.Success;
         }
         catch (Exception ex)
         {
                session.Log(ex.Message);
                return ActionResult.Failure;
         }
         finally 
          {
            db.Close();
          }
        }
    }
}

In the ShowMessageImmediate method, we’re reading from the currently executing MSI database by accessing session.Database, calling OpenView on it to select the rows from our custom table, and then Execute on the view that’s returned. We then iterate over each row in the table by using a foreach statement on the view.

The data is stored in a new CustomActionData object. This class can be used like a hash table, so I’m using each SuperElement element’s Id attribute as the key and the Type attribute as the value. After we’ve set all of the key-value pairs in the hash table, we serialize it out to a new session property called ShowMessageDeferred. By naming the property that, a custom action with the same name can have access to the data. It’s a way of passing data from the immediate phase to the deferred phase.

The next step is to define the deferred custom action. Add a new method to the same class and call it ShowMessageDeferred. The following is the code:

[CustomAction]
public static ActionResult ShowMessageDeferred(Session session)
{
   try
   {
      CustomActionData data = session.CustomActionData;

      foreach (KeyValuePair<string, string> datum in data)
      {
         DisplayWarningMessage(
            session, 
            string.Format("{0} => {1}", datum.Key, datum.Value));
      }

      return ActionResult.Success;
   }
   catch (Exception ex)
   {
      session.Log(ex.Message);
      return ActionResult.Failure;
   }
}

private static void DisplayWarningMessage(Session session, string message)
{
   Record record = new Record(0);
   record[0] = message;
   session.Message(InstallMessage.Warning, record);
}

Here we’re reading from the CustomActionData property on the Session object to get the SuperElement data that was set by the other custom action. We use another foreach statement to iterate over each item and display it in a message box, using a small helper function called DisplayWarningMessage.

Now we can move on to storing these custom actions in a WiX library. The reason is so that we can schedule the custom actions using WiX markup. Add a new Setup Library Project template to your solution and call it AwesomeLibrary, as shown in the following screenshot:

add-new-project-1

The setup library will contain a file called Library.wxs. We can modify it to point to our C# custom actions:

<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
  <Fragment>
    <Binary Id="CA_DLL" 
            SourceFile="SuperElementActions.CA.dll" />

    <CustomAction Id="ShowMessageImmediate" 
                  BinaryKey="CA_DLL" 
                  DllEntry="ShowMessageImmediate" 
                  Execute="immediate" 
                  Return="check" />

    <CustomAction Id="ShowMessageDeferred" 
                  BinaryKey="CA_DLL" 
                  DllEntry="ShowMessageDeferred" 
                  Execute="deferred" 
                  Return="check" />

    <InstallExecuteSequence>
       <Custom Action="ShowMessageImmediate"  
               Before="ShowMessageDeferred">
          NOT Installed</Custom>

       <Custom Action="ShowMessageDeferred" 
               After="InstallInitialize">
          NOT Installed</Custom>
    </InstallExecuteSequence>
  </Fragment>
</Wix>

Note that the Binary element references a DLL called SuperElementActions.CA.dll. The easiest way to make that true is to add the custom actions project as a reference in the AwesomeLibrary project. Then, use preprocessor variables to reference the DLL from that project’s output folder. Update the Binary element in the following manner:

<Binary Id="CA_DLL"  
        SourceFile=
"$(var.SuperElementActions.TargetDir)SuperElementActions.CA.dll" />

The two custom actions are both scheduled to run during InstallExecuteSequence, but the first will run during the immediate phase and the other during the deferred phase. I’ve added a condition to each Custom element so that these actions will only run during an install and not during an uninstall or repair. Because we don’t want to have to deploy our custom actions’ DLL separately from our extension, we should embed it inside the WIXLIB. To do so you can add the -bf flag to the Librarian settings of the project’s properties, as shown in the following screenshot:

librarian-settings-1

Alternatively, check the box labelled Bind files into the library file on the properties’ Build page:

linking-together-1

Copy the output from this .wixlib project to the AwesomeExtension project’s folder. You’ll probably want to use a post-build step in Visual Studio to do this. Add the AwesomeLibrary.wixlib file to the AwesomeExtension project using the Solution Explorer window and change its Build Action value to Embedded Resource.You should now have three projects: AwesomeExtension, AwesomeLibrary, and SuperElementActions, as shown in the following screenshot:

linking-together-2

I’ll summarize the steps we take to link them together:

  • Use a project reference to SuperElementActions in the AwesomeLibrary project and preprocessor variables to include SuperElementActions.CA.dll in a Binary variable.
  • Copy AwesomeLibrary.wixlib to the AwesomeExtension project’s folder. Consider using a post-build action to do this.
  • Add the AwesomeLibrary.wixlib file to the AwesomeExtension project, and change its Build Action value to Embedded Resource.

We’ll need to add another method to the AwesomeWixExtension.cs file called GetLibrary. This will retrieve the WiX library from the extension. The following is the code to add:

private Library library;

public override Library GetLibrary(
   TableDefinitionCollection tableDefinitions)
{
   if (this.library == null)
   {
      this.library = 
         WixExtension.LoadLibraryHelper(
            Assembly.GetExecutingAssembly(), 
            "AwesomeExtension.AwesomeLibrary.wixlib", 
             tableDefinitions);
   }

   return this.library;
}

The LoadLibraryHelper method takes the currently executing assembly, the name of the embedded WIXLIB, and the table definitions collection that was passed into the method and returns the library.

Next, add a call to CreateWixSimpleReferenceRow to the end of the ParseSuperElement method. This will create a relationship between our WIXLIB and the MSI, sort of like pulling a Fragment element into a project, by referencing one of the elements in the library. I’m referencing the ShowMessageImmediate action from the CustomAction table here to make this link:

this.Core.CreateWixSimpleReferenceRow(
   sourceLineNumber, 
   "CustomAction", 
   "ShowMessageImmediate");

Now when you use SuperElement in a WiX project, the ShowMessageImmediate and ShowMessageDeferred custom actions will be added to the CustomAction table in the MSI. All other elements in the WIXLIB will also be pulled in.

When you install the MSI you will see a message displayed for each SuperElement, as shown in the following screenshot:

message-1

Summary

In this chapter, we discussed how to extend WiX to perform custom operations at install time. Our extension provided new WiX elements that tie to custom actions. All of this is bundled up into a .NET assembly so that others can use it too. With this knowledge, you’ve opened the door to advanced WiX functionality for yourself and possibly the WiX community at large.

In the next chapter, we will have a look at the new Burn engine that was introduced in WiX 3.6. Burn provides bootstrapping capabilities that allow us to bundle our software and its dependencies into a single setup executable. The potential uses for this are many and we’ll dig into what you need to know to get started.

, , , ,

  1. No comments yet.
(will not be published)