
Visual CSharp 2005 Recipes (2006) [eng]
.pdf
448 C H A P T E R 1 2 ■ U N M A N A G E D C O D E I N T E R O P E R A B I L I T Y
static void Main(string[] args)
{
//Request that the operating system enumerate all windows,
//and trigger your callback with the handle of each one. EnumWindows(DisplayWindowInfo, 0);
//Wait to continue. Console.WriteLine(Environment.NewLine); Console.WriteLine("Main method complete. Press Enter."); Console.ReadLine();
}
//The method that will receive the callback. The second
//parameter is not used, but is needed to match the
//callback's signature.
public static bool DisplayWindowInfo(IntPtr hWnd, int lParam)
{
int chars = 100;
StringBuilder buf = new StringBuilder(chars); if (GetWindowText(hWnd, buf, chars) != 0)
{
Console.WriteLine(buf);
}
return true;
}
}
}
12-5. Retrieve Unmanaged Error Information
Problem
You need to retrieve error information (either an error code or a text message) explaining why a Win32 API call failed.
Solution
On the declaration of the unmanaged method, set the SetLastError field of the DllImportAttribute to true. If an error occurs when you execute the method, call the static Marshal.GetLastWin32Error method to retrieve the error code. To get a text description for a specific error code, use the unmanaged
FormatMessage function.
How It Works
You cannot retrieve error information directly using the unmanaged GetLastError function. The problem is that the error code returned by GetLastError might not reflect the error caused by the unmanaged function you are using. Instead, it might be set by other .NET Framework classes or the CLR. You can retrieve the error information safely using the static Marshal.GetLastWin32Error method. This method should be called immediately after the unmanaged call, and it will return the error information only once. (Subsequent calls to GetLastWin32Error will simply return the error code 127.) In addition, you must specifically set the SetLastError field of the DllImportAttribute to true to indicate that errors from this function should be cached.
[DllImport("user32.dll", SetLastError=true)]

