Click here to Skip to main content
Click here to Skip to main content

Wrapping the Windows Installer 2.0 API

, 4 Jan 2004
Rate this:
Please Sign up or sign in to vote.
An article describing wrapping the Windows Installer 2.0 API using C# and .NET interop.

Introduction

A project of mine required a setup application which could handle installation of Microsoft® Windows Installer .MSI packages using a custom user interface. The interface would be able to handle progress messages, etc. from the Windows Installer Service. There was one solution at Youseful, however it wasn't a complete enough solution for my needs.

So I wrote my own wrapper, and this article describes how to use it.

This article will not describe the full Windows Installer API, nor the nuances involved in its use, nor .NET Interop.

Please refer to the Microsoft® Windows Installer Reference for further information. The accompanying source code contains comments from the MSDN library, but it is far from a complete guide.

The accompanying source code is distributed under the GNU Lesser License Version 2.1.

The Wrapper

The Windows Installer Wrapper provided here, wraps all API calls in the MsiInterop class in the WindowsInstaller namespace. Supporting structures, delegates, constants and enumerations are also provided. The interop class as well as these constructs are marked internal, the rational being the contents of this namespace are meant to be used within an assembly, perhaps wrapped by a publicly visible object. Of course, you are free to change the namespace as you see fit.

There was a period of trial and error in defining just what the interop signatures would be, and I had to tweak and rebuild (along with some UI test harnesses) to get things to work happily. No doubt there will be more tweaking in the future as people use the wrapper.

All Win32 HANDLES are IntPtrs in the wrapper.

Where possible, the return values are MsiErrors, which map to both MSI-specific as well as Win32 error codes.

Constants are also provided for the MSI database tables (in the MsiDatabaseTable class) as well as Windows Installer properties (in the MsiInstallerProperty class.)

It should be noted that this wrapper is intended for systems running Microsoft® Windows® 2000 or higher, with Windows Installer 2.0 installed.

Using MsiInterop for Custom UI Progress Messages

In order for you to circumvent the Windows Installer internal user interface, you must first disable it using a call to MsiInterop.MsiSetInternalUI, then tell the service about your own, by calling MsiInterop.MsiSetExternalUI, providing it with your MsiInstallUIHandler delegate for handling the UI messages.

The following code describes an "external UI" scenario, using code below (see The Delegate):

IntPtr parent = IntPtr.Zero;
MsiInstallUILevel oldLevel = 
  MsiInterop.MsiSetInternalUI(MsiInstallUILevel.None | 
  MsiInstallUILevel.SourceResOnly, ref parent);
MsiInstallUIHandler   oldHandler = null;

try
{
   oldHandler = 
     MsiInterop.MsiSetExternalUI(new 
     MsiInstallUIHandler(_OnExternalUI), 
     MsiInstallLogMode.ExternalUI, IntPtr.Zero);

   Application.DoEvents();

   MsiError ret = 
     MsiInterop.MsiInstallProduct(/*  path to .msi  */, 
     /*   command line args   */);

   if (ret != MsiError.Success)
      throw new 
      ApplicationException(string.Format("Failed to install -- {0}", ret));
}
catch (Exception e)
{
   Debug.WriteLine("EXCEPTION -- " + e.ToString());
   //   do something meaningful
}
finally
{
   if (oldHandler != null)
      MsiInterop.MsiSetExternalUI(oldHandler, 
        MsiInstallLogMode.None, IntPtr.Zero);

   MsiInterop.MsiSetInternalUI(oldLevel, ref parent);
}

Notice how a try/catch/finally is used to ensure that we clean up after ourselves!

The MsiInstallLogMode.ExternalUI is provided in the wrapper source code as a convenient enumeration value, for commonly-used logging modes for external user interfaces. Of course, you use and roll your own bitwise-OR*ed MsiInstallLogMode value, however it should be noted that the MsiInstallLogMode.ResolveSource cannot be handled by an external UI; the delegate code below handles it properly be returning 0, indicating the external UI did not handle the request.

The Delegate

The delegate used for callbacks from the Windows Installer API has the following signature:

internal delegate int MsiInstallUIHandler(IntPtr context, 
   uint messageType, [MarshalAs(UnmanagedType.LPTStr)] string message);

Your call to MsiInterop.SetExternalUI can provide a context which can be used to help you with UI state. This context can be extracted using the Marshal.PtrToStructure or a similar method (assuming you used something like Marshal.StructureToPtr to create the beast).

The following code shows an example MsiInstallUIHandler:

