Chapter 17. Implementing .NET Interfaces for Type Compatibility

In This Chapter

Class Interfaces

Interface Inheritance

Considerations for Visual C++ Programmers

Considerations for Visual Basic 6 Programmers

To conclude our examination of developing COM components that are exposed to .NET, we’re going to focus on implementing .NET interfaces in unmanaged code. A .NET interface is any interface defined in metadata that’s not marked with the ComImportAttribute pseudo-custom attribute.

Just as a .NET class that implements a COM interface is sometimes said to be a “COM-compatible” class, a COM class that implements a .NET interface could be called a “.NET-compatible” class. This scenario is much rarer than the reverse, however, because programmers writing new code that implements .NET interfaces are likely writing managed code. But having a COM class implement a .NET interface can be a minor but important tweak that can greatly improve the use of an existing COM object from managed code. Because .NET is not a binary standard like COM but a type standard, COM objects implementing .NET interfaces can achieve a degree of type compatibility with .NET objects.

This process begins by exporting a type library containing an interface definition if one doesn’t already exist, and ends with importing a type library for your COM component that references the exported type library. Implementing a .NET interface is really no different than implementing a COM interface, but there are some surprises that you may encounter, as you’ll see in this chapter.

Unlike the reverse direction (covered in Chapter 14, “Implementing COM Interfaces for Binary Compatibility”) in which a COM-Callable Wrapper (CCW) implements several COM interfaces, a Runtime-Callable Wrapper (RCW) doesn’t implement any .NET interfaces by default. This is because none of a Runtime-Callable Wrapper’s base classes (System.__ComObject, System.MarshalByRefObject, and System.Object) implement any interfaces. Therefore, it’s up to the COM object to implement any .NET interfaces it wishes to expose.

Class Interfaces

Class interfaces exposed for .NET classes require special care. Although they can be used by COM clients, they cannot be implemented in a way that is meaningful for the .NET world because class interfaces do not exist from the .NET perspective. For example, suppose a Visual Basic 6 application attempts to implement the _Object class interface exposed for System.Object as follows:

Implements mscorlib.Object

Private Function Object_Equals(ByVal obj As Variant) As Boolean
  ...
End Function

Private Function Object_GetHashCode() As Long
  ...
End Function

Private Function Object_GetType() As mscorlib.Type
  ...
End Function

Private Property Get Object_ToString() As String
  ...
End Property

Image

(Visual Basic 6 hides the underscore for the _Object interface, making it look like we’re implementing something called Object.) Although a separate COM component could consume such an object and use its _Object implementation, chances are that the author of the previous code wanted to expose the class to .NET and have its methods override the default implementation of the System.Object methods in its Runtime-Callable Wrapper. Things don’t work that way, however. Thanks to the IDL custom attribute on the _Object interface definition linking it to the System.Object type, attempting to import a type library for the previous code results in an error because it appears that the class is trying to implement a class rather than an interface:

TlbImp error: System.TypeLoadException - Could not load type Project1.Class1Class from assembly Project1, Version=1.0.0.0 because it attempts to implement a class as an interface.

Caution

You cannot implement a .NET class interface in COM in a meaningful way for .NET clients. Although COM clients could communicate amongst themselves using a .NET class interface definition, you should never do this.

You cannot override methods nor can you control the class type of a COM object’s Runtime-Callable Wrapper by implementing a class interface. Note that this differs from the class interfaces generated by Visual Basic 6 for any coclass because these are real interfaces that can always be treated as such. Fortunately, users can only run into this confusion for auto-dual class interfaces, which are sufficiently rare. There should be no temptation to implement auto-dispatch class interfaces because their definitions contain no members.

Caution

When a .NET class type is used as a parameter of a .NET member (or field of a struct), the parameter that’s exported to a type library is always replaced with the corresponding coclass’s default interface. When a .NET class implements a real interface and suppresses its class interface with the recommended ClassInterface(ClassInterfaceType.None) setting, the result can be confusing when the class type is used as a parameter rather than the interface type. Consider the following C# code:

[ClassInterface(ClassInterfaceType.None)] public class MyClass : IRealInterface{  ...}public interface IRealInterface{  ...}public interface IDemo{  void GoodMethod(IRealInterface x);  void BadMethod(MyClass x);}

