Using .NET Objects from COM Clients

Team-Fly    

 
.NET and COM Interoperability Handbook, The
By Alan Gordon
Table of Contents
Chapter Six.  An Introduction to COM Interop

Using .NET Objects from COM Clients

Now that you understand how to use COM objects from your managed code clients, let's look at the other side of the same coin: using a .NET object from a COM client. Once again, you face the problem of trying to bridge two very different object models (COM and .NET). In this case, you want a .NET object to be exposed to a COM/Win32 client as a COM object, and, once again, your interface should be as natural as possible. In other words, the .NET object should appear to the COM/Win32 client (for better or worse ) to be just like any other COM object. You should be able to create the object using CoCreateInstance or one of the other COM activation functions. The object should expose the IUnknown interface that you use to manage the life cycle of the object. The object should also have a type library and store information in the registry just like any other COM object. And just as it was when I used COM objects from managed code, the marshaling from unmanaged types to managed typesand vice versafor parameters and return values should be handled automatically. The CLR provides both a runtime wrapper object called a CCW and a tool that you can use to convert your .NET metadata into a COM type library and add the necessary entries to the registry (regasm.exe).

CCW

When you instantiate a .NET object from a Win32 application or a COM component, the CLR will automatically create a CCW that transparently handles all of the mapping issues between a Win32/COM client and a .NET component as shown in Figure 6-8.

Figure 6-8. A CCW.

graphics/06fig08.gif

A CCW provides the opposite functionality to an RCW. It handles marshaling of unmanaged types on the client to managed types in the object and vice versa. The CCW is a COM object. It holds a reference to the managed object and exposes the managed object to unmanaged clients. When the client releases all of the interfaces it holds on the CCW, the CCW removes its reference to the underlying managed object, and it then becomes eligible for garbage collection. Notice in Figure 6-8 that, in addition to IDispatch and IUnknown, which the CCW always supports, the CCW also may expose generated class interfaces. It will also expose COM equivalents of any managed code interfaces that the underlying managed code object implements. Because the COM runtime interrogates the registry for location information when you instantiate an object, you must first insert a few entries in the registry before you can use a .NET object from an unmanaged client. Also, because type libraries are such a fundamental part of COM, you will also need to have a type library before you can use the .NET object from your managed code client.

Fortunately, the .NET Framework SDK provides you with three ways to generate the registry entries that you need and two ways to generate a type library. You can generate the registry entries in one of the following ways:

  • Using the command-line Assembly Registration Tool (regasm.exe) in the .NET Framework SDK

  • Using the Register for COM Interop option under Configuration Properties Build on the project properties page in Visual Studio .NET

  • Using the RegisterAssembly or RegisterTypeForComClients methods in the System.Runtime.InteropServices.RegistrationServices class

I explore using the Assembly Registration Tool (regasm.exe) and the Register for COM Interop option in this chapter.

Assembly Registration Tool (regasm.exe)

The Assembly Registration Tool uses the metadata in an assembly to generate registry entries and ( optionally ) a type library, which allow COM clients to use .NET objects. Figure 6-9 shows a conceptual view of this process.

Figure 6-9. The Assembly Registration Tool.

graphics/06fig09.gif

The regasm is a command-line tool, and the basic syntax for using it is as follows :

 regasm assemblyfilename [options] 

The list of command-line options for regasm is shown in Table 6-5.

Table 6-5. Available options for regasm

Parameter

Description

tlb:[filename]

Allows you to specify the name for the type library file.

regfile:[filename]

Generates a reg. file instead of registering the types.

codebase

Sets the codebase in the registry.

registered

Only refers to registered type libraries.

nologo

Removes the types in the registry.

silent

Suppresses all output except errors.

verbose

Displays extra diagnostic output information.

Run regasm with the simplest command line as follows:

 regasm [assemblyname] 

Running this command will add entries to the registry that allow all the types in the assembly to be used by unmanaged clients as though they were COM types. If you want to generate a type library too, use the /tlb option on regasm. If you do not want to add the registry entries immediately, you can use the regfile option to generate a registry (.reg) file that you can use to add the registry entries later. You will need to use the registry editor (regedit) to add the contents of this file to the registry as shown here:

 regedit [registryfilename] 

