Class Interfaces Revisited

Team-Fly    

 
.NET and COM Interoperability Handbook, The
By Alan Gordon
Table of Contents
Chapter Eight.  Advanced COM to .NET Interop

Class Interfaces Revisited

You always interact with a COM object through the interface(s) that it implements. A managed object, however, is not required to implement any interfaces. Therefore, when you use a managed object from an unmanaged client, one of the first questions you must ask yourself is what COM interface should you use to talk to the managed object? In Chapter 6, you learned that, by default, the CCW will expose a late-bound (IDispatch) interface through which you can call all of the public methods that the managed object exports. You can either use this IDispatch interface to call the managed object's methods (by running a QueryInterface for IID_IDispatch in Visual C++ or declaring your object variable to be of type object in Visual Basic), or you can use an empty IDispatch-derived interface that regasm will generate for you. This interface is called a class interface. For instance, a managed class that is declared as shown here in C#:

 namespace DotNetFinancial {     public class TimeValue     {       public TimeValue()       { }       public decimal MonthlyPayment(           short numMonths,           double interestRate,           decimal loanAmt)       {       //...       }       public decimal LoanAmount(           short numMonths,           double interestRate,           decimal monthlyPmt)       {       //...       }     } } 

will produce the following interface and coclass when you generate a type library using the assembly registration tool:

 [    odl,    uuid(F00794BD-914A-3411-A633-6FABAD549E80),    hidden,    dual,    oleautomation,    custom({0F21F359-AB84-41E8-9A78-36D110E6D2F9},    "DotNetFinancial.TimeValue") ] interface _TimeValue : IDispatch { }; [    uuid(311F0CBD-FB08-3DB4-A87C-96EA3C30DA0D),    version(1.0),    custom({0F21F359-AB84-41E8-9A78-36D110E6D2F9},    "DotNetFinancial.TimeValue") ] coclass TimeValue {    [default] interface _TimeValue;    interface _Object; }; 

The class interface is _TimeValue. Notice that this interface contains no methods. You call methods in this interface in a late-bound manner, that is, by calling the Invoke method on IDispatch. If you want an early-bound interface, you can get the CCW to expose a COM dual interface by adding the ClassInterface attribute to the TimeValue class and specifying AutoDual as shown here:

 namespace DotNetFinancial {     [ClassInterface(ClassInterfaceType.AutoDual)]     public class TimeValue     {       public TimeValue()       { }       public decimal MonthlyPayment(           short numMonths,           double interestRate,           decimal loanAmt)       {       //...       }       public decimal LoanAmount(           short numMonths,           double interestRate,           decimal monthlyPmt)       {       //...       }     } } 

Doing this will yield the following interface when you use (regasm.exe):

 [       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)]         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);     }; 

With this approach, you can either call the LoanAmount or MonthlyPayment methods directly on the _TimeValue interface, or you can still make late-bound calls through IDispatch.

Although both the AutoDispatch and AutoDual settings are interesting, there are major problems with each of these approaches. With AutoDispatch, the biggest problem is that you must use late binding. With unmanaged Visual Basic (that is, Visual Basic 6), the only thing you really give up is IntelliSense, but, with Visual C++, using the IDispatch interface is verbose, error prone, and complicated, as you have already seen with many of the code samples in this chapter. Using AutoDual has a different set of problems that are caused by the different approaches to versioning that .NET and COM use. With COM, published interfaces are immutable, that is, after you publish an interface, you are not allowed to change it. This is the way that COM handles the versioning problem. As long as the old interfaces that existing clients use are always supported, they should (in theory, at least) never break.

The .NET Framework takes a different approach to solving the versioning problem. It uses private assemblies and side-by-side execution. In most cases, an application should be deployed with its own private versions of the assemblies that it is dependent on. It's not a problem if two applications use different versions of an assembly because the CLR can load and execute the code in two different versions of the same assembly simultaneously . Even assemblies that are shared through the GAC don't require backward compatibility because the GAC can store multiple versions of the same assembly. Therefore, with .NET components, there is no compelling need to maintain backward compatibility when updating your components . If you want to change the signature of a method (thus changing an object's interface), you can do it, as long as you change the version number of the component. As long as you continue to deploy the old version on client machines, either as a private assembly or as a shared assembly in the GAC, existing clients will continue to work.

Although you are not required to maintain backward compatibility with your managed components, you will break COM clients who are using your components through COM Interop if you use the AutoDual ClassInterface setting and if clients early bind to the corresponding COM interface. Because of this and the aforementioned problems with the AutoDispatch setting, the recommended approach is to use the None setting on the ClassInterface attribute as follows :

 [ClassInterface(ClassInterfaceType.None)] public class TimeValue {     public TimeValue()     {     }     public decimal MonthlyPayment(short numMonths,           double interestRate, decimal loanAmt)     {     //...     }     public decimal LoanAmount(short numMonths,           double interestRate, decimal monthlyPmt)     {     //...     } } 

This will cause the CCW to expose only a generic IDispatch interface as opposed to the empty IDispatch-derived interface that you get when you use the ClassInterface attribute with the AutoDispatch setting. This yields the following definition of the TimeValue coclass in the type library if you used the assembly registration tool:

 [      uuid(57290E60-9265-359D-859E-B26FBB7861CE),      version(1.0) ] coclass TimeValue {     [default] interface _Object; }; 

In this case, the TimeValue coclass supports a single interface called _Object that contains the ToString, GetHashCode, and GetType methods from the System.Object class. The _Object interface derives from IDispatch, so you can still call all of the TimeValue classes' methods using the Invoke method of IDispatch. If you want to support an early-bound interface, you must explicitly define and implement managed interfaces in your managed classes. These interfaces will be exposed as COM interfaces when you use COM Interop. Therefore, if I change the definition of the TimeValue class to look as follows:

  public interface ITimeValue   {   decimal MonthlyPayment(short numMonths,   double interestRate, decimal loanAmt);   decimal LoanAmount(short numMonths,   double interestRate, decimal monthlyPmt);   }   [ClassInterface(ClassInterfaceType.None)]  public class TimeValue  : ITimeValue  {     public TimeValue()     {     }     public decimal MonthlyPayment(short numMonths,           double interestRate, decimal loanAmt)     {     // Implementation omitted...     }     public decimal LoanAmount(short numMonths,           double interestRate, decimal monthlyPmt)     {     // Implementation omitted...     } } 

the regasm-generated COM interface and the coclass in the type library will now look as follows:

 [    odl,    uuid(3B36FDAF-A541-3765-8853-1397B24BB95A),    version(1.0),    dual,    oleautomation  ]  interface ITimeValue : IDispatch {      [id(0x60020000)]      HRESULT MonthlyPayment(                   [in] short numMonths,                   [in] double interestRate,                   [in] wchar_t loanAmt,                   [out, retval] wchar_t* pRetVal);      [id(0x60020001)]      HRESULT LoanAmount(                   [in] short numMonths,                   [in] double interestRate,                   [in] wchar_t monthlyPmt,                   [out, retval] wchar_t* pRetVal);  };  [    uuid(57290E60-9265-359D-859E-B26FBB7861CE),    version(1.0)  ]  coclass TimeValue {      interface _Object;      [default] interface ITimeValue;  }; 

You can use this version of the managed TimeValue class from an unmanaged Visual C++ client as follows:

 1.      HRESULT hRes; 2.      DECIMAL decLoanAmt,decMonthlyPmt; 3.      ITimeValue *pTimeValue; 4. 5.      CoInitializeEx(NULL,COINIT_MULTITHREADED); 6.      hRes=CoCreateInstance(CLSID_TimeValue,NULL, 7.        CLSCTX_ALL,IID_ITimeValue, 8.        (void **)&pTimeValue); 9.      VarDecFromR8(360000,&decLoanAmt); 10.     hRes=pTimeValue->MonthlyPayment(360, 11.       6.75,decLoanAmt,&decMonthlyPmt); 12.     pTimeValue->Release(); 

Notice how simple this code is compared to the late-bound code samples that you have looked at recently. Lines 1 through 3 have variable declarations. Line 5 enters the MTA. Lines 6 through 8 create an instance of the TimeValue class and request the ITimeValue interface. Line 9 converts the value 360,000 to a decimal, and lines 10 and 11 call the MonthlyPayment method. Line 12 releases the ITimeValue interface pointer. In order to write this code, you must add the following code to the stdafx.h file in your project:

 #import "[path]\dotnetfinancial.tlb" no_namespace named_guids raw_interfaces_only 

where "[path]" is the path to the type library for your assembly.

By default, the ITimeValue interface will be exposed as a dual interface, but you can change this using the InterfaceType attribute. The following code shows how you would change the ITimeValue interface so that it will be exposed to COM clients as an IUnknown-derived interface:

 [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] public interface ITimeValue {     decimal MonthlyPayment(short numMonths,           double interestRate, decimal loanAmt);     decimal LoanAmount(short numMonths,           double interestRate, decimal monthlyPmt); } 

Notice that I used the InterfaceType attribute to specify that the managed interface should have an IUnknown-derived COM equivalent. The InterfaceType attribute has the following three values:

  • InterfaceIsDual Indicates that the managed interface should be exposed to COM clients as a dual interface.

  • InterfaceIsDispatch Indicates that the managed interface should be exposed to COM clients as a dispatch interface.

  • InterfaceIsIUnknown Indicates that the managed interface should be exposed to COM clients as an IUnknown-derived interface.

After you make this change to the managed interface, regasm will now generate the following COM interface for unmanaged clients:

 [    odl,    uuid(3B36FDAF-A541-3765-8853-1397B24BB95A),    version(1.0),    oleautomation  ]  interface ITimeValue :  IUnknown  {      HRESULT _stdcall MonthlyPayment(              [in] short numMonths,              [in] double interestRate,              [in] wchar_t loanAmt,              [out, retval] wchar_t* pRetVal);      HRESULT _stdcall LoanAmount(              [in] short numMonths,              [in] double interestRate,              [in] wchar_t monthlyPmt,             [out, retval] wchar_t* pRetVal);  }; 

In some cases, you may also want to specify the Interface Identifier (IID) that regasm should generate for the unmanaged version of the interface. You can do this using the GUID attribute in the System.Runtime.InteropServices namespace. The following code shows the ITimeValue managed interface declared in C# with a specified GUID:

 [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] [Guid("FB66D91D-7A53-490f-ABEB-A53486A516C2")] public interface ITimeValue {     decimal MonthlyPayment(short numMonths,           double interestRate, decimal loanAmt);     decimal LoanAmount(short numMonths,           double interestRate, decimal monthlyPmt); } 

This GUID will be used as the GUID for the ITimeValue interface in its COM equivalent.


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