The IDemo interface appears as follows in an exported type library:

interface IDemo : IDispatch {   [id(0x60020000)]  HRESULT GoodMethod([in] IRealInterface* x);  [id(0x60020001)]  HRESULT BadMethod([in] IRealInterface* x);};

Regardless of whether the IRealInterface interface or MyClass class is used as the .NET parameter, the exported parameter looks the same! However, a COM object can implement IRealInterface and be passed to GoodMethod, whereas no COM object can ever be passed to BadMethod. That’s because an instance of MyClass is needed at run time, despite the misleading type library description of BadMethod.

Therefore, sometimes COM objects can’t even usefully implement real interfaces in order to be used with certain .NET APIs! This is why using class types for parameters when defining public .NET methods should be avoided wherever possible.

Often developers are tempted to implement _Object, because the ability to override its members’ default implementation can be very useful. Although this isn’t possible, there are reasonable alternatives for plugging in new functionality related to the members of System.Object. The following list contains the public and protected virtual (Overridable in VB .NET) instance methods of System.Object and what a COM object can do to customize related functionality:

Equals—Can implement IComparer instead to use in some scenarios, described in the “Example: Implementing IHashCodeProvider and IComparer to Use a COM Object as a Hashtable Key” section.

Finalize—A COM object is released during its RCW’s finalization, so its destructor (or Class_Terminate method in Visual Basic 6) is run at this time if it’s ready to be destroyed. Any COM objects that release limited resources upon destruction should also implement IDisposable, covered in the “Example: Implementing IDisposable to Clean Up Resources” section.

GetHashCode—Can implement IHashCodeProvider instead to use in some scenarios, described in the “Example: Implementing IHashCodeProvider and IComparer to Use a COM Object as a Hashtable Key” section.

ToString—Can implement IFormattable instead, shown in the “Example: Implementing IFormattable to Customize ToString” section.

Notice that none of these alternatives are specific to COM objects. .NET objects can and do use these additional interfaces.

Interface Inheritance

Recall that interface hierarchies are “sliced” when exposed to COM. Listing 17.1 demonstrates how three related interfaces in the System.Collections namespace are transformed by the type library exporter when defined in mscorlib.tlb.

Listing 17.1. An Interface Hierarchy Is Exported to COM as Unrelated Interfaces Containing Only their Direct Members

Image

Image

Image

This slicing of interfaces is significant when writing a COM object implementing a .NET interface that derives from other .NET interfaces. That’s because a COM object can implement a derived interface like IDictionary without bothering to implement its base ICollection and IEnumerable interfaces! A COM object implementing only IDictionary and its ten members defined in mscorlib.tlb compiles without errors because compilers consuming the type library are not aware of any relationship IDictionary has with other interfaces. The type library importer even successfully imports an Interop Assembly containing such a class; the metadata description indicates that it implements IDictionary.

But what does this mean for .NET programs that attempt to use such an object that only partially implements the interface? .NET languages enable calling base interface members without even a cast. Suppose you have the following C# code that uses the IDictionary interface:

public void PrintDictionaryProperties(IDictionary d)
{
  // Call property defined directly on IDictionary
  Console.WriteLine(d.IsReadOnly);
  // Call property on base ICollection
  Console.WriteLine(d.Count);
}

If a COM object implementing IDictionary but not ICollection were passed to this method, the first call to IDictionary.IsReadOnly would succeed, but the second call to ICollection.Count would throw the following exception:

System.InvalidCastException: QueryInterface for interface System.Collections.ICollection failed.
   at System.Collections.ICollection.get_Count()
   at Chapter17.PrintDictionaryProperties(IDictionary d)
   at Chapter17.Main()

It just goes to show you that what you see isn’t always what you get when dealing with COM objects. Fortunately the exception message is clear enough to determine what happened.

Caution

Whenever implementing a .NET interface, be sure to check its .NET definition for any base interfaces and implement them too. Passing an object with an incomplete interface definition to .NET objects is likely to behave incorrectly.

Considerations for Visual C++ Programmers

Implementing a .NET interface in an unmanaged C++ project is just like implementing any other interface defined in an external type library. There are a few things to be careful with, however. Here are the steps for implementing a .NET interface in an ATL COM project in Visual C++ 6:

