Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Visual CSharp 2005 Recipes (2006) [eng]

.pdf
Скачиваний:
51
Добавлен:
16.08.2013
Размер:
4.04 Mб
Скачать

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;

450 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

int retVal = FormatMessage(dwFlags, 0, errorCode, 0, ref lpMsgBuf, messageSize, 0);

if (0 == retVal)

{

return null;

}

else

{

return lpMsgBuf;

}

}

}

}

Usage

Here is the output generated by the preceding program:

1400

Invalid window handle.

12-6. Use a COM Component in a .NET Client

Problem

You need to use a COM component in a .NET client.

Solution

Use a primary interop assembly (PIA), if one is available. Otherwise, generate a runtime callable wrapper (RCW) using the Type Library Importer (Tlbimp.exe) or the Add Reference feature in Visual Studio .NET.

How It Works

The .NET Framework includes extensive support for COM interoperability. To allow .NET clients to interact with a COM component, .NET uses an RCW—a special .NET proxy class that sits between your .NET code and the COM component. The RCW handles all the details, including marshaling data types, using the traditional COM interfaces, and handling COM events.

You have the following three options for using an RCW:

Obtain an RCW from the author of the original COM component. In this case, the RCW is created from a PIA provided by the publisher, as Microsoft does for Microsoft Office.

Generate an RCW using the Tlbimp.exe command-line utility or Visual Studio .NET.

Create your own RCW using the types in the System.Runtime.InteropServices namespace. (This can be an extremely tedious and complicated process.)

If you want to use Visual Studio .NET to generate an RCW, you simply need to select Project Add Reference from the menu, and then select the appropriate component from the COM tab.

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.

C H A P T E R 1 3

■ ■ ■

Commonly Used Interfaces

and Patterns

The recipes in this chapter show you how to implement patterns you will use frequently during the development of Microsoft .NET Framework applications. Some of these patterns are formalized using interfaces defined in the .NET Framework class library. Others are less rigid, but still require you to take specific approaches to their design and implementation of your types. The recipes in this chapter describe how to do the following:

Create serializable types that you can easily store to disk, send across the network, or pass by value across application domain boundaries (recipe 13-1)

Provide a mechanism that creates accurate and complete copies (clones) of objects (recipe 13-2)

Implement types that are easy to compare and sort (recipe 13-3)

Support the enumeration of the elements contained in custom collections using the built-in iterator capability of C# 2.0 or by creating a custom iterator (recipes 13-4 and 13-5)

Ensure that a type that uses unmanaged resources correctly releases those resources when they are no longer needed (recipe 13-6)

Display string representations of objects that vary based on format specifiers (recipe 13-7)

Correctly implement custom exception and event argument types, which you will use frequently in the development of your applications (recipes 13-8 and 13-9)

Implement the commonly used Singleton and Observer design patterns using the built-in features of C# and the .NET Framework class library (recipes 13-10 and 13-11)

13-1. Implement a Serializable Type

Problem

You need to implement a custom type that is serializable, allowing you to do the following:

Store instances of the type to persistent storage (for example, a file or a database).

Transmit instances of the type across a network.

Pass instances of the type “by value” across application domain boundaries.

457

Соседние файлы в предмете Программирование на C++