![]() |
Languages »
C# »
COM Interop
Advanced
License: The Code Project Open License (CPOL)
Wrapping the Windows Installer 2.0 APIBy ian marianoAn article describing wrapping the Windows Installer 2.0 API using C# and .NET interop. |
C#.NET 1.1, Win2K, WinXP, Win2003, Visual Studio, Dev
|
|
Advanced Search |
|
|
|
||||||||||||||||
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 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.
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 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.
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.
This code is maintained in a GotDotNet workspace.
Please feel free to browse the following reference material:
Revisions:
General
News
Question
Answer
Joke
Rant
Admin
|
PermaLink |
Privacy |
Terms of Use
Last Updated: 4 Jan 2004 Editor: Smitha Vijayan |
Copyright 2004 by ian mariano Everything else Copyright © CodeProject, 1999-2009 Web16 | Advertise on the Code Project |