1. Right-click on your class in the ClassView window and select Implement Interface..., as shown in Figure 17.1. If you haven’t compiled your project yet, a dialog will appear warning that your project doesn’t have a type library. You can click OK to get past that because we want to implement an interface in an external type library anyway.

Figure 17.1. Implementing a COM interface in Visual C++ 6 using the ATL Wizard.

Image

4. On the Implement Interface dialog that appears, click the Add Typelib... button. This brings up the dialog shown in Figure 17.2, listing all the type libraries registered on your computer. Select one and click OK. As in the Visual Basic 6 References dialog and the Visual Studio .NET Add Reference dialog, you can click the Browse... button to select a type library that isn’t registered. In the figure, the user selects Common Language Runtime Library, which is the type library exported from the mscorlib assembly.

Figure 17.2. Selecting a type library containing the desired interface to implement in Visual C++ 6.

Image

7. After selecting a type library, we’re now back to the Implement Interface dialog. Select the interface you wish to implement and press OK. This is shown in Figure 17.3.

Figure 17.3. Selecting the COM interface you wish to implement in Visual C++ 6.

Image

These steps are sufficient for implementing an interface, but if you plan to use the object from .NET clients, you should also list the interface as being implemented by your coclass in the project’s IDL file (as recommended in Chapter 15, “Creating and Deploying Useful Primary Interop Assemblies”). The wizard does not do this for you. This involves two extra steps:

1. Add an importlib statement inside your library block listing the type library containing the interface definition, for example: importlib("mscorlib.tlb").

2. List the interface inside the coclass statement, for example:

[
  uuid(1700FAA2-790D-4D27-AD0E-89AAB1EC39F2)
]
coclass FileWriter
{
  [default] interface IFileWriter;
  interface IDisposable;
};

Caution

If you’re implementing an interface defined in mscorlib.tlb, the ATL wizard generates a line like the following in your header file:

#import "path\mscorlib.tlb" raw_interfaces_only, raw_native_types, no_namespace, named_guids

However, the class interface for System.Reflection.Module is exported as a _Module interface, and this causes compilation errors in a Visual C++ 6 ATL project because it already defines a CComModule instance with the name _Module. To fix this, you could remove the no_namespace directive or add the #import statement’s rename directive to rename the _Module identifier from the type library. For example:

#import "path\mscorlib.tlb" raw_interfaces_only, raw_native_types, no_namespace, named_guids, rename("_Module", "_ReflectionModule")

ATL projects in Visual C++ .NET no longer use _Module, so this error does not occur for them.

Example: Implementing IDisposable to Clean Up Resources

Now that you’ve seen how to implement a .NET interface, let’s see how to update an existing COM component with an additional implemented interface. Listings 17.2 and 17.3 contain a C++ header file and implementation for the following simple FileWriter coclass that implements the IFileWriter interface, shown here in IDL:

[
  object,
  uuid(379FF39A-2404-4FAE-B928-B53FADF9255B),
  dual,
  pointer_default(unique)
]
interface IFileWriter : IDispatch
{
  [id(1)] HRESULT WriteLine([in] BSTR message);
};

[
  uuid(1700FAA2-790D-4D27-AD0E-89AAB1EC39F2)
]
coclass FileWriter
{
  [default] interface IFileWriter;
};

The CFileWriter C++ class contains the implementation, which opens a file upon construction and closes it upon destruction. Therefore, this is a good candidate to be updated to implement the .NET IDisposable interface, as discussed in Chapter 16, “COM Design Guidelines for Components Used by .NET Clients.”

To create such a project in Visual C++ 6, do the following:

1. Create a new ATL COM AppWizard project, and on the two dialogs that follow, click Finish, then OK.

2. Right-click on the topmost node in the ClassView window and select New ATL Object....

3. Select Simple Object from the Objects category, then click the Next button.

4. Type “FileWriter” in the Short Name text box, then click the Attributes tab, check Support ISupportErrorInfo, and click OK.

5. Right-click on IFileWriter in the ClassView window and select Add method....

6. Type “WriteLine” in the Method Name text box and type “[in] BSTR message” in the Parameters text box, then click OK. This creates an IDL file with the class and interface previously shown (but with different GUIDs) and a header file similar to Listing 17.2.

7. Fill in the implementation as in Listing 17.3 and update the header file with the contents of Listing 17.2.

Listing 17.2. FileWriter.h. The Initial Header File for the CFileWriter Class, Originally Generated by the ATL COM AppWizard

Image

Listing 17.3. FileWriter.cpp. Initial Implementation of the CFileWriter Class that Keeps a File Open During the Course of the Object’s Lifetime

Image

Image

Notice that the class implements ISupportErrorInfo, so the ATL wizard-generated implementation of InterfaceSupportsErrorInfo appears in Lines 12–24. This listing doesn’t use IErrorInfo because it doesn’t return any failure HRESULTs, but we’ll be making use of it when we update the code in Listing 17.5.

Lines 29–33 contain the class’s constructor, which opens a file for reading and appending using the fopen C runtime library function. The destructor in Lines 38–41 uses the fclose C runtime library function to close the file. The only other method is WriteLine, in Lines 46–51, which prints the passed-in message to the file using fwprintf, followed by a newline character.

This simple COM component’s strategy of opening and closing the file has no problems when COM objects release the component as soon as they are finished (as they usually do). When called from .NET clients, however, the file can remain open a long time longer than it should because of the time difference between being finished and garbage collection.

Listings 17.4 and 17.5 update the two files from Listings 17.2 and 17.3 to make the class implement the .NET IDisposable interface, using the steps outlined previously, at the beginning of the “Considerations for Visual C++ Programmers” section.

Listing 17.4. FileWriter.h. Updated C++ Header File for the CFileWriter Class That Implements IDisposable

Image

Image

The differences between Listings 17.4 and 17.2 appear in bold. All of the additional lines were added by the ATL wizard when choosing to implement IDisposable. The one line that was tweaked is Line 8, to add the rename directive for the _Module type to avoid conflicts with ATL’s _Module definition.

Listing 17.5. FileWriter.cpp. Updated Implementation of the CFileWriter Class That Implements IDisposable

Image

Image

Image

Again, the differences between Listings 17.5 and 17.3 appear in bold. None of the changes in this listing were done by the ATL wizard, but had to be done manually instead. Line 5 includes CorError.h, the header file defining .NET HRESULTs (with FACILITY_URT), which ships with the .NET Framework SDK. Line 18 adds the IID of IDisposable to the list of interfaces for which InterfaceSupportsErrorInfo returns S_OK. Although this listing’s implementation of IDisposable doesn’t use IErrorInfo, it’s important to remember to update your implementation of InterfaceSupportsErrorInfo whenever you implement a new interface. Otherwise, any IErrorInfo information you set in the members belonging to the interface gets ignored.

The constructor is the same as in Listing 17.3, but the destructor in Lines 40–45 has a new check for NULL before calling fclose. This is necessary because the file might have already been closed by a call to Dispose (which also sets the filePointer variable to NULL). If Dispose has already been called, then the destructor simply exits without doing any work. This is analogous to a .NET Dispose implementation calling GC.SuppressFinalize to suppress any work done by a finalizer during garbage collection. Because the class’s destructor is always called, the check must be done to suppress its own behavior.

The implementation of WriteLine (Lines 50–91) is now significantly longer than before, but the logic is simple. If the file has already been closed (meaning filePointer is NULL), a failure HRESULT is returned, otherwise the message is printed to the file. This extra check is a necessity (in managed or unmanaged code) when the encapsulated resource can be disposed while the object instance is still alive.

Before returning the failure HRESULT, Lines 59–81 set the IErrorInfo information so .NET clients see a descriptive exception message when trying to call WriteLine after Dispose has been called. Objects that implement IDisposable are supposed to throw a System.ObjectDisposedException, but it’s not possible for a COM object to cause that exception type to be thrown because it doesn’t have a distinct HRESULT value. Instead, it returns the HRESULT corresponding to ObjectDisposedException’s base InvalidOperationException class and sets the message to explain that it’s related to the object being disposed prematurely.

Tip

When implementing a .NET interface, try to return HRESULT values that correspond to the exceptions .NET clients would expect to be thrown by .NET components implementing the same interface. This isn’t always possible, however, because only exception types that are defined in the mscorlib assembly and have distinct HRESULT values can be thrown by COM clients. When you can’t return an HRESULT that corresponds exactly to the desired exception, you can do two things to make up for it:

Return the HRESULT value corresponding to the exception’s most derived base class that defines a distinct value. This is the same value you’d get by calling Marshal.GetHRForException from the System.Runtime.InteropServices namespace. If this ends up being a generic HRESULT like E_FAIL that results in a COMException when thrown, you might as well define and return your own custom HRESULT value (and document it). See Appendix C, “HRESULT to .NET Exception Transformations,” to see which HRESULTs correspond to which .NET exception types.

Use IErrorInfo to give the exception a message that describes exactly what the problem is. This is often just as critical as returning the appropriate HRESULT value.

Even when you can return the desired HRESULT that corresponds to a .NET exception, COM components have a disadvantage because they can’t use a .NET exception’s default message (which would be seen when a .NET client throws the exception without setting a message). If a COM method returns a .NET HRESULT without setting a message, it will always be “Exception from HRESULT...,” so setting a message is always critical.

Finally, IDisposable’s Dispose method appears on Lines 96–106. In this method, the file is closed if it hasn’t been already. As the contract for Dispose requires, calling it multiple times has the same effect as calling it once.

A .NET client can now use the FileWriter object and take advantage of its IDisposable implementation to close the file in a timely and familiar fashion. For example, a C# client can use the using construct as follows:

using ((IDisposable)FileWriter f = new FileWriter())
{
  foreach(int i in new int[]{1,2,3,4,5,6,7,8,9,10})
    ((FileWriter)f).WriteLine(i.ToString());
}

COM objects implementing .NET interfaces aren’t quite as natural to use in managed code as .NET objects implementing .NET interfaces simply because a coclass interface must be explicitly cast to any interface other than the coclass’s default interface. To avoid the cast, you could use the actual class type instead (with the Class suffix):

using (FileWriterClass f = new FileWriterClass())
{
  foreach(int i in new int[]{1,2,3,4,5,6,7,8,9,10})
    f.WriteLine(i.ToString());
}

Considerations for Visual Basic 6 Programmers

Implementing a .NET interface in a Visual Basic 6 project is pretty easy; just reference the appropriate type library, as shown in Chapter 8, “The Essentials for Using .NET Components from COM,” then type Implements InterfaceName at the top of the class module file.

There’s one unfortunate limitation to implementing interfaces in Visual Basic 6—the compiler doesn’t allow implementing an interface containing a member whose name contains an underscore. Because of that, any interface with overloaded members cannot be implemented by a Visual Basic 6 class. Figure 17.4 demonstrates what happens when a Visual Basic 6 user attempts to implement System.Reflection.ICustomAttributeProvider from the mscorlib assembly. This interface has two GetCustomAttribute methods, so cannot be implemented. As shown, the Visual Basic 6 IDE silently omits the interface from the drop-down list that usually lists all implemented interfaces.

Figure 17.4. Attempting to implement a .NET interface with overloaded methods in Visual Basic 6.

Image

When attempting to compile such a project, the error message shown in Figure 17.5 is displayed.

Figure 17.5. The error message when attempting to implement a .NET interface with overloaded methods in Visual Basic 6.

Image

There is no good workaround for this problem. If the interface were derived directly from IUnknown rather than IDispatch (so late binding via name is out of the question), you could safely modify the exported method names to something without underscores, using OLEVIEW.EXE to save the IDL representation of mscorlib.tlb and using MIDL.EXE to create a new type library from the altered IDL file. Or, you could create a new type library that contains an updated definition of ICustomAttributeProvider with the new names. But this is not a good idea when the interface in question derives from IDispatch, as ICustomAttributeProvider does, because such modified names would be unrecognized by the object’s IDispatch implementation if COM clients attempted to use late binding.

Example: Implementing IFormattable to Customize ToString

As mentioned in the “Class Interfaces” section at the beginning of the chapter, it’s impossible for a COM object to override Object.ToString in its Runtime-Callable Wrapper. The functionality for this handy method, however, can be controlled by implementing the .NET System.IFormattable interface.

IFormattable has a single ToString method that returns a string and has two parameters—a string specifying a format and an IFormatProvider instance for customizing the output with locale-specific information. Listing 17.6 contains a Visual Basic 6 class that implements IFormattable in order to control its ToString output.

Listing 17.6. FormattableClass.cls. A Visual Basic 6 Class That Implements IFormattable to Control its ToString Formatting

Image

Image

Line 2 contains the critical line that makes the class implement IFormattable, defined in mscorlib.tlb, which must be referenced by the Visual Basic 6 project. The definition of IFormattable.ToString appears in Lines 13–66. Notice that it’s defined as a property get accessor rather than a method. That’s because of the type library exporter’s transformation of any ToString methods into read-only properties, as explained in Chapter 9, “An In-Depth Look at Exported Type Libraries.” This doesn’t affect the .NET view of the method, so you can just implement the property the same way you’d implement the method.

By default, calling ToString on a COM object returns the fully-qualified type name of the Runtime-Callable Wrapper, which could be MyProject.FormattableClass in this case or System.__ComObject if the class has no metadata available or associated with an instance. The goal of this IFormattable implementation is to enable a variety of different formats to be displayed, including displaying the class’s CLSID or even the internal state of the object’s private array member.

The IFormatProvider parameter is ignored in this simple implementation, making it culture-neutral. The format string parameter can be one of the following values: “G”, “GN”, “GD”, “GB”, “GP”, “P”, “S”, an empty string or a null string. The documentation for IFormattable states that all implementations must support at least the “G” formatting code (which stands for “General”). Every implementation should also be prepared to handle a null string to accept the default formatting. The default formatting doesn’t have to match the “G” formatting, although it’s a good idea in order to prevent confusion.

Line 20 chooses the “G” formatting if a null string or empty string is passed. Visual Basic 6 strings cannot be null, so if a .NET client passes null (Nothing in VB .NET), the VB6 object simply sees an empty string. Line 22 begins a Select Case statement to return the correct string for each valid formatting specifier. The “G” formatting returns “COM Class: ProgID {CLSID}”, and “GN”, “GD”, “GB”, and “GP” return the CLSID with formatting that matches System.Guid.ToString’s “N”, “D”, “B”, and “P” formatting, respectively. The CLSID needs to be hardcoded after compiling once and checking the project’s type library, because Visual Basic 6 doesn’t provide a way to control CLSIDs.

The “P” formatting returns the class’s ProgID, and “S” returns “ClassName with Array { contents of array } “. If an unsupported formatting specifier was given, Lines 62–64 cause a FormatException to be thrown, as specified by the IFormattable contract. A FormatException can be thrown simply by raising an error with the COR_E_FORMAT HRESULT value.

A Visual Basic .NET client can use an instance of FormattableClass as follows:

Dim c As FormattableClass = new FormattableClass()
Console.WriteLine(c)
Console.WriteLine(c.ToString("GN", Nothing))
Console.WriteLine(c.ToString("GD", Nothing))
Console.WriteLine(c.ToString("GB", Nothing))
Console.WriteLine(c.ToString("GP", Nothing))
Console.WriteLine(c.ToString("P", Nothing))
Console.WriteLine(c.ToString("S", Nothing))

Running this would produce the following output:

COM Class: MyProject.FormattableClass {C2FBA9E3-B523-47C5-AFB6-F6180EEB6CD4}
C2FBA9E3B52347C5AFB6F6180EEB6CD4
C2FBA9E3-B523-47C5-AFB6-F6180EEB6CD4
{C2FBA9E3-B523-47C5-AFB6-F6180EEB6CD4}
(C2FBA9E3-B523-47C5-AFB6-F6180EEB6CD4)
MyProject.FormattableClass
FormattableClass with Array { 0 1 2 3 4 5 6 7 8 9 }

Example: Implementing IHashCodeProvider and IComparer to Use a COM Object as a Hashtable Key

An object’s GetHashCode and Equals methods are important for objects serving as keys in a hashtable. The default implementation provided by System.Object is acceptable if two keys should be compared based on their object references, but developers often want key objects to be compared based on some sort of value that’s independent of object references.

For example, imagine that you have instances of a chess board that you want to use as keys in a hashtable. You want to consider two chess boards as equal if they “look” exactly the same—both have the exact same pieces in the exact same places. To get this behavior, a ChessBoard class would normally need to override its Equals method to choose a behavior other than reference equality and override its GetHashCode method to return the same hash code for “equal” instances.