Curiously, you cannot use the regfile option with the /tlb option. Another important optional parameter for regasm is the codebase option. The regasm tool inserts the full assembly name into registry (that is, the friendly name, version number, public key token if there is one, and culture). The CLR uses its regular search rules (which were explained in Chapter 3) to locate the assembly. This means that the assembly that contains the managed object must reside in a subdirectory beneath the unmanaged client application or in the GAC. The codebase option on regasm sets the codebase in the registry equal to the path of the assembly that you originally passed to regasm. The CLR will check the codebase after it checks the GAC, but before it starts probing beneath the directory structure of the client executable. This option is useful if you do not want to put the .NET assembly into the GAC or into a subdirectory of the managed application. Some example command lines for regasm are as follows:

 regasm dotnetfinancial.dll regasm dotnetfinancial.dll /tlb:dotnetfinancial.tlb regasm dotnetfinancial.dll /reg:dotnetfinancial.reg regasm dotnetfinancial.dll /codebase 

The first command line will insert the required entries into the registry for a .NET assembly called dotnetfinancial. The second command line will insert the required registry entries into the registry and create and register a type library called dotnetfinancial.tlb . The third command will generate the registry entries, but it won't add them to the registry. It will instead create a registry file called dotnetfinancial.reg that contains the registry entries. You can run the regedit command to add these entries to the registry later. The fourth command line will insert the required entries into the registry and add a codebase to the registry that the CLR will search for the assembly after it searches the GAC.

The Type Library Exporter (tlbexp.exe)

You can also generate a type library using the Type Library Exporter (tlbexp), which is yet another command-line tool in the .NET Framework SDK. The Type Library Exporter only generates type libraries. Unlike the /tlb option on regasm, tlbexp does not also register the assembly. You have to do that separately. The syntax for using tlbexp is as follows:

 tlbexp assemblyfilename [options] 

The list of available options for tlbexp is shown in Table 6-6.

Table 6-6. Available options for tlbexp

Parameter

Description

out:[filename]

Allows you to specify the name for the type library file.

nologo

Prevents tlbimp from displaying the Microsoft logo.

silent

Suppresses all output except errors.

verbose

Displays extra diagnostic output information.

names :[FileName]

A file where each line specifies the capitalization to use for each name in the generated type library.

An example command line for tlbexp is as follows:

 tlbexp dotnetfinancial.dll /out:dotnetfinancial.tlb 

Using Visual Studio .NET

If you have Visual Studio .NET, you do not have to resort to the command-line tools to register your .NET assemblies for use by unmanaged clients using COM Interop. With your managed code project open, perform the following steps to register your assembly for use by COM and to create a type library:

  1. Right-click the project name in the solution explorer window in Visual Studio .NET. A context menu will appear. (See Figure 6-10.)

    Figure 6-10. The Solution Explorer window in Visual Studio .NET.

    graphics/06fig10.jpg

  2. Select Properties from the context menu. The project property pages dialog will appear. (See Figure 6-11.)

    Figure 6-11. The project property pages dialog in Visual Studio .NET.

    graphics/06fig11.jpg

  3. Select True from the dropdown list next to Register for COM Interop.

  4. Click the OK button.

An Example

To show you how to use regasm to make a .NET assembly accessible from COM, I created a .NET version of the financial component. I won't go into a series of step-by-step instructions for building this component. Creating the project for this assembly is very similar to the spell-checker example that I built in Chapter 5. The assembly has a namespace called DotNetFinancial that contains a single class called TimeValue , which follows:

 namespace DotNetFinancial {     public class TimeValue     {         public TimeValue()         {         }         public decimal MonthlyPayment(short numMonths,             double interestRate,             decimal loanAmt)         {             double monthlyRate, tempVal;             decimal unRoundedAmt;             monthlyRate=interestRate/1200;             tempVal=Math.Pow((1+monthlyRate),                 (double)numMonths);             unRoundedAmt=(decimal)((double)loanAmt*                 (monthlyRate*tempVal/(tempVal-1)));             return decimal.Round(unRoundedAmt,2);         }         public decimal LoanAmount(short numMonths,             double interestRate,             decimal monthlyPmt)         {             double monthlyRate, tempVal;             decimal unRoundedAmt;             monthlyRate=interestRate/1200;             tempVal=                 Math.Pow((1+monthlyRate),                 (double)numMonths);             unRoundedAmt=                 (decimal)((double)monthlyPmt*                 (tempVal-1)/(monthlyRate*tempVal));             return decimal.Round(unRoundedAmt,2);         }     } } 

