ISWIX, LLC View Christopher Painter's profile on LinkedIn profile for Christopher Painter at Stack Overflow, Q&A for professional and enthusiast programmers

Sunday, February 15, 2009

MSI Tip: Authoring an ICE using C# / DTF

I once wrote an article for InstallShield entitled "MSI Tip: Authoring a Custom ICE using InstallShield 2008". The article can be found here. [Warning: PDF]
In the article, I demonstrated how to write a unit test that could find evil script custom actions. While there is value and humor in doing so, the real point of the article was to demonstrate how InstallShield's refactored InstallScript language could be used to write ICEs.

While InstallScript is certainly an improvement over C++ in terms of cutting down on the line noise and complexity, the difference between C++/InstallScript and C# is nothing short of amazing. As such, I've created a sample project demonstrating how to author ICE's using C#/DTF and Stefan Krueger of InstallSite.org is kind enough to host it for me here.

Consider the following snippet from an MSDN sample demonstrating how to write an ICE in C++:


#include <windows.h>
#include <stdio.h>
#include <tchar.h>
#include <strsafe.h>
#include <MsiQuery.h>
 
///////////////////////////////////////////////////////////
// ICE01 - simple ICE that does not test anything
UINT __stdcall ICE01(MSIHANDLE hInstall)
{
// setup the record to describe owner and date created
PMSIHANDLE hRecCreated = ::MsiCreateRecord(1);
::MsiRecordSetString(hRecCreated, 0, TEXT("ICE01\t3\tCreated 04/29/1998 by <insert author's name here>"));
 
// post the owner message
::MsiProcessMessage(hInstall, INSTALLMESSAGE(INSTALLMESSAGE_USER), hRecCreated); 
// setup the record to describe the last time the ICE was modified
::MsiRecordSetString(hRecCreated, 0, TEXT("ICE01\t3\tLast modified 05/06/1998 by <insert author's name here>"));
 
// post the last modification message
::MsiProcessMessage(hInstall, INSTALLMESSAGE(INSTALLMESSAGE_USER), hRecCreated);
 
// setup the record to describe what the ICE evaluates
::MsiRecordSetString(hRecCreated, 0, TEXT("ICE01\t3\tSimple ICE illustrating the ICE concept"));
 
// post the description of evaluation message
::MsiProcessMessage(hInstall, INSTALLMESSAGE(INSTALLMESSAGE_USER), hRecCreated);
// time value to be sent on
TCHAR szValue[200];
DWORD cchValue = sizeof(szValue)/sizeof(TCHAR);
 
// try to get the time of this call
if (MsiGetProperty(hInstall, TEXT("Time"), szValue, &cchValue) != ERROR_SUCCESS)
StringCchCopy(szValue,  sizeof("(none)")/sizeof(TCHAR)+1, TEXT("none"));// no time available
 
// setup the record to be sent as a message
PMSIHANDLE hRecTime = ::MsiCreateRecord(2);
::MsiRecordSetString(hRecTime, 0, TEXT("ICE01\t3\tCalled at [1]."));
::MsiRecordSetString(hRecTime, 1, szValue);
 
// send the time
::MsiProcessMessage(hInstall, INSTALLMESSAGE(INSTALLMESSAGE_USER), hRecTime);
 
return ERROR_SUCCESS; // allows other ICEs will continue
}

With DTF and a little OOP ( Object Oriented Programming ) this can be reduced to the following snippet in C# / DTF:

using DE.TestFramework;
using Microsoft.Deployment.WindowsInstaller;
 
namespace DE.Tests
{
    partial class ICE01 : TestBase
    {
        [ICETest]
        public void TestExample()
        {
            Publish( ICELevel.Information, "Created 04/29/1998 by <insert author's name here>");
            Publish( ICELevel.Information, "Last modified 05/06/1998 by <insert author's name here>");
            Publish( ICELevel.Information, "Simple ICE illustrating the ICE concept");
            Publish( ICELevel.Information, "Called at " + Session["Time"] );
        }
    }
}
 
That may not seem like a big deal but consider the flexibity a few overloads can provide:

public void Publish(ICELevel iceLevel, string Description)
public void Publish(ICELevel iceLevel, string Description, string HelpLocation )
public void Publish(ICELevel iceLevel, string Description, string HelpLocation, string Table, string Column, string PrimaryKey)
public void Publish(ICELevel iceLevel, string Description, string HelpLocation, string Table, string Column, string[] PrimaryKeys)

Of course the real power comes from DTF's awesome interop classes. Going back to example the of detecting script custom actions in InstallScript, here is what it would look like in C# / DTF:

using DE.TestFramework;
using Microsoft.Deployment.WindowsInstaller;
using System;
 
namespace DE.Tests
{
    partial class ICE_DE_10
    {
        [ICETest]
        public void TestForScriptCustomActionsAreEvil()
        {
            Publish(ICELevel.Information, "Searching for Evil Script Custom Actions...");
 
            using (View view = Session.Database.OpenView("SELECT `CustomAction`.`Action`, `CustomAction`.`Type` FROM `CustomAction`"))
            {
                view.Execute();
                foreach (var record in view)
                {
                    string customActionName = record.GetString("Action");
                    int type = record.GetInteger("Type") & 0x00000007;
                    CustomActionTypes customActionType = (CustomActionTypes)type;
 
 
                    if (customActionType.Equals(CustomActionTypes.VBScript) || customActionType.Equals(CustomActionTypes.JScript))
                    {
                        Publish(ICELevel.Error, 
                            "Found Evil " + customActionType.ToString() + " Custom Action " + customActionName,
                            "http://blogs.msdn.com/robmen/archive/2004/05/20/136530.aspx",
                            "CustomAction",
                            "Action",
                            customActionName ); 
                    }
                    else
                    {
                        Publish(ICELevel.Information, "Found Nice Custom Action: " + customActionName + " Type: " + customActionType.ToString());
                    }
                }
            }
        }
    }
}

4 comments:

  1. I am writing custom ICE using DTF (WIX 3.5).I use C# custom action project to build the custom action and I stream that into a cub file.
    In your sample I saw you use “ DE.TestFramework “. I don’t find it in DTF? Could please show how to use DE.TestFramework?

    ReplyDelete
  2. I am writing custom ICE using DTF (WIX 3.5).I use C# custom action project to build the custom action and I stream that into a cub file.
    In your sample I saw you use “ DE.TestFramework “. I don’t find it in DTF? Could please show how to use DE.TestFramework?

    ReplyDelete
  3. Follow the link to the ZIP at InstallSite.org for a sample solution that has the namespace and class you are looking for.

    ReplyDelete
  4. Hi,
    I have one more question, how to give hyperlink along with ICE message. I have tried the MSDN sample http://msdn.microsoft.com/en-us/library/aa371380(v=VS.85).aspx, but even from the sample itself I don’t get the hyperlink. Any idea?

    ReplyDelete