Although COM objects can’t override the System.Object implementation of Equals or GetHashCode, they can implement the .NET IComparer interface (which has a single Equals method) and the .NET IHashCodeProvider interface (which has a single GetHashCode method) to plug in the same sort of functionality used by certain .NET types. For example, an IComparer instance can be passed to System.Array.BinarySearch, System.Array.Sort, a System.Collections.Hashtable constructor, or a System.Collections.SortedList constructor, and these types will use its Equals method rather than the usual Object.Equals method to test equality. Similarly, an IHashCodeProvider instance can be passed to a System.Collections.Hashtable constructor, and it will use its GetHashCode method rather than the usual Object.GetHashCode method to retrieve the hash code for any key.

These interfaces exist in order to provide hash codes or equality comparisons on behalf of a separate object that doesn’t appropriately override its GetHashCode and Equals methods. Therefore, you could author a new .NET class implementing IHashCodeProvider and IComparer that can be used in conjunction with an existing COM object. However, a COM object could easily implement these two interfaces on behalf of itself. Listing 17.7 does this for a Visual Basic 6 COM class that represents a chess board.

Listing 17.7. ChessBoard.cls. A Visual Basic 6 Class That Implements IHashCodeProvider and IComparer So the ChessBoard Type Can Be Used as Keys in a Hashtable

Image

Image

Lines 1 and 2 make the class implement IHashCodeProvider and IComparer, referenced from mscorlib.tlb. Line 4 contains the class’s raw representation of the chess board—an 8×8 array of bytes. Each element represents a square on the chess board, and each value represents the contents of the square, such as 0 for empty, 1 for white king, 2 for white queen, and so on. Lines 6–8 contain a public read-only property for accessing the contents of each square. The ellipses in Line 10 represent additional ChessBoard functionality that’s not important for this listing.

The implementation of IComparer.Compare in Lines 12–27 returns true if the value of every element of the array belonging to x matches every element of the array belonging to y, or false if a single element doesn’t match. The passed-in x and y parameters are assumed to be instances of ChessBoard. The implementation of IHashCodeProvider.GetHashCode in Lines 29–41 does a simple mathematical operation to ensure that a given chess board arrangement always returns the same hash code value, which is the only requirement of a hash code. Coming up with one that’s unique for every key gives the best performance, but is not necessary. In this example, the same hash code can correspond to multiple board states, but it still enables appropriate hashing behavior whereas Object.GetHashCode would not. (For example, any two boards that differ only by the value of element (0,0) return the same hash code in our implementation. That’s why GetHashCode isn’t used by the Equals method.)

A C# client can use ChessBoard instances in a System.Collections.Hashtable instance it constructs as follows:

ChessBoard b = new ChessBoard();
Hashtable t = new Hashtable((IHashCodeProvider)b, (IComparer)b);

or:

ChessBoardClass b = new ChessBoardClass();
Hashtable t = new Hashtable(b, b);

The first parameter is used for its IHashCodeProvider implementation, and the second parameter (which could have been a completely different object) is used for its IComparer implementation.

Conclusion

You should now know everything there is to know about writing COM classes that implement .NET interfaces. The four main points are:

• Implementing regular .NET interfaces is no different than implementing a COM interface once you reference an exported type library.

• Do not attempt to implement a .NET class interface.

• Visual Basic 6 classes can’t implement .NET interfaces with overloaded methods because of the renaming that includes an underscore.

• Try to return failure HRESULTs that are as close as possible to the .NET exception expected, and always return additional error information that includes a message.

Many interfaces in the .NET Framework are COM-invisible because of their assemblies being marked with ComVisible(false). There’s no way for a COM object to implement such an interface without resorting to custom marshaling, discussed in Chapter 20, “Custom Marshaling.”

Caution

When using the type library importer to create an Interop Assembly for a COM class that implements a .NET interface, two things must be true:

• The exported type library describing COM’s view of the .NET interface must be registered so the importer can resolve the dependency.

• The assembly containing the original .NET interface must be in a location that can be loaded (such as the GAC or the directory in which you’re running TLBIMP.EXE). The importer must load this assembly to get the .NET type of the interface being implemented. Failure to have it in a loadable location results in a System.IO.FileNotFoundException.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.224.59.231