private int _OnExternalUI(IntPtr context, uint messageType, string message)
{
   MsiInstallMessage msg = 
     (MsiInstallMessage)(MsiInterop.MessageTypeMask & messageType);

   Debug.WriteLine(string.Format("MSI:  {0} {1}", msg, message));

   try
   {
      switch (msg)
      {
         case MsiInstallMessage.ActionData:
            //   set a label's text to the message

            Application.DoEvents();

            return (int)DialogResult.OK;

         case MsiInstallMessage.ActionStart:
            //   set a label's text to the message, with the
            //   message.Substring(message.LastIndexOf(".") + 1);
            //   being the action start description
         
            Application.DoEvents();

            return (int)DialogResult.OK;

         case MsiInstallMessage.CommonData:
            string[] data = _ParseCommonData(message);

            if (data != null && data[0] != null)
            {
               switch (data[0][0])
               {
                  case   '0':   //   language
                     break;

                  case   '1':   //   caption
                     //   store data[1] for dialog captions

                     break;

                  case   '2':   //   CancelShow
                     if ("0" == data[1])
                        //   hide / disable the "cancel" button
                     else
                        //   show / enable the cancel button

                     break;

                  default:   break;
               }
            }

            Application.DoEvents();

            return (int)DialogResult.OK;

         case   MsiInstallMessage.Error:
            return (int)MessageBox.Show(message,
               "Error", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);

         case   MsiInstallMessage.FatalExit:
            return (int)MessageBox.Show(message,
               "Fatal Error", MessageBoxButtons.OK, MessageBoxIcon.Error);

         case   MsiInstallMessage.FilesInUse:
            //   display in use files in a dialog, informing the user
            //   that they should close whatever applications are using
            //   them.  You must return the DialogResult to the service
            //   if displayed.

            Application.DoEvents();

            return 0;   //   we didn't handle it in this case!

         case   MsiInstallMessage.Info:
            Application.DoEvents();

            return (int)DialogResult.OK;

         case   MsiInstallMessage.Initialize:
            Application.DoEvents();

            return (int)DialogResult.OK;

         case   MsiInstallMessage.OutOfDiskSpace:
            Application.DoEvents();

            break;

         case   MsiInstallMessage.Progress:
            string[]   fields = _ParseProgressString(message);

            if (null == fields || null == fields[0])
            {
               Application.DoEvents();

               return (int)DialogResult.OK;
            }

            switch (fields[0][0])
            {
               case   '0':   //   reset progress bar
                  //   1 = total, 2 = direction , 3 = in progress, 4 = state

                  break;

               case   '1':   //   action info
                  //   1 = # ticks for the step size, 2 = actuall step it?

                  break;

               case   '2':   //   progress
                  //   1 = how far the progress bar moved,
                  //   forward / backward, based on case '0'

                  break;

               default:   break;
            }
         
            Application.DoEvents();

            if (/*  the user cancelled */)
               return (int)DialogResult.Cancel;
            else
               return (int)DialogResult.OK;

         case   MsiInstallMessage.ResolveSource:
            Application.DoEvents();

            return 0;

         case   MsiInstallMessage.ShowDialog:
            Application.DoEvents();

            return (int)DialogResult.OK;

         case   MsiInstallMessage.Terminate:
            Application.DoEvents();

            return (int)DialogResult.OK;

         case   MsiInstallMessage.User:
            //   get message, parse

            Application.DoEvents();

            return (int)DialogResult.OK;

         case   MsiInstallMessage.Warning:
            return (int)MessageBox.Show(message,
               "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning);

         default:   break;
      }
   }
   catch (Exception e)
   {
      //   do something meaningful, but don't rethrow here.
      Debug.WriteLine("EXCEPTION -- " + e.ToString());
   }

   Application.DoEvents();

   return 0;
}

In order to get what MsiInstallMessage type the message is, you have to bitwise-AND it with MsiInterop.MessageTypeMask as shown above. The switch block handles the individual message types.

We wrap the whole activity in a try/catch block, to ensure we let the service run through, returning 0 to let it know we didn't handle the problem. If an exception gets thrown, the service fails the installation activity.

The spurious Application.DoEvents in there allows your application's message pump to run, and the return of the DialogResults tells the service that the message was handled. The functions to crack the message for MsiInstallMessage.CommonData and MsiInstallMessage.Progress are below:

private string[]   _ParseCommonData(string s)
{
   string[]   res = new string[3];
   Regex   regex = new Regex(@"\d:\w+\s");
   int   i = 0;

   foreach (Match m in regex.Matches(s))
   {
      if (i > 3)   return null;

      res[i++] = m.Value.Substring(m.Value.IndexOf(":") + 1).Trim();
   }

   return res;
}

private string[]   _ParseProgressString(string s)
{
   string[]   res = new string[4];
   Regex   regex = new Regex(@"\d:\s\d+\s");
   int   i = 0;

   foreach (Match m in regex.Matches(s))
   {
      if (i > 4)   return null;

      res[i++] = m.Value.Substring(m.Value.IndexOf(":") + 2).Trim();
   }

   return res;
}

The actual meanings of these "cracked" messages can be derived by referring to the Parsing Windows Installer Messages in the MSDN library.

Other Uses for MsiInterop

Since I've wrapped the complete (well, as complete as I can muster) Windows Installer API, the API becomes quite useful in the .NET world. You may find an interesting use, or even a problem with my wrapper! Let me know! It's my hope that this code will be useful.

Workspace

This code is maintained in a GotDotNet workspace.

References

Please feel free to browse the following reference material:

History

Revisions:

  • 2004-01-05: Initial revision.
  • 2004-01-06: Slight code revision.

License

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

About the Author

ian mariano
Architect
United States United States
Summary
 
Project Manager and Software Architect with 20+ years of experience in the software development field. Broad experience in delivering robust and scalable information solutions in a variety of horizontal and vertical markets including media, finance, supply chain and health care.
 
Specialties
 
Current emphasis on Digital Content Solutions, Service Orientation, User Experience (UX) and Distributed Large Scale Software Patterns. Experience in managing teams of developers and enforcing compliance with industry standards.
 
ianmariano.com
Follow on   Twitter   Google+

Comments and Discussions

 
GeneralMy vote of 4 PinprofessionalAmir Mohammad Nasrollahi14-Aug-13 23:44 
GeneralMy vote of 1 Pinmemberomeriko928-Aug-12 4:42 
BugLicense mismatch on website vs files Pinmemberomeriko928-Aug-12 4:39 
GeneralHow to kill running instances of app to be uninstalled Pinmemberdsikic25-May-11 5:28 
QuestionHow to make CRC check stronger PinmemberCrazy Kiya re26-Dec-10 17:36 
GeneralInstalling on Vista PinmemberNate Anderson30-Oct-09 9:29 
AnswerRe: Installing on Vista Pinmemberian mariano2-Nov-09 2:40 
QuestionAdd files to a Msi installer PinmemberRadu_209-Apr-08 3:12 
QuestionCancel Event Pinmemberswissnik20-Feb-06 0:12 
GeneralProgrammatic install of Installer... PinmemberSave My Soul - (SMS)27-Dec-05 20:33 
GeneralRe: Programmatic install of Installer... PinmemberLalit N Dubey19-Aug-07 23:21 
Generalbug in MsiRecordGetString PinmemberAlbertoFujimori6-Sep-05 2:11 
AnswerRe: bug in MsiRecordGetString Pinmemberian mariano6-Sep-05 2:45 
GeneralNull Refernce Exception PinmemberKVerma18-Aug-05 4:20 
GeneralRe: Null Refernce Exception Pinsusshaof3-Sep-05 19:40 
QuestionRe: Null Refernce Exception Pinmemberm90034914-Nov-12 0:54 
GeneralBetter solution ))) PinmemberValery_Minsk15-Apr-05 0:48 
GeneralRe: Better solution ))) PinmemberSchenz4-Nov-05 17:14 
QuestionProblem in RegEx? Pinmemberwasawasa19-Feb-05 1:38 
GeneralExternal UI & Maintenance Mode Pinmemberjnanneng6-Jan-05 15:09 
GeneralRe: External UI & Maintenance Mode PinmemberAlcedoSoftware5-Jun-05 3:50 
GeneralSession Object PinmemberMikeOliszewski14-Mar-04 9:05 
GeneralRe: Session Object Pinmemberian mariano14-Mar-04 9:41 
GeneralRe: Session Object PinmemberMikeOliszewski14-Mar-04 10:02 
GeneralRe: Session Object PinmemberMikeOliszewski14-Mar-04 18:05 
GeneralRe: Session Object PinmemberMikeOliszewski15-Mar-04 8:07 
GeneralRe: Session Object Pinmemberian mariano15-Mar-04 12:03 

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

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

| Advertise | Privacy | Mobile
Web01 | 2.8.140721.1 | Last Updated 5 Jan 2004
Article Copyright 2004 by ian mariano
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid