Click here to Skip to main content
15,885,309 members
Articles / Programming Languages / C#
Article

Wrapping the Windows Installer 2.0 API

Rate me:
Please Sign up or sign in to vote.
3.44/5 (15 votes)
4 Jan 2004CPOL4 min read 229.1K   3.6K   67   29
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):

C#
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:

C#
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:

C#
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:

C#
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)


Written By
Chief Technology Officer
United States United States
20+ years as a strategist at the intersection of business, design and technology.

Comments and Discussions

 
GeneralMy vote of 1 Pin
Member 1182186113-Jul-15 21:16
Member 1182186113-Jul-15 21:16 
GeneralMy vote of 4 Pin
Amir Mohammad Nasrollahi14-Aug-13 23:44
professionalAmir Mohammad Nasrollahi14-Aug-13 23:44 
GeneralMy vote of 1 Pin
omeriko928-Aug-12 4:42
omeriko928-Aug-12 4:42 
BugLicense mismatch on website vs files Pin
omeriko928-Aug-12 4:39
omeriko928-Aug-12 4:39 
GeneralHow to kill running instances of app to be uninstalled Pin
dsikic25-May-11 5:28
dsikic25-May-11 5:28 
QuestionHow to make CRC check stronger Pin
Crazy Kiya re26-Dec-10 17:36
Crazy Kiya re26-Dec-10 17:36 
GeneralInstalling on Vista Pin
Nate Anderson30-Oct-09 9:29
Nate Anderson30-Oct-09 9:29 
AnswerRe: Installing on Vista Pin
ian mariano2-Nov-09 2:40
ian mariano2-Nov-09 2:40 
Hi,

Basically you have to handle UAC correctly.

You can embed a manifest[^] as part of your installer which requires elevation or restart the process[^] as an elevated user.

There are many examples out there on the 'net and on CodeProject[^]

Ian Mariano - www.ianmariano.com


QuestionAdd files to a Msi installer Pin
Radu_209-Apr-08 3:12
Radu_209-Apr-08 3:12 
QuestionCancel Event Pin
swissnik20-Feb-06 0:12
swissnik20-Feb-06 0:12 
AnswerRe: Cancel Event Pin
Séamus Haughian28-Jul-19 0:47
Séamus Haughian28-Jul-19 0:47 
GeneralProgrammatic install of Installer... Pin
HakunaMatada27-Dec-05 20:33
HakunaMatada27-Dec-05 20:33 
GeneralRe: Programmatic install of Installer... Pin
Lalit N Dubey19-Aug-07 23:21
Lalit N Dubey19-Aug-07 23:21 
Generalbug in MsiRecordGetString Pin
AlbertoFujimori6-Sep-05 2:11
AlbertoFujimori6-Sep-05 2:11 
AnswerRe: bug in MsiRecordGetString Pin
ian mariano6-Sep-05 2:45
ian mariano6-Sep-05 2:45 
GeneralNull Refernce Exception Pin
KVerma18-Aug-05 4:20
KVerma18-Aug-05 4:20 
GeneralRe: Null Refernce Exception Pin
ti40173-Sep-05 19:40
ti40173-Sep-05 19:40 
QuestionRe: Null Refernce Exception Pin
m90034914-Nov-12 0:54
m90034914-Nov-12 0:54 
GeneralBetter solution ))) Pin
Valery_Minsk15-Apr-05 0:48
Valery_Minsk15-Apr-05 0:48 
GeneralRe: Better solution ))) Pin
Schenz4-Nov-05 17:14
Schenz4-Nov-05 17:14 
QuestionProblem in RegEx? Pin
wasawasa19-Feb-05 1:38
wasawasa19-Feb-05 1:38 
GeneralExternal UI & Maintenance Mode Pin
jnanneng6-Jan-05 15:09
jnanneng6-Jan-05 15:09 
GeneralRe: External UI & Maintenance Mode Pin
AlcedoSoftware5-Jun-05 3:50
AlcedoSoftware5-Jun-05 3:50 
GeneralSession Object Pin
MikeOliszewski14-Mar-04 9:05
MikeOliszewski14-Mar-04 9:05 
GeneralRe: Session Object Pin
ian mariano14-Mar-04 9:41
ian mariano14-Mar-04 9:41 

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.