技术开发 频道

使用.NET应用的Classic COM组件


简介

    在熟悉了.NET Beta 1 & Beta 2 bits之后,大部分开发者都会赞同这样的观点:对于企业来说,.NET 技术绝对是创建组件和分布式系统的强有力的方法。但是,我们又该如何看待在过去的日子里早已创建的那些可再度使用的大量COM组件,更不用说创建这些组件时你所花的物力人力还有你所付出的那些无眠的夜晚,它是.NET 世界中所有组件的终结吗?这些组件将会和.NET 托管运行库一起为我们工作吗?

    对于那些靠编写COM组件来谋生的人,以及那些唱着“COM就是爱”这首颂歌的人来说,这里有一个好消息。COM组件仍然会继续下去,.NET 托管应用程序可以兼容现有的COM 组件。当然,微软不会强迫公司禁止它们现有的组件,尤其是那些以使用得最广泛的对象模式这一方法编写的组件(这些组件是为了开发桌面和分布式应用程序而设计的)。Classic COM 组件通过一个interop 层与.NET 运行库进行互操作,此interop 层将处理信息转化之间的所有plumbing ,这些转化信息在托管运行库和COM 组件(此COM 组件操作非托管领域)之间循环运行,反之亦然。

    现在,让我们看一下另一个方面。如果你决定使用以通用语言执行层为目标的.NET友好语言编译你的组件,而且仍然需要使用这些来自于COM 感知客户端(例如:VB 6.0 或者Classic ASP)的.NET 组件时,将会发生什么情况?更多的将是失望的对待非. COM 感知客户端,而并非愉快的通过COM Interop 与.NET 组件合作。附带有.NET 结构的工具允许你向COM感知客户端展示.NET 组件,好像它们就是普通的COM 组件。COM Interop 处理covers 下面所有枯燥乏味的工作和plumbing 。在文章的第一部分,我们将重点集中于:怎样可以使COM组件和.NET一起工作,然后在后面的部分,我们将研究在非托管世界中怎样才能使用来自于COM客户端的.NET组件。在文章的末尾部分,希望你具备足够的基础知识,能理解Classic COM 和.NET结构怎样才能和平共处,一起发挥作用。那么,如果你已经准备好了,让我们一起来探讨Classic COM是怎样与.NET世界里的各大方案相匹配的。
 
使用.NET应用程序的Classic COM 组件

开始

    怎样向.NET应用程序展示一个Classic COM组件?任务的第一个原则就是使用ATL(此ATL为特定定期航线提供抵达详情)编写一个简单的COM组件。为了简洁一点,我们仅仅为'Air Scooby IC 5678'定期航线返回一些详情,而给其它航线提供的是错误。通过这种方法可以看到:由COM组件引起的错误是怎样传回来,并且被.NET客户端应用程序发现调用的。
下面是IAirlineInfo接口的IDL定义。
interface IAirlineInfo : IDispatch 
{

[id(1), helpstring("method GetAirlineTiming")]
HRESULT GetAirlineTiming([in] BSTR bstrAirline,
[out,retval] BSTR* pBstrDetails);

[propget, id(2), helpstring("property LocalTimeAtOrlando")]
HRESULT LocalTimeAtOrlando([out, retval] BSTR *pVal);

};


   这里是GetAirlineTiming方法的执行情况。
CAirlineInfo::GetAirlineTiming(BSTR bstrAirline, BSTR *pBstrDetails) 
{
_bstr_t bstrQueryAirline(bstrAirline);
if(NULL == pBstrDetails) return E_POINTER;

if(_bstr_t("Air Scooby IC 5678") == bstrQueryAirline)
{
*pBstrDetails = _bstr_t(_T("16:45:00 - Will arrive at Terminal 3")).copy();

}
else
{
return Error(LPCTSTR(_T("Airline Timings not available for this Airline" )),
__uuidof(AirlineInfo), AIRLINE_NOT_FOUND);
}
return S_OK;

}
    熟悉了组件之后,让我们看一下怎样从组件类型库生成某种元数据,从而.NET客户端可以使用这个元数据与组件对话,并且调用它的方法。