I also added a strong name to the assembly by creating a key pair using the Strong Name Tool ( sn.exe ) and then referencing the key file in the AssemblyKeyFile attribute in the Assemblyinfo file of my project. If you are not sure how to do this, see the example project that I built in Chapter 5. The process is exactly the same for this assembly. You can also download the code for this book and look at the DotNetFinancial server project in the code for Chapter 6.

The DotNetFinancial assembly resides in a file called DotNetFinancial.dll . You can register the assembly for use by an unmanaged client using the following command line:

 regasm dotnetfinancial.dll /tlb:dotnetfinancial.tlb 

This command line will generate a type library and insert the following entries into the registry for the assembly:

 [HKEY_CLASSES_ROOT\DotNetFinancial.TimeValue] @="DotNetFinancial.TimeValue" [HKEY_CLASSES_ROOT\DotNetFinancial.TimeValue\CLSID] @="{311F0CBD-FB08-3DB4-A87C-96EA3C30DA0D}" [HKEY_CLASSES_ROOT\CLSID\ {311F0CBD-FB08-3DB4-A87C-96EA3C30DA0D}] @="DotNetFinancial.TimeValue" [HKEY_CLASSES_ROOT\CLSID\ {311F0CBD-FB08-3DB4-A87C-96EA3C30DA0D}\InprocServer32] @="D:\WINNT\System32\mscoree.dll" "ThreadingModel"="Both" "Class"="DotNetFinancial.TimeValue" "Assembly"="DotNetFinancial, Version=1.0.814.40736, Culture=neutral, PublicKeyToken=null" "RuntimeVersion"="v1.0.3328" [HKEY_CLASSES_ROOT\CLSID\ {311F0CBD-FB08-3DB4-A87C-96EA3C30DA0D}\ProgId] @="DotNetFinancial.TimeValue" [HKEY_CLASSES_ROOT\CLSID\ {311F0CBD-FB08-3DB4-A87C-96EA3C30DA0D}\ Implemented Categories\ {62C8FE65-4EBB-45E7-B440-6E39B2CDBF29}] 

It will also generate the following registry entries for the type library:

 [HKEY_CLASSES_ROOT\TypeLib\{1D726C13-12E6-32A3-95F0-E30D9286344B}] [HKEY_CLASSES_ROOT\TypeLib\{1D726C13-12E6-32A3-95F0-E30D9286344B}.0] @="DotNetFinancial" [HKEY_CLASSES_ROOT\TypeLib\{1D726C13-12E6-32A3-95F0-E30D9286344B}.0 
 [HKEY_CLASSES_ROOT\TypeLib\{1D726C13-12E6-32A3-95F0-E30D9286344B}] [HKEY_CLASSES_ROOT\TypeLib\{1D726C13-12E6-32A3-95F0-E30D9286344B}\1.0] @="DotNetFinancial" [HKEY_CLASSES_ROOT\TypeLib\{1D726C13-12E6-32A3-95F0-E30D9286344B}\1.0\0] [HKEY_CLASSES_ROOT\TypeLib\{1D726C13-12E6-32A3-95F0-E30D9286344B}\1.0\0\win32] @="C:\\demos\\chap6\\bin\\dotnetfinancial.tlb" [HKEY_CLASSES_ROOT\TypeLib\{1D726C13-12E6-32A3-95F0-E30D9286344B}\1.0\FLAGS] @="0" [HKEY_CLASSES_ROOT\TypeLib\{1D726C13-12E6-32A3-95F0-E30D9286344B}\1.0\HELPDIR] @="C:\\demos\\chap6\\bin\\" 
] [HKEY_CLASSES_ROOT\TypeLib\{1D726C13-12E6-32A3-95F0-E30D9286344B}.0
 [HKEY_CLASSES_ROOT\TypeLib\{1D726C13-12E6-32A3-95F0-E30D9286344B}] [HKEY_CLASSES_ROOT\TypeLib\{1D726C13-12E6-32A3-95F0-E30D9286344B}\1.0] @="DotNetFinancial" [HKEY_CLASSES_ROOT\TypeLib\{1D726C13-12E6-32A3-95F0-E30D9286344B}\1.0\0] [HKEY_CLASSES_ROOT\TypeLib\{1D726C13-12E6-32A3-95F0-E30D9286344B}\1.0\0\win32] @="C:\\demos\\chap6\\bin\\dotnetfinancial.tlb" [HKEY_CLASSES_ROOT\TypeLib\{1D726C13-12E6-32A3-95F0-E30D9286344B}\1.0\FLAGS] @="0" [HKEY_CLASSES_ROOT\TypeLib\{1D726C13-12E6-32A3-95F0-E30D9286344B}\1.0\HELPDIR] @="C:\\demos\\chap6\\bin\\" 
\win32] @="C:\demos\chap6\bin\dotnetfinancial.tlb" [HKEY_CLASSES_ROOT\TypeLib\{1D726C13-12E6-32A3-95F0-E30D9286344B}.0\FLAGS] @="0" [HKEY_CLASSES_ROOT\TypeLib\{1D726C13-12E6-32A3-95F0-E30D9286344B}.0\HELPDIR] @="C:\demos\chap6\bin\"

