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

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());
                    }
                }
            }
        }
    }
}

6 comments:

Lambert said...

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?

Lambert said...

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?

Christopher Painter said...

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

Lambert said...

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?

Rama Charan said...

Thanks for the post it helped me.

Can you share any references on how to merge 2 cubfiles.

Rama Charan said...

Thanks for post it helped me.

Can you share any references or let me know how to merge 2 cub files or modify\add to existing cub files using the C# code.

I need to keep adding to existing cub file at a predefiend location when i build your solution with my own tests added.