从COM类型库生成元数据



    与COM组件对话的.NET应用程序不能直接地使用这个它的功能。因此需要生成某种元数据。运行库使用这个元数据层,以此来收集类型信息,从而它可以在运行时使用这个类型信息,生产所谓的Runtime Callable Wrapper (运行库可调用的包装)。 运行库可调用的包装处理COM 对象的实际激活,而且当.NET应用程序与它相作用时,处理marshaling要求。运行库可调用的包装 也做其它大量的琐碎的事情,例如:操作标识对象,标识生命周期,以及接口缓存。在这里,对象生命周期管理是一个非常重要的问题,因为.NET 运行库不停地移动对象,回收站则回收。运行库可调用的包装提供了下面这一服务——它通知.NET应用程序,它正在与托管 .NET组件相互作用,并在非托管空间中提供COM组件,并且它让.net应用程序给人们一个印象——.net应用程序是一个优秀的使用了很久的COM客户端。运行库可调用的包装的创建和行为随着你是前期绑定还是晚期绑定到COM对象而不同。在看似简单的背后,运行库可调用的包装正在做所有困难的事情,把所有方法调用都形实转换成相应的虚函数表呼叫,再转移到COM组件中,此COM组件属于非托管世界。它是托管世界和非托管IUnknown世界之间的友善大使。

    让我们为Airline COM组件生成一个元数据包装器。为了完成这一目标需要使用一个被称为TLBIMP.exe的工具,The Type library Importer (TLBIMP)与.NET结构SDK一起运行,在你的SDK安装的Bin子文件夹中可以找到Type library Importer (TLBIMP)。Type library Importer (TLBIMP)工具阅读一个类型库,生成相应的元数据包装器,此元数据包装器包括.NET运行库所能支持的类型信息。


   从DOS命令行,键入以下命令。
TLBIMP AirlineInformation.tlb /out:AirlineMetadata.dll
   这个命令告诉类型库导入程序阅读AirlineInfo COM类型库,并且生成相应的元数据包装器,此元数据包装器被称为AirlineMetadata.dll.如果一切顺利,应该看到一个信息,此信息暗示元数据代理已经从类型库产生。
Type library imported to E:\COMInteropWithDOTNET\AirlineMetadata.dll
    这个生成的元数据包括什么样的类型信息,此信息是什么样子的?像COM 爱好者一样,我们一直喜欢OleView.exe,但是有时候感觉需要浏览一下类型库的内容,或者是瞥一下OleView 所能做的大量事情。幸运的是,.NET SDK 与一个被称为ILDASM的分解器 一起运转,此ILDASM反汇编器允许浏览此元数据以及为托管程序集所生成的中间语言代码。每一个托管程序集包括自由描写的元数据,当需要研究中间语言代码和元数据时,ILDASM是一个非常有用的工具,能给许多帮助。继续向前,使用ILDASM打开AirlineMetadata.dll。仔细看一下被生成的元数据,将发现GetAirlineTiming 方法被列为IAirlineInfo 接口的公共方法,此IAirlineInfo 接口由AirlineInfo 类型执行操作。这里也有一个构造器,是为AirlineInfo 类型准备的。同时也用方法参数的数据类型和返回值来代替等同的.NET 对应体。在例子中,附带有BSTR 数据类型的GetAirlineTiming方法参数被string(System.String的别名)数据类型取代了。同时也要注意GetAirlineTiming方法中标记有[out,retval]的参数被转换成方法的实际返回值(作为string而返回)。任何失败的从COM 对象返回来的HRESULT值(假设是一个错误或者是失败的非选拔的商业逻辑)是作为.NET异常而返回的。