Let's look first at the registry entries that regasm generated for the assembly. The first two entries contain the progID key for the DotNetFinancial.TimeValue class as shown here:

 [HKEY_CLASSES_ROOT\DotNetFinancial.TimeValue] @="DotNetFinancial.TimeValue" [HKEY_CLASSES_ROOT\DotNetFinancial.TimeValue\CLSID] @="{311F0CBD-FB08-3DB4-A87C-96EA3C30DA0D}" 

The value at the first key contains the name of the managed class that implements the ProgID ( DotNetFinancial.TimeValue ). The second key contains the usual pointer from the ProgID to its associated CLSID. Notice that the generated progID has the form assemblyname.classname. Once again, this is a default that you can affect using the ProgID attribute in System.Run-time.InteropServices. You change the definition of the managed TimeValue class to be as follows:

 using System.Runtime.InteropServices; namespace DotNetFinancial {     [Guid("8D37CFCB-1C0E-45bb-B1D6-FE21C07237B8")]  [ProgId("Financials.AlansFunkyClass")]  public class TimeValue { //... } } 

Regasm will use Financials.AlansFunkyClass as the ProgID, and the first two registry entries will now look as follows:

 [HKEY_CLASSES_ROOT\Financials.AlansFunkyClass] @="DotNetFinancial.TimeValue" [HKEY_CLASSES_ROOT\Financials.AlansFunkyClass\CLSID] @="{8D37CFCB-1C0E-45BB-B1D6-FE21C07237B8}" 

The rest of the assembly entries define the CLSID information for the TimeValue type beneath the HKEY_CLASSES_ROOT\CLSID key. Regasm synthesizes a CLSID from the full name of the assembly, that is, the friendly name (DotNetFinancial in this case), the version number, the public key token (if the assembly has a strong name), and the culture. You will need to keep this in mind because, unless you explicitly specify the CLSID that regasm should generate (you'll see how to do this shortly), the CLSID for the TimeValue class will change whenever you change the version number of the assembly.

Note

To guarantee that the generated GUID is unique to your organization, it is always a good idea to add a strong name to any managed code assemblies that you intend to use through COM Interop. When you get right down to it, it's a good idea to always add a strong name to your assemblies anyway.


In this case, regasm generated the following CLSID for the TimeValue class in the DotNetFinancial assembly:

 {311F0CBD-FB08-3DB4-A87C-96EA3C30DA0D} 

If you have a specific CLSID that you would like regasm to use, or if you would like to guarantee that the generated GUID will never change, you can use the GUID attribute in the System.Runtime.InteropServices namespace to associate a CLSID with the managed TimeValue class. For instance, if you change the C# class definition of the TimeValue class to look as follows:

 using System.Runtime.InteropServices; namespace DotNetFinancial { [Guid("8D37CFCB-1C0E-45bb-B1D6-FE21C07237B8")]     public class TimeValue     {     //...     } } 

then the CLSID generated by regasm for the TimeValue class will always be {8D37CFCB-1C0E-45bb-B1D6-FE21C07237B8} regardless of the version number of the DotNetFinancial assembly.

The CLSID key that regasm generated for the DotNetFinancial.TimeValue class in this case is shown here:

 [HKEY_CLASSES_ROOT\CLSID\ {311F0CBD-FB08-3DB4-A87C-96EA3C30DA0D}] @="DotNetFinancial.TimeValue" 

The default value at this key DotNetFinancial.TimeValue is the name of the managed class that implements the CLSID. The next registry entry is the most important one for any COM object, the InProcServer32 key, which is shown here:

 [HKEY_CLASSES_ROOT\CLSID\ {311F0CBD-FB08-3DB4-A87C-96EA3C30DA0D}\InprocServer32] @="D:\WINNT\System32\mscoree.dll" "ThreadingModel"="Both" "Class"="DotNetFinancial.TimeValue" "Assembly"="DotNetFinancial, Version=1.0.814.40736, Culture=neutral, PublicKeyToken=null" "RuntimeVersion"="v1.0.3328" 

This key will contain five values:

  • The default value for the key contains the path to the piece of software that implements the class identified by this CLSID. The specified path in this case points to the CLR's execution engine: D:\WINNT\System32\mscoree.dll . The execution engine does not implement the TimeValue object, but it does implement the CoGetClassObject entry point that the COM runtime will look for and call after it loads the execution engine. The execution engine will then load the managed class specified in the Class value beneath the InProcServer32 subkey (see the third bullet).

  • The ThreadingModel value specifies which COM Apartments this object may run in. All managed classes that are called through COM Interop use the Both threading model, which really means that the object can run in either a Single Threaded Apartment (STA) or a Multithreaded Apartment (MTA). The object will always run in the same Apartment as its client.

  • The Class value contains the name of the managed class that implements the CLSID, that is, the name of the managed class that the CLR should load when you call the object through a CCW.

  • The Assembly value contains the full name, that is, the friendly name, four-part version number, public key token, and culture of the assembly that implements the managed class identified in the previous Class value.

  • The RuntimeVersion value identifies the version of the CLR that the managed class will work with.

The next key contains the back-pointer from the CLSID to the ProgID.

 {311F0CBD-FB08-3DB4-A87C-96EA3C30DA0D}\ProgId] @="DotNetFinancial.TimeValue" 

The final key, which follows, contains the component categories for this COM class:

 [HKEY_CLASSES_ROOT\CLSID\ {311F0CBD-FB08-3DB4-A87C-96EA3C30DA0D}\ Implemented Categories\ {62C8FE65-4EBB-45E7-B440-6E39B2CDBF29}] 

Component categories provide a way to group related COM classes together. For instance, there is a component category that you can use to specify that a COM object is an ActiveX control. There is another component category that you can use to specify that a COM object is an embeddable object. Each component category has a GUID associated with it: a CATID. The CATID for the ActiveX control component category is {40FC6ED4-2438-11CF-A3DB-080036F12502}, and the one for an embeddable object is {40FC6ED3-2438-11CF-A3DB-080036F12502}. You can find all of the component categories on your system by looking under the HKEY_CLASSES_ROOT\Component Categories\ key on your machine. An object indicates that it is part of a category by adding a subkey with the desired CATID beneath its Implemented Categories under the object's CLSID subkey. The registry entries for a managed code object include a component category called .NET Category with GUID {62C8FE65-4EBB-45e7-B440-6E39B2CDBF29}.

The Type Library

The Assembly Registration Tool (regasm) also created a type library when I ran it. The IDL in the generated type library is as follows:

 1.  [ 2.   uuid(1D726C13-12E6-32A3-95F0-E30D9286344B), 3.   version(1.2) 4.  ] 5.  library DotNetFinancial 6.  { 7.      importlib("mscorlib.tlb"); 8.      importlib("stdole2.tlb"); 9. 10.     interface _TimeValue; 11.     [ 12.       uuid(311F0CBD-FB08-3DB4-A87C-96EA3C30DA0D), 13.       version(1.0), 14.         custom({0F21F359-AB84-41E8-9A78-36D110E6D2F9}, 15.         "DotNetFinancial.TimeValue") 16.     ] 17.     coclass TimeValue { 18.         [default] interface _TimeValue; 19.         interface _Object; 20.     }; 21. 22.     [ 23.       odl, 24.       uuid(F00794BD-914A-3411-A633-6FABAD549E80), 25.       hidden, 26.       dual, 27.         oleautomation, 28.         custom({0F21F359-AB84-41E8-9A78-36D110E6D2F9}, 29.         "DotNetFinancial.TimeValue") 30.     ] 31.     interface _TimeValue : IDispatch { 32.    }; 33.  }; 

The GUID for the type library (the LIBID), which you can see on line 2, is generated from the full assembly name just like the CLSID for the TimeValue class. The version number on line 3 is the major and minor version number of the managed code assembly. On line 7, you can see that the type library imports the type library for the mscorlib.dll assembly, which contains many of the types in the System namespace. The type library has to do this because it uses the _Object interface, which contains all of the methods that reside in the System.Object class that is the base class for all other types in .NET. The _Object contains such methods as ToString, GetType, GethashCode, and Equals. Line 10 is a forward declaration for the _TimeValue class interface. Lines 11 through 20 contain a declaration of the TimeValue coclass, which maps to the TimeValue class that I defined in the .NET assembly. The TimeValue class implements the _TimeValue and _Object interfaces with _TimeValue as the default. Lines 22 through 32 contain the declaration of the _TimeValue class interface.

In COM, you can only call methods on an interface. Coclasses simply group interfaces. If your managed class does not implement any managed interfaces, regasm will automatically create a COM interface that has the same name as the managed class ( TimeValue in this case) prefixed with an underscore . This automatically generated interface is called a Class Interface. It would seem logical that this interface would contain all of the methods that are declared in the managed TimeValue class, that is, LoanAmount and MonthlyPayment, but you will notice that it does not. Instead, the _TimeValue interface is an empty dual interface that derives from IDispatch. You have to use late binding through the Invoke method IDispatch to call the methods in the class interface.

The reason that regasm defines the class interface this way has to do with the way clients bind to methods in COM and the differences between the way that COM and .NET implement versioning. A COM object's interfaces form a binary-level contract between the COM object and its clients. COM interfaces are implemented as C++ style vtables, that is, an array of function pointers. When a COM-enabled compiler sets up an early-bound call to a COM object, it generates code to invoke the method pointed to by a specified index in the vtable. If this interface changes in any way (even if you just add a method), you can potentially break clients. This is why COM enforced its rule of immutable interfaces, that is, that an interface, after it is published, should never be changed. The problem is that there is no such immutable class rule for .NET. The .NET clients bind to classes dynamically at runtime, so it is permissible to add a method to a class without breaking a managed code client. Unfortunately, if you create a .NET object, early bind to it from an unmanaged client through COM Interop, and then alter the .NET class, you will probably break your early bound unmanaged clients because they are dependent on the vtable index of each method. The solution to this problem is not to allow early binding by default and require you to use late binding. You can get regasm to generate a dual interface with methods that you can early bind to using the ClassInterface attribute. The ClassInterface attribute, which resides in the System.Runtime.InteropServices namespace, has three possible values:

  • AutoDispatch This is the default and indicates that an empty IDispatch class interface will be created at runtime that only supports the methods in IDispatch. Hence, it can only be late bound. You should always call GetIDsOfNames from your unmanaged client to fetch the DispIDs for your methods. You should not cache the DispIDs in a wrapper class like the ColeDispatchDriver-derived classes do in MFC.

  • AutoDual Indicates that the runtime (and regasm when you run it) should generate a dual class interface that both derives from IDispatch and contains all of the methods that you declared in your managed class. You can thus either early bind or late bind to this interface. Early binding is discouraged because it does not version well.

  • None No class interface is generated. The only COM interfaces supported by the CCW (besides the system interfaces like IDispatch and ISupportsErrorInfo) are COM equivalents of the managed interfaces that are explicitly implemented by the object.

AutoDispatch is the default value for the ClassInterface attribute, so, if you do not specify a value for this attribute on your managed class, the COM interface that the CLR creates at runtime will contain only an empty IDispatch interface. If you specify AutoDual for the class interface attribute as follows:

 using System; using System.Runtime.InteropServices; namespace DotNetFinancial {     [ClassInterface(ClassInterfaceType.AutoDual)]     public class TimeValue     {     // implementation omitted for clarity...     } } 

Then the generated COM interface for your class in the CCW will contain all of the methods that you declared in your managed code class as follows:

 [   uuid(F26CCE1C-A2DC-3C16-B2B2-350FDA238CEE),   version(1.2) ] library DotNetFinancial {     importlib("mscorlib.tlb");     importlib("stdole2.tlb");     interface _TimeValue;     [       uuid(50D80C16-B2A3-35B4-8A67-DBD4940781C5),       version(1.0),         custom({0F21F359-AB84-41E8-9A78-36D110E6D2F9},         "DotNetFinancial.TimeValue")     ]     coclass TimeValue {         [default] interface _TimeValue;         interface _Object;     };     [       odl,       uuid(8C14F750-B657-3ACA-B95D-14B9733A5F29),       hidden,       dual,       nonextensible,       oleautomation,         custom({0F21F359-AB84-41E8-9A78-36D110E6D2F9},         "DotNetFinancial.TimeValue")     ]     interface _TimeValue : IDispatch {         [id(00000000), propget,        custom({54FC8F55-38DE-4703-9C4E-250351302B1C}, "1")]         HRESULT ToString([out, retval] BSTR* pRetVal);         [id(0x60020001)]         HRESULT Equals([in] VARIANT obj,                     [out, retval] VARIANT_BOOL* pRetVal);         [id(0x60020002)]         HRESULT GetHashCode([out, retval] long* pRetVal);         [id(0x60020003)]         HRESULT GetType([out, retval] _Type** pRetVal);         [id(0x60020004)]         HRESULT MonthlyPayment([in] short numMonths,                     [in] double interestRate,                     [in] wchar_t loanAmt,                     [out, retval] wchar_t* pRetVal);         [id(0x60020005)]         HRESULT LoanAmount([in] short numMonths,                     [in] double interestRate,                     [in] wchar_t monthlyPmt,                     [out, retval] wchar_t* pRetVal);     }; }; 

Notice that the _TimeValue class interface contains declarations for the LoanAmount and MonthlyPayment methods that were defined in the managed code class as well as the ToString, GetHashCode, GetType, and Equals methods from System.Object . Even though it might seem like AutoDual is a better choice than AutoDispatch, Microsoft does not recommend this approach because the layout of the automatically generated _TimeValue interface will change if you change the managed class. The best approach is either to stick with the AutoDispatch default and let your COM clients late bind to your managed code classes. Better yet, if you know that your managed code classes will be used by COM clients, you should implement managed interfaces in those classes to expose their functionality. COM equivalents of these managed code interfaces will be exposed to COM clients. I talk about this more in Chapter 8 when I talk about advanced aspects of .NET object to unmanaged client Interop.

Before I conclude this chapter, let's take a look at some unmanaged VB 6 and Visual C++ code that uses the managed DotNetFinancial.TimeValue class through COM Interop. Because it is so much easier to use, let's start with the VB 6 code.

The following code shows the key portion of the code for a VB 6 standard executable client that uses the DotNetFinancial.TimeValue class:

 Private Sub cmdPayment_Click()     On Error GoTo errHandler     Dim objFinancial As Object     Set objFinancial =         CreateObject("DotNetFinancial.TimeValue")     lblPayment.Caption =         objFinancial.MonthlyPayment(txtNumMonths.Text,         txtInterestRate, txtLoanAmount)     Exit Sub errHandler:     MsgBox Err.Description End Sub 

I am using late binding here, so I declare my object variable to be of type object (which maps to the IDispatch interface). I then instantiate the object using the CreateObject function, passing in the ProgID for the managed class. Unfortunately, unless you were prescient enough to have run regasm with the /codebase parameter or you installed the DotNetFinancial assembly in the GAC, you will probably receive the error shown in Figure 6-12 if you attempt to run this executable either from within the VB IDE or by compiling and then running the executable.

Figure 6-12. The CLR cannot locate the referenced assembly.

graphics/06fig12.jpg

This is the error that you get when the CLR cannot locate an assembly. If you refer back to the registry entries that regasm added, you can probably figure out why this didn't work. For a regular COM object, the default value of the InprocServer32 registry key contains the path to the DLL or executable that implements a CLSID. For a .NET class that is being used through COM Interop, the default value at the InprocServer32 key contains the path to the CLR's execution engine ( mscoree.dll ). Additional values beneath this registry key contain the full name of the assembly that implements the CLSID. By default, there is no path information, so the CLR must be able to locate the assembly using its usual search rules. That means that, in order for the client to work, the DotNetFinancial assembly must reside in one of the following areas:

  • In the same directory as the client's executable

  • In a bin or DotNetFinancial directory beneath the client executable's directory

  • In the GAC

If you cannot put the assembly in one of these locations, you must set the codebase for the assembly using regasm.

Note

When you run an executable within the VB 6 environment, the client executable is actually the VB IDE, that is, D:\Program Files\Microsoft Visual Studio\VB98\vb6.exe. Therefore, if you don't want to put the DotNetFinancial assembly in the GAC or use the codebase parameter on regasm, you must put the DotNetFinancial assembly in the same directory as vb6.exe.


To put the DotNetFinancial assembly in the GAC, run the following command at a Visual Studio .NET command prompt:

 gacutil i dotnetfinancial.dll 

Remember that, if you want to put the assembly in the GAC, you must sign it with a strong name first.

If you would rather use the /codebase option on regasm, just run regasm again with the following command line:

 regasm dotnetfinancial.dll /tlb:dotnetfinancial.tlb /codebase 

Regasm will add the following value beneath the InprocServer32 registry subkey for the CLSID:

 "CodeBase"=file:///C:/demos/chap6/bin/DotNetFinancial.DLL 

The CLR will search the codebase location for the assembly after it checks the GAC, but before it starts probing. Either using the GAC or the codebase parameter will cause the error shown in Figure 6-12 to go away. Microsoft recommends that you put the assembly in a directory beneath the client, but, if you can't do this, the GAC is a good second choice. Using the codebase parameter on regasm should be a last resort.

I'll conclude this chapter by looking at an unmanaged Visual C++ (MFC) client. I included this mainly so you can see clearly that using a managed code object through COM is exactly the same from the client side as using a regular COM object.

 1.  void CVcppclientDlg::OnCalculatePaymentButton() 2.  { 3.    HRESULT hRes; 4.    DISPID dispid; 5.    float fltResult; 6.    IDispatch *pDisp; 7.    OLECHAR *methodName=L"MonthlyPayment"; 8.    CLSID clsID; 9.    EXCEPINFO errorInfo; 10.   UINT intArg; 11.   VARIANT vntArgs[3], vntResult; 12.   DISPPARAMS param; 13.   param.cArgs=3; 14.   param.rgvarg=vntArgs; 15.   param.cNamedArgs=0; 16.   param.rgdispidNamedArgs=NULL; 17.   vntResult.vt=VT_R4; 18.   vntArgs[0].vt=VT_R4; 19.   vntArgs[0].fltVal=m_loanAmount; 20.   vntArgs[1].vt=VT_R4; 21.   vntArgs[1].fltVal=m_apr; 22.   vntArgs[2].vt=VT_I2; 23.   vntArgs[2].iVal=m_numMonths; 24. 25.   UpdateData(TRUE); 26.   hRes=::CLSIDFromProgID(27.     L"DotNetFinancial.TimeValue",&clsID); 28.   hRes=CoCreateInstance(clsID,NULL,CLSCTX_ALL, 29.     IID_IDispatch,(void**)&pDisp); 30.   hRes=pDisp->GetIDsOfNames(IID_NULL,&methodName,1, 31.     GetUserDefaultLCID(),&dispid); 32.   hRes=pDisp->Invoke(dispid,IID_NULL, 33.     GetUserDefaultLCID(),DISPATCH_METHOD, 34.     &param,&vntResult,&errorInfo,&intArg); 35.   VarR4FromDec(&vntResult.decVal, &fltResult); 36.   m_payment.Format("%.2f",fltResult); 37.   UpdateData(FALSE); 38.   pDisp->Release(); 39. } 

The code in this example is much longer than the VB code, but it is functionally equivalent. On lines 3 through 23, I declare all the variables that I need to make an Invoke call on an IDispatch interface pointer. The important lines are line 7 where I declare a string that holds the name of the method that I plan to call and lines 11 through 23 where I set up the DISPPARAMS structure that will contain the parameters that I pass to the method. Keep in mind that, when using Invoke on a method that has more than one parameter, you must pass the parameters in the reverse order, that is, the parameter that appears last in the parameter list for the method will be at index zero in your parameter array. The UpdateData call on line 25 is for MFC UI processing. On line 26, I use the CLSIDFromProgID COM API method to fetch the CLSID for the managed class. On line 28, I call CoCreateInstance, passing in the CLSID that I received on line 26 and specifying that I want to obtain an IDispatch interface on the object. On line 30, I call GetIDsOfNames to fetch the Dispatch ID (DispID) of the MonthlyPayment method. Lines 32 through 34 are probably the most important lines in this example because this is where I actually call the Invoke method on the IDispatch pointer. On lines 35 thru 37 we process the results for display, and on line 38 we release the IDispatch interface pointer. Note that I have completely ignored error checking in this function for the sake of clarity. The code that I just showed is exactly the same as the code that you would write for any COM object regardless of how it was implemented.


Team-Fly    
Top
 


. Net and COM Interoperability Handbook
The .NET and COM Interoperability Handbook (Integrated .Net)
ISBN: 013046130X
EAN: 2147483647
Year: 2002
Pages: 119
Authors: Alan Gordon

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net