C H A P T E R 1 2 ■ U N M A N A G E D C O D E I N T E R O P E R A B I L I T Y |
449 |
You can extract additional information from the Win32 error code using the unmanaged FormatMessage function from the Kernel32.dll file.
The Code
The following console application attempts to show a message box, but submits an invalid window handle. The error information is retrieved with Marshal.GetLastWin32Error, and the corresponding text information is retrieved using FormatMessage.
using System;
using System.Runtime.InteropServices;
namespace Apress.VisualCSharpRecipes.Chapter12
{
class Recipe12_05
{
// Declare the unmanaged functions. [DllImport("kernel32.dll")]
private static extern int FormatMessage(int dwFlags, int lpSource, int dwMessageId, int dwLanguageId, ref String lpBuffer, int nSize, int Arguments);
[DllImport("user32.dll", SetLastError = true)]
public static extern int MessageBox(IntPtr hWnd, string pText, string pCaption, int uType);
static void Main(string[] args)
{
//Invoke the MessageBox function passing an invalid
//window handle and thus force an error.
IntPtr badWindowHandle = (IntPtr)453;
MessageBox(badWindowHandle, "Message", "Caption", 0);
// Obtain the error information.
int errorCode = Marshal.GetLastWin32Error(); Console.WriteLine(errorCode); Console.WriteLine(GetErrorMessage(errorCode));
// Wait to continue. Console.WriteLine(Environment.NewLine); Console.WriteLine("Main method complete. Press Enter."); Console.ReadLine();
}
//GetErrorMessage formats and returns an error message
//corresponding to the input errorCode.
public static string GetErrorMessage(int errorCode)
{
int FORMAT_MESSAGE_ALLOCATE_BUFFER = 0x00000100; int FORMAT_MESSAGE_IGNORE_INSERTS = 0x00000200; int FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000;
int messageSize = 255; string lpMsgBuf = "";
int dwFlags = FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS;


C H A P T E R 1 2 ■ U N M A N A G E D C O D E I N T E R O P E R A B I L I T Y |
451 |
When you click OK, the PIA will be generated and added to your project references. After that, you can use the Object Browser to inspect the namespaces and classes that are available.
If you are not using Visual Studio .NET, you can create a wrapper assembly using the Tlbimp.exe command-line utility that is included with the .NET Framework. The only mandatory piece of information is the filename that contains the COM component. For example, the following statement creates an RCW with the default filename and namespace, assuming that the MyCOMComponent.dll file is in the current directory.
tlbimp MyCOMComponent.dll
Assuming that MyCOMComponent has a type library named MyClasses, the generated RCW file will have the name MyClasses.dll and will expose its classes through a namespace named MyClasses. You can also configure these options with command-line parameters, as described in the MSDN reference. For example, you can use /out:[Filename] to specify a different assembly filename and /namespace:[Namespace] to set a different namespace for the generated classes. You can also specify a key file using /keyfile[keyfilename] so that the component will be signed and given a strong name, allowing it to be placed in the global assembly cache (GAC). Use the /primary parameter to create a PIA.
If possible, you should always use a PIA instead of generating your own RCW. PIAs are more likely to work as expected, because they are created by the original component publisher. They might also include additional .NET refinements or enhancements. If a PIA is registered on your system for a COM component, Visual Studio .NET will automatically use that PIA when you add
a reference to the COM component. For example, the .NET Framework includes an adodb.dll assembly that allows you to use the ADO classic COM objects. If you add a reference to the Microsoft ActiveX Data Objects component, this PIA will be used automatically; no new RCW will be generated. Similarly, Microsoft Office 2003 provides a PIA that improves .NET support for Office Automation.
However, you must download this assembly from the MSDN web site (at http://msdn.microsoft.com/ downloads/list/office.asp).
The Code
The following example shows how you can use COM Interop to access the classic ADO objects from a .NET Framework application.
using System;
namespace Apress.VisualCSharpRecipes.Chapter12
{
class Recipe12_06
{
static void Main(string[] args)
{
//Create a new ADODB connection. ADODB.Connection con = new ADODB.Connection(); string connectionString = "Provider=SQLOLEDB.1;" +
"Data Source=localhost;" +
"Initial Catalog=Northwind;Integrated Security=SSPI"; con.Open(connectionString, null, null, 0);
//Execute a SELECT query.
object recordsAffected;
ADODB.Recordset rs = con.Execute("SELECT * From Customers", out recordsAffected, 0);

452 C H A P T E R 1 2 ■ U N M A N A G E D C O D E I N T E R O P E R A B I L I T Y
//Print out the results. while (rs.EOF != true)
{
Console.WriteLine(rs.Fields["CustomerID"].Value);
rs.MoveNext();
}
//Wait to continue. Console.WriteLine(Environment.NewLine); Console.WriteLine("Main method complete. Press Enter."); Console.ReadLine();
}
}
}
12-7. Release a COM Component Quickly
Problem
You need to ensure that a COM component is removed from memory immediately, without waiting for garbage collection to take place, or you need to make sure that COM objects are released in
a specific order.
Solution
Release the reference to the underlying COM object using the static Marshal.FinalReleaseComObject method and passing the appropriate RCW.
How It Works
COM uses reference counting to determine when objects should be released. When you use an RCW, the reference will be held to the underlying COM object even when the object variable goes out of scope. The reference will be released only when the garbage collector disposes of the RCW object. As a result, you cannot control when or in what order COM objects will be released from memory.
To get around this limitation, you usually use the Marshal.ReleaseComObject method. However, if the COM object’s pointer is marshaled several times, you need to repeatedly call this method to decrease the count to zero. However, the FinalReleaseComObject method allows you to release all references in one go, by setting the reference count of the supplied RCW to zero. This means that you do not need to loop and invoke ReleaseComObject to completely release an RCW.
For example, in the ADO example in recipe 12-6, you could release the underlying ADO Recordset and Connection objects by adding these two lines to the end of your code:
System.Runtime.InteropServices.Marshal.FinalReleaseComObject(rs);
System.Runtime.InteropServices.Marshal.FinalReleaseComObject(con);
■Note The ReleaseComObject method does not actually release the COM object; it just decrements the reference count. If the reference count reaches zero, the COM object will be released. FinalReleaseComObject works by setting the reference count of an RCW to zero. It thus bypasses the internal count logic and releases all references.

C H A P T E R 1 2 ■ U N M A N A G E D C O D E I N T E R O P E R A B I L I T Y |
453 |
12-8. Use Optional Parameters
Problem
You need to call a method in a COM component without supplying all the required parameters.
Solution
Use the Type.Missing field.
How It Works
The .NET Framework is designed with a heavy use of method overloading. Most methods are overloaded several times so that you can call the version that requires only the parameters you choose to supply. COM, on the other hand, does not support method overloading. Instead, COM components usually use methods with a long list of optional parameters. Unfortunately, C# does not support optional parameters, which means C# developers are often forced to supply numerous additional or irrelevant values when accessing a COM component. And because COM parameters are often passed by reference, your code cannot simply pass a null reference. Instead, it must declare an object variable and then pass that variable.
You can mitigate the problem to some extent by supplying the Type.Missing field whenever you wish to omit an optional parameter. If you need to pass a parameter by reference, you can simply declare a single object variable, set it equal to Type.Missing, and use it in all cases, like this:
private static object n = Type.Missing;
The Code
The following example uses the Microsoft Word COM objects to programmatically create and show a document. Many of the methods the example uses require optional parameters passed by reference. You will notice that the use of the Type.Missing field simplifies this code greatly. Each use is emphasized in the code listing.
using System;
namespace Apress.VisualCSharpRecipes.Chapter12
{
class Recipe12_08
{
private static object n = Type.Missing;
static void Main(string[] args)
{
// Start Word in the background.
Word.ApplicationClass app = new Word.ApplicationClass(); app.DisplayAlerts = Word.WdAlertLevel.wdAlertsNone;
// Create a new document (this is not visible to the user).
Word.Document doc = app.Documents.Add(ref n, ref n, ref n, ref n);

454 C H A P T E R 1 2 ■ U N M A N A G E D C O D E I N T E R O P E R A B I L I T Y
Console.WriteLine();
Console.WriteLine("Creating new document.");
Console.WriteLine();
// Add a heading and two lines of text.
Word.Range range = doc.Paragraphs.Add(ref n).Range; range.InsertBefore("Test Document");
string style = "Heading 1"; object objStyle = style; range.set_Style(ref objStyle);
range = doc.Paragraphs.Add(ref n).Range; range.InsertBefore("Line one.\nLine two."); range.Font.Bold = 1;
//Show a print preview, and make Word visible. doc.PrintPreview();
app.Visible = true;
//Wait to continue. Console.WriteLine(Environment.NewLine); Console.WriteLine("Main method complete. Press Enter."); Console.ReadLine();
}
}
}
12-9. Use an ActiveX Control in a .NET Client
Problem
You need to place an ActiveX control on a form or a user control in a .NET Framework application.
Solution
Use an RCW exactly as you would with an ordinary COM component (see recipe 12-6). To work with the ActiveX control at design time, add it to the Visual Studio .NET Toolbox.
How It Works
The .NET Framework includes the same support for all COM components, including ActiveX controls. The key difference is that the RCW class for an ActiveX control derives from the special .NET Framework type System.Windows.Forms.AxHost. You add the AxHost control to your form, and it communicates with the ActiveX control “behind the scenes.” Because AxHost derives from System.Windows.Forms. Control, it provides the standard .NET control properties, methods, and events, such as Location, Size, Anchor, and so on. In the case of an autogenerated RCW, the AxHost classes will always begin with the letters Ax.

C H A P T E R 1 2 ■ U N M A N A G E D C O D E I N T E R O P E R A B I L I T Y |
455 |
You can create an RCW for an ActiveX control as you would for any other COM component, as described in recipe 12-6: use the Type Library Exporter (Tlbimp.exe) command-line utility or use the Add Reference feature in Visual Studio .NET and create the control programmatically. However, an easier approach in Visual Studio .NET is to add the ActiveX control to the Toolbox.
Nothing happens to your project when you add an ActiveX control to the Toolbox. However, you can use the Toolbox icon to add an instance of the control to your form. The first time you do this, Visual Studio .NET will create the interop assembly and add it to your project. For example, if you add the Microsoft Masked Edit control, Visual Studio .NET creates an RCW assembly with a name such as AxInterop.MSMask.dll. Here is the code you might expect to see in the hidden designer region that creates the control instance and adds it to the form:
this.axMaskEdBox1 = new AxMSMask.AxMaskEdBox(); ((System.ComponentModel.ISupportInitialize)(this.axMaskEdBox1)).BeginInit();
//
// axMaskEdBox1
//
this.axMaskEdBox1.Location = new System.Drawing.Point(16, 12); this.axMaskEdBox1.Name = "axMaskEdBox1"; this.axMaskEdBox1.OcxState = ((System.Windows.Forms.AxHost.State)
(resources.GetObject("axMaskEdBox1.OcxState")));
this.axMaskEdBox1.Size = new System.Drawing.Size(112, 20); this.axMaskEdBox1.TabIndex = 0;
this.Controls.Add(this.axMaskEdBox1);
Notice that the custom properties for the ActiveX control are not applied directly through property set statements. Instead, they are restored as a group when the control sets its persisted OcxState property. However, your code can use the control’s properties directly.
12-10. Expose a .NET Component Through COM
Problem
You need to create a .NET component that can be called by a COM client.
Solution
Create an assembly that follows certain restrictions identified in this recipe. Export a type library for this assembly using the Type Library Exporter (Tlbexp.exe) command-line utility.
How It Works
The .NET Framework includes support for COM clients to use .NET components. When a COM client needs to create a .NET object, the CLR creates the managed object and a COM callable wrapper (CCW) that wraps the object. The COM client interacts with the managed object through the CCW. The runtime creates only one CCW for a managed object, regardless of how many COM clients are using it.
Types that need to be accessed by COM clients must meet certain requirements:

456C H A P T E R 1 2 ■ U N M A N A G E D C O D E I N T E R O P E R A B I L I T Y
•The managed type (class, interface, struct, or enum) must be public.
•If the COM client needs to create the object, it must have a public default constructor. COM does not support parameterized constructors.
•The members of the type that are being accessed must be public instance members. Private, protected, internal, and static members are not accessible to COM clients.
In addition, you should consider the following recommendations:
•You should not create inheritance relationships between classes, because these relationships will not be visible to COM clients (although .NET will attempt to simulate this by declaring
a shared base class interface).
•The classes you are exposing should implement an interface. For added versioning control, you can use the attribute System.Runtime.InteropServices.GuidAttribute to specify the GUID that should be assigned to an interface.
•Ideally, you should give the managed assembly a strong name so that it can be installed into the GAC and shared among multiple clients.
In order for a COM client to create the .NET object, it requires a type library (a .tlb file). The type library can be generated from an assembly using the Tlbexp.exe command-line utility. Here is an example of the syntax you use:
tlbexp ManagedLibrary.dll
Once you generate the type library, you can reference it from the unmanaged development tool. With Visual Basic 6, you reference the .tlb file from the Project References dialog box. In Visual C++ 6, you can use the #import statement to import the type definitions from the type library.