绑定到COM组件,从.NET应用程序调用它的方法

   既然已经生成了.NET客户端要求的元数据,让我们在COM对象中从.NET客户端调用GetAirlineTiming方法。这里有一个简单的C#客户端应用程序,此C#客户端应用程序通过使用元数据(我们先前生成的)创建了COM对象,并且调用GetAirlineTiming方法。第一个方法调用命令应该运行得很顺畅,我们重新得到定期航线"Air Scooby IC 5678".的详情。然后,我们在一个未知的定期航线"Air Jughead TX 1234"中运行,因此COM对象给我们带来我们先前定义的AIRLINE_NOT_FOUND错误。

Collapse 
.......

String strAirline = "Air Scooby IC 5678";
String strFoodJunkieAirline = "Air Jughead TX 1234";
try
{
AirlineInfo objAirlineInfo;
objAirlineInfo = new AirlineInfo();

// 调用 GetAirlineTiming() 方法
System.Console.WriteLine("Details for Airline {0} --> {1}",
strAirline,
objAirlineInfo.GetAirlineTiming(strAirline));

System.Console.WriteLine("Details for Airline {0} --> {1}",
strFoodJunkieAirline,
objAirlineInfo.GetAirlineTiming(
strFoodJunkieAirline));
}
catch(COMException e)
{
System.Console.WriteLine("Oops an error occured !. Error Code is : {0}.
Error message is : {1}",e.ErrorCode,e.Message);
}
这是输出的情况:
Details for Airline Air Scooby IC 5678 --> 16:45:00 - Will arrive at Terminal 3 
Oops an error occured !. Error Code is : -2147221502.
Error message is : Airline Timings not available for this Airline
    在更深入的层面,运行库虚构一个运行库可调用的包装 ,此运行库可调用的包装映射元数据代理类型方法,方法的领域,以及由接口(此接口由COM对象执行)显示的属性。为每一个COM 对象实例创建了一个运行库可调用的包装 实例。.NET 执行时期仅仅在乎管理运行库可调用的包装 的生命周期,回收站回收运行库可调用的包装 。运行库可调用的包装处理将引用数保持在被它映射的COM 对象上,因此,能保护.NET 运行库,使它远离管理实际COM 对象上的引用数。正如ILDASM图中所示,AirlineInfo元数据类型被定义在一个被称为AirlineMetadata的命名空间下面。这个类型执行IAirlineInfo 接口。你所要做的就是:通过使用new操作器,仅仅创建一个AirlineInfo 类型实例,并且调用被创建的对象的公共类型方法。当调用了方法时,运行库可调用的包装形实转换这个调用到相应的COM 方法。运行库可调用的包装也处理所有的封送拆收和对象生命周期问题。对于.NET客户端,除了实际上创建一个托管对象,调用其中一个公共类型成员以外,它没有作其他事情。无论什么时候,当COM方法引起一个错误时,COM错误会被运行库可调用的包装限制,此错误会被转换成对等的COMException类型(可以在System.Runtime.InteropServices命名空间里找到)当然,COM对象仍然需要执行ISupportErrorInfo和IErrorInfo接口,使这个错误传播能运行工作,因此运行库可调用的包装知道对象提供了扩展错误信息。.NET客户端使用平常的try-catch异常处理机制检查错误,并且此错误能访问得到实际错误数目、描写,异常的来源以及其它任何COM感知客户端能利用到的详情。你也能返回标准HRESULTs,运行库可调用的包装将它们映射到相应的.NET异常,并返回到客户端。例如:如果想从COM方法返回一个E_NOTIMPL的HRESULT,运行库可调用的包装将会把它映射到.NET NotImplementedException异常,并且引发那个类型的异常。

    请阅读这篇文章中的Exception Handling那一部分,可以了解更多的关于.NET是怎样被映射到COM HRESULTs.的知识。



访问其他支持接口以及动态类型查询

    从.NET客户端的角度来看,如果COM对象执行一个特定的接口,它需要检查时,经典的QueryInterface方案是怎样工作的。对于另一个接口的QI,你所要做的就是把对象转换到你正在质疑的接口,如果它成功了,voilà, 你的 QI也就成功了。万一你想把对象转换到对象所不支持的某种任意接口,就要引发一个System.InvalidCastException异常,暗示QI失败了。这个很简单。运行库可调用的包装做简单外表下面所有做枯燥的工作。当你为另一个相联系的类型对象创建一个对象类型时,它就像VB运行库一样,使得不需要编写任何与显式QueryInterface相关联的代码,就能简单地创建QI。

   如果当前拥有的对象实例支持或者执行一个特定接口类型,此时另一个可选择的检查方案就是:使用C#'s 'is' operator。'is' operator操作执行时检查,看对象能否安全地转换到一个特定的类型中。如果结果证明是正确的,然后你可以安全地执行一个转型,完成QI。通过这种方法,运行库可调用的包装使你相信你正在转型到接口(这些接口被COM对象操作),而不是任何任意接口类型。你也可以使用C#'s 'as' operator,从一个类型转型到另一个相容的类型(正如下面的例子所示)。这些简单的构造是你在接口之间所要保持的转换,而COM对象以类型安全方式支持这些接口。
Collapse
.......
try
{
AirlineInfo objAirlineInfo = null;
IAirportFacilitiesInfo objFacilitiesInfo = null;

// 创建一个新的AirlineInfo 对象
objAirlineInfo = new AirlineInfo();

// 引用 GetAirlineTiming 方法
String strDetails = objAirlineInfo.GetAirlineTiming(strAirline);

// 检查AirlineInfo对象是否支持(C#的IS操作)IairportFacilitiesInfo的接口
if(objAirlineInfo is IAirportFacilitiesInfo)
{
objFacilitiesInfo = (IAirportFacilitiesInfo)objAirlineInfo;

objFacilitiesInfo = objAirlineInfo as IAirportFacilitiesInfo;

//引用IairportFacilitiesInfo接口上的一个方法
System.Console.WriteLine("{0}",
objFacilitiesInfo.GetInternetCafeLocations());

}

// 检查任意接口类型
if(objAirlineInfo is IJunkInterface)
{
System.Console.WriteLine("We should never get here ");
}
else
{
System.Console.WriteLine("I'm sorry I don't implement" +
" the IJunkInterface interface ");
}

IJunkInterface objJunk = null;
objJunk = (IJunkInterface)objAirlineInfo;

}/* 结束try */
catch(InvalidCastException eCast)
{
System.Console.WriteLine("Here comes trouble" +
" ... Error Message : {0}",
eCast.Message);

}/* 结束 catch */
.......
下面是输出的情况:
Your nearest Internet Cafe is at Pavilion 3 in Terminal 2 - 
John Doe's Sip 'N' Browse Cafe
I'm sorry I don't implement the IJunkInterface interface
Here comes trouble ... Error Message :
An exception of type System.InvalidCastException was thrown.


    后期绑定COM对象


    上面看到的所有例子使用了元数据代理前期绑定.NET客户端到COM对象中。前期绑定提供了很多好处,例如:在编译时的类型检查,为工具开发从类型信息中提供了自动完成功能,当然,还有更好的性能,当真正需要晚期绑定Classic COM 对象时,而正在绑定的COM 对象没有编译时间元数据时,这里可能有一些实例。我们可以通过Reflection 机制来晚期绑定COM 对象。这个并不只适合于COM 对象。甚至.NET 托管对象可以被晚期绑定,并且可以通过使用Reflection 来下载。而且,如果对象仅仅执行一个纯调度接口,那样在很大程度上,限制仅仅使用Reflection 来激活对象,调用它的接口上的方法。

    对于COM 对象的晚期绑定,必须了解对象的ProgID or CLSID 。System.Activator 类型的CreateInstance静态方法允许你为一个特定的类型指定Type 信息,并且它将自动地创建那个特定类型的对象。但是真正拥有的只有COM 对象的COM ProgID 和 COM CLSID,并没有真正的.NET Typ信息。因此,通过使用System.Type类型的GetTypeFromProgID 或者GetTypeFromCLSID静态方法,我们需要从ProgID or CLSID得到Type 信息。System.Type 类型是Reflection 的其中一个核心推动力量。当使用Activator.CreateInstance,创建了一个对象实例后,使用Type对象(你从Type.GetTypeFromProgID 或者Type.GetTypeFromCLSID中得到的)的System.Type.InvokeMember方法,可以调用对象支持的任何方法/属性。所要知道的就是,方法的名称和属性,调用接受的参数类型。参数被捆绑成一个普通的System.对象阵列,并且被传送给方法。根据你是正在调用一个方法或者还是在创建属性值,你也需要创建正确的binding flags。这就是COM对象晚期绑定的全部内容。
Collapse 
.......
try
{

objAirlineLateBound;
Type objTypeAirline;

// 创建数组容器对象
// 方法的输入参数
[] arrayInputParams= { "Air Scooby IC 5678" };

//获取类型信息
objTypeAirline =
Type.GetTypeFromProgID("AirlineInformation.AirlineInfo");

// .NET 与System.Type的关联
objTypeAirline = Type.GetTypeFromCLSID(new Guid(
"{F29EAEEE-D445-403B-B89E-C8C502B115D8}"));


objAirlineLateBound = Activator.CreateInstance(objTypeAirline);

// 引用'GetAirlineTiming' 方法
String str = (String)objTypeAirline.InvokeMember("GetAirlineTiming",
BindingFlags.Default |
BindingFlags.InvokeMethod,
null,
objAirlineLateBound,
arrayInputParams);

System.Console.WriteLine("Late Bound Call" +
" - Air Scooby Arrives at : {0}",str);

// 获取'LocalTimeAtOrlando' 的值
String strTime = (String)objTypeAirline.InvokeMember("LocalTimeAtOrlando",
BindingFlags.Default |
BindingFlags.GetProperty,
null,
objAirlineLateBound,
new object []{});

Console.WriteLine ("Late Bound Call - Local" +
" Time at Orlando,Florida is: {0}",
strTime);

}/* 结束try */
catch(COMException e)
{
System.Console.WriteLine("Error code : {0}, Error message : {1}",
e.ErrorCode, e.Message);
}/* 结束catch */
.......
   输出情况如下:

Late Bound Call - Air Scooby Arrives at 16:45:00 - Will arrive at Terminal 3 
Late Bound Call - Local Time at Orlando,Florida is: Sun Jul 15 16:50:01 2001

Classic COM中的事件处理连接点和.NET中的委托事件模式


    Connection Points 事件处理机制,正如你所知道的,是其中一个主要的推动力量,用于你的COM 组件和组件使用者之间的双向交流。只是为了唤醒记忆,我将简要地叙述一下Classic COM 组件中的事件处理机制。尤其是支持事件通知的,此COM组件有一个输出接口。 当特殊事件发生时,组件用它来调用客户端。在组件IDL 文件的coclass 这一章中,输出接口标记有[source] 属性。IDL 中的[source]属性允许开发工具和IDEs语法分析类型库,检查对象是否支持一个输出接口 。这些组件的使用者或者客户端通常建立一个接受对象, 此接受 对象,执行这个输出接口。这个接受对象的接口指针由客户端传输到组件。这个组件中断输出接口指针,就像一张包括接受 对象的输出接口指针的机器,而机器中的接受对象乐于从组件中接受通知。无论何时,只要组件需要引起事件,它使用机器来得到接受对象(接受对象预定了通知)的接口指示表。然后,它通过调用输出接口(由接受对象执行)上各自的方法来通知它们。

Classic COM中的连接点

    本质上,COM 对象支持输出接口,执行IConnectionPointContainer 接口。一个想要接受事件通知的客户端为IConnectionPointContainer 接口在COM 对象上面创建QI ,以此来看它是否支持输出接口。如果QI 失败了,对象就不支持事件。如果QI 成功了,客户端通过传输输出接口的IID,调用IConnectionPointContainer 接口上的FindConnectionPoint 方法(也叫EnumConnectionPoints方法)。如果支持此接口,客户端重新接受到与输出接口 相应的IConnectionPoint 接口指针。它然后调用IConnectionPoint::Advise 方法,把它传输到接受对象IUnknown 指针。COM对象为它的机器增添这个IUnknown指针,以此来保持接受对象表,而这些对象表预先定购了通知。客户端从COM对象收到一个cookie,接下来它可以用此cookie来撤销事件通知。当COM对象需要引起事件时,它通过机器来迭代 ,得到所有接受对象的接口指针表,调用输出接口(由接受对象执行)上面相应的事件方法。当客户端不想再接受通知时,通过调用IConnectionPoint::Unadvise方法(而IConnectionPoint::Unadvise方法是靠传递它先前在IConnectionPoint::Advise调用中接受到的cookie而完成的)它从目标机器中移出来。


    简而言之,那就是事件处理机制和Classic COM组件中双向交流的工作过程。大部分关于程序设计的书用了一整章来解释这个结构体系,你可能需要查阅它们来进一步了解这个主题。
创建ATL COM组件


    让我们来看一下,COM 中的连接点事件处理机制是怎样翻译成.NET世界中委托事件处理机制的。同时也让我们来看一下看怎样使用.NET 托管 事件接受来检查事件通知,此事件通知是由COM对象发送来的。首先,让我们来研究COM对象,它正在向你的.NET应用程序的事件源运行。无论什么时候,只要航线到达一个假定的飞机场(此飞机场都被称为John Doe International Airport)时,让我们创建一个简单的COM对象(此COM对象将呼叫你的.NET应用程序)。我们将先从我们的.NET应用程序预定这个服务,因此,无论飞机什么时候在John Doe机场的跑道上着陆都可以传呼。


   
    创建一个ATL EXE项目,此ATL EXE项目装有一个AirlineArrivalPager对象。AirlineArrivalPager对象支持输入接口(此接口被称为IAirlineArrivalPager)和输出的接口(此接口被称为IAirlineArrivalPagerEvents)。这里是IAirlineArrivalPagerEvents 输出接口的接口定义。在coclass定义中,这个接口被标记有[source]属性。

..... 

interface IAirlineArrivalPager : IDispatch
{
[id(1), helpstring("method AddArrivalDetails")]
HRESULT AddArrivalDetails([in] BSTR bstrAirlineName,
[in] BSTR bstrArrivalTerminal);
};

....

dispinterface _IAirlineArrivalPagerEvents
{
properties:
methods:
[id(1), helpstring("method OnAirlineArrivedEvent")]
HRESULT OnAirlineArrivedEvent([in] BSTR bstrAirlineName,
[in] BSTR bstrArrivalTerminal);
};

....

coclass AirlineArrivalPager
{
[default] interface IAirlineArrivalPager;
[default, source] dispinterface _IAirlineArrivalPagerEvents;
};

.......
输入IAirlineArrivalPager接口的AddArrivalDetails方法的执行情况。

....... 
STDMETHODIMP CAirlineArrivalPager::AddArrivalDetails(
BSTR bstrAirlineName,BSTR bstrArrivalTerminal)
{
Fire_OnAirlineArrivedEvent(bstrAirlineName,bstrArrivalTerminal);

// 返回状态
return S_OK;
}
.......
    这个方法的执行情况使用了Fire_OnAirlineArrivedEvent帮助程序方法,以此来通报所有接受对象执行IAirlineArrivalPagerEvents,IAirlineArrivalPagerEvents早已预定了事件通知通知。Fire_OnAirlineArrivedEvent是一个函数代理类型方法,通过Implement Connection point wizard,自动地从IConnectionPointImpl派生而来。本质上,当IConnectionPoint::Advise被调用时,它通过映射迭代,这个映射把接口指针存储到接受对象,使用这些接口指针来调用事件通知方法(OnAirlineArrivedEvent),此事件通知方法由客户端接受对象执行。
0
相关文章