技术开发 频道

从感知客户端使用.NET 组件:创建一个.NET组件

 
【IT168技术文档】

   本系列文章学习怎样从非托管 COM感知客户端中使用托管 .NET 组件。将属于.NET 组件的用户仅仅限制在托管 客户端中这一点,对于那些在过去的时光中已经花费了很多时间来开发应用程序的人来说是非常困难的,这些应用程序不可能一夜之间转化成托管代码。通过使用线路协议,例如:SOAP,.NET结构允许不同平台中全部不相同的应用程序与托管应用程序对话。非托管 COM感知客户端仍然有更容易的方法与托管组件对话。.NET运行库允许非托管 COM 感知客户端通过使用COM Interop以及通过使用由结构提供的工具无缝访问NET 组件。这保证COM 感知客户端可以与.NET 组件对话,好像它们在与普通的Classic COM 组件对话一样。

创建一个.NET组件

   首先创建一个简单的.NET 组件,此.NET 组件允许查看城市的温度。仅仅只有public类型被添加到了类型库,并且展示给了COM 感知客户端。而且,如果需要从COM 感知客户端中创建类型,它需要有一个public默认构造器。一个没有public default 构造器的public类型仍然出现在类型库中,尽管它不能从COM中共同直接地制造。Temperature组件有两个方法:DisplayCurrentTemperature和GetWeatherIndications.它有一个被称为Temperature的公共读写属性,此Temperature有相应的获得/设置方法。
Collapse 
using System;
using System.Windows.Forms;
using System.Runtime.InteropServices;

public enum WeatherIndications
{
Sunny = 0,
Cloudy,
Rainy,
Snowy
}

[ClassInterface(ClassInterfaceType.AutoDual)]
public class TemperatureComponent
{
private float m_fTemperature = 0;

// 公共构造器
public TemperatureComponent()
{
m_fTemperature = 30.0f;
}

//公共属性
public float Temperature
{
get { return m_fTemperature; }

set { m_fTemperature = value;}

}/* 结束Temperature get/set属性 */


// 显示当前温度的通用方法
public void DisplayCurrentTemperature()
{
String strTemp = String.Format("The current " +
"temperature at Marlinspike is : " +
"{0:####} degrees fahrenheit",
m_fTemperature);

MessageBox.Show(strTemp,"Today's Temperature");

}/* 结束 DisplayCurrentTemperature */

//另一种公共方法来返回数值类型
public WeatherIndications GetWeatherIndications()
{
if(m_fTemperature > 70) {

return WeatherIndications.Sunny;
}
else {

// 返回阴天
return WeatherIndications.Cloudy;
}

}/* 结束GetWeatherIndications */


}/* 结束Temperature类 */

    你将注意到这里有一个ClassInterface 的属性,此ClassInterface 被连接到Temperature 类型中,而它的值则被设置在了ClassInterfaceType.AutoDual 上。我们将看到在Snooping in on the generated Typelibrary这一章中引用这个属性的意义。现在,把它看成是告诉类型库生成工具(例如:REGASM.EXE and TLBEXP.EXE),把.NET 部件类型输出到生成的类型库中的默认的Class接口。同时也要记住一般不建议使用一个Class接口来展示.NET 类型的公共方法,因为COM 译本非常不可信。我们来看看使用接口怎样明确地完成同一事情。明确地定义一个接口,从这个接口派生你的.NET组件类型,然后在你的.NET 组件里执行这个接口方法,如果你准备相你的COM 感知客户端展示你的.NET 组件,这是一个非常值得推荐的方法。我们将详细比较这两个方法,在Snooping in on the generated Typelibrary这章中,可以看到为什么不推荐前一个方法。

    如果正在使用Visual Studio.NET,可以创建一个Visual C# 项目,使用类型库模版来编译上面的组件。如果是一个命令行操作员,然后这是建立这个组件的命令。这样创建了一个Temperature.dll程序集。
csc /target:library /r:System.Windows.Forms.dll 
/out:Temperature.dll TemperatureComponent.cs
从程序集生成类型库&注册程序集

    刚才所生成的是一个.NET 程序集,COM 感知客户端,例如:Visual Basic 6.0不能操作此.NET 程序集。你需要从此.NET 程序集得到某种COM 友好类型信息,因此我们的VB客户将会高兴地与它合作。先前使用了一个TLBIMP (类型库导入程序)工具,用它来从COM类型库创建一个.NET元数据代理。在这里要做与此相反的工作。你需要接受一个.NET 程序集,在此.NET程序集中生成一个类型库,因此它在COM 感知客户端是可以利用的。.NET 框架为此提供了一些工具。你可以使用类型库导入程序工具(TLBEXP.exe) 或者是使用程序集注册工具(Regasm.exe),在.NET SDK 安装的Bin 目录中。你可以找到类型库导入程序工具(TLBEXP.exe) 或者是程序集注册工具(Regasm.exe)。REGASM是TLBEXP工具的扩展集,它所作的不仅仅是操作一个类型库。它也被用来注册程序集,因此制造appropriate registry entries来推动COM运行库和.NET运行库,把COM 感知客户端连接到.NET组件中。在这里使用REGASM.EXE完成程序集注册和类型库生成。也能使用TLBEXP来生成类型库,然后用REGASM注册程序集。
regasm Temperature.dll /tlb:Temperature.tlb
   上面调用REGASM.EXE产生了正确的注册途径,并且从.NET程序集生成了一个类型库(Temperature.tlb),因此可以从VB 6 客户端应用程序引用类型库。

从VB 6.0客户端使用组件

    让我们迅速创建基于VB模式的应用程序,此应用程序创建并且调用.NET组件,并注册了.NET组件的程序集,并且从.NET组件程序集生成了一个类型库。此组件的创建和怎样创建一个COM 对象的方法一样。你可以引用类型库,前期绑定组件,或者可以通过使用组件ProgID,晚期绑定组件,以此来执行一个Create对象调用。通常,被生成的ProgID是与这个类型最相适合的名字。生成的ProgID可以是TemperatureComponent,但是也能使用ProgIDAttribute,指定一个用户定义的ProgID,覆盖被它生成的默认ProgID。
Collapse
Private Sub MyButton_Click()

On Error GoTo ErrHandler

Dim objTemperature As New TemperatureComponent

objTemperature.DisplayCurrentTemperature

objTemperature.Temperature = 52.7

objTemperature.DisplayCurrentTemperature

If (objTemperature.GetWeatherIndications() = _
WeatherIndications_Sunny) Then
MsgBox "Off to the beach"
Else
MsgBox "Stay at home and watch Godzilla on TV"
End If

Exit Sub

ErrHandler:

MsgBox "Error Message : " & Err.Description, _
vbOKOnly, "Error Code " & CStr(Err.Number)
End Sub
    使.NET程序集解析器能找到包含组件的程序集,你要么需要把同一目录中的组件当作正在使用它的应用程序, 或者在全局装配件缓存(全局程序集缓存 )中把程序集安置为一个共享程序集。现在把Temperature.dll复制到同一目录,作为可以执行的VB客户端应用程序。如果VB可以使用平常的基于Classic COM的调用机制,而且仍不需要调用并使用.NET组件,在VB6客户端和.NET组件之间有一个好的撒马利亚人,此撒马利亚人把COM调用请求连接到实际的.NET组件。不久我们就会看到简单的外表下面发生了什么。
进一步理解COM Interop wizardry

    让我们看一下当注册程序集时,Regasm.exe创造的注册途径。



    通过打开被REGASM生成的类型库,你可以检查OLEVIEW.EXE里的组件CLSID 。检查coclass 章节下面的uuid 属性。如果浏览到注册中的HKCR\CLSID\{...Component's CLSID...}键,可以看到REGASM 早已创建了一个被COM要求的相关联的注册途径,以此来激活包含在Inproc 服务器中的对象。除此以外,它还创建了其它一些注册途径,例如:类型, 程序集, 以及运行库译本,.NET执行时要用到这些途径。mscoree.dll 上设置了Inproc 服务器处理程序(由InProc服务器32键默认值显示),Inproc 服务器处理程序是通用语言执行层执行时执行驱动的核心。COM运行库调用MSCOREE.dll (通用语言执行层运行库)里的DllGetClass对象入口点。 然后运行库使用Class ID (CLSID),把它传递到DllGetClass对象,检查InProc服务器32 键下面的程序集和类型,以此来下载并且解决.NET程序集,此.NET程序集将为这个要求服务。运行库也动态地创建一个COM可调用包装程序(COM可调用的包装),处理非托管代码和托管组件之间的交流。这使得COM 感知客户端认为它们正在与Classic COM组件交流,使得.NET 组件认为它们正在接受来自于托管应用程序的要求。这里有一个被每一个.NET组件实例创建的COM可调用的包装。



    正如谚语所说,一幅图胜过千言万语,因此当COM 感知客户端与一个.NET 组件交流时,理解简单外表下面发生了什么。这里主要的参与者就是通用语言执行层运行库和COM可调用包装程序(COM可调用的包装),通用语言执行层运行库和COM可调用包装程序(COM可调用的包装)是由.NET 运行时假冒创作的。从那以后,COM可调用的包装 管理并且处理大部分的基础工作,让两者一起工作。在这里,COM可调用的包装 处理生命周期管理问题。非托管领域里的COM 客户端将引用计数保持在COM可调用的包装 代理上而不是在实际的.NET 组件上面。COM可调用的包装仅仅拥有.NET组件的引用。 .NET组件和其它任何托管类型一样,以通用语言执行层 垃圾回收站的准则为生。COM可调用的包装属于非托管 heap,当COM 感知客户端不拥有任何对象的引用时,COM可调用的包装就崩溃了。就像运行库可调用的包装一样,COM可调用的包装也有调用参数,这些调用参数在非托管客户端和托管 .NET组件之间来回移动。它负责按要求合成虚拟函数表。特殊接口的虚拟函数表是被动态地生成的。只有当COM 感知客户端通过QueryInterface调用,真正请求一个特殊接口时,就会懒散地创建特殊接口的虚拟函数表。COM可调用的包装代理上的调用最后被发送到一个存根,此存根真正地把调用制成托管对象。


探讨生成类型库

    让我们迅速地看一下由程序集注册工具 (REGASM)生成的被放到类型库中的信息。通过OLEVIEW的类型库查看器打开类型库,因此你可以研究IDL文件,IDL文件是从类型库反向运行的。

Collapse 

[
uuid(A9F20157-FDFE-36D6-90C3-BFCD3C8C8442),
version(1.0)
]
library Temperature
{
importlib("mscorlib.tlb");
importlib("STDOLE2.TLB");

interface _TemperatureComponent;

typedef [uuid(0820402E-B8B6-330F-8D56-FF079E5B4659), version(1.0),
custom({0F21F359-AB84-41E8-9A78-36D110E6D2F9}, "WeatherIndications")]
enum {
WeatherIndications_Sunny = 0,
WeatherIndications_Cloudy = 1,
WeatherIndications_Rainy = 2,
WeatherIndications_Snowy = 3
} WeatherIndications;

[
uuid(01FAD74C-3DC4-3DE0-86A9-8490FAEE8964),
version(1.0),
custom({0F21F359-AB84-41E8-9A78-36D110E6D2F9},
"TemperatureComponent")
]
coclass TemperatureComponent {
[default] interface _TemperatureComponent;
interface _ Object;
};

[
odl,
uuid(C51D54FA-7C81-35A5-9998-3963EAB4AA12),
hidden,
dual,
nonextensible,
oleautomation,
custom({0F21F359-AB84-41E8-9A78-36D110E6D2F9},
"TemperatureComponent")
]
interface _TemperatureComponent : 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), propget]
HRESULT Temperature([out, retval] single* pRetVal);
[id(0x60020004), propput]
HRESULT Temperature([in] single pRetVal);
[id(0x60020006)]
HRESULT DisplayCurrentTemperature();
[id(0x60020007)]
HRESULT GetWeatherIndications([out, retval]
WeatherIndications* pRetVal);
};
};
    如果看一下coclass这一章,它指定默认接口来担任被带有_ (underscore)特点前缀的类型名称的工作。这个接口被称为类型接口,它的方法由类型的许多非静态公共方法,领域和属性组成。因为你用ClassInterface 属性标记了你的.NET类型,所以生成了类型金额类型接口。这个属性告诉类型库生成工具(例如:RegAsm.exe和TlbExp.exe)如何生成默认接口(也就是我们所知道的类型接口),如何把类型的所有公共方法,领域和属性增加到其中,因此它能被展示给COM 感知客户端。

    如果没有把ClassInterface属性连接到.NET组件类型,仍然会生成一个默认类型接口。但是在这种情况下,它是一个基于IDispatch 的类型接口,此默认类型接口没有包含被展示的任何方法的类型信息,也没有包含它们的DISPIDs。只有晚期绑定用户可以使用这种类型接口类型。效果与将ClassInterfaceType.AutoDispatch值应用到ClassInterface属性的效果一样。AutoDispatch选择的优点就是:因为DISPIDs没有而被高速缓存,也没有作为可以利用的类型信息的一部分,当发行一个组件的新版本时,它们没必要打破现有的客户端,因为通过使用像IDispatch::GetIDsOfNames.一样的事物,客户端在运行时获得了DISPIDs。

 
   在类型库中只能看到public方法,并且COM客户端只能使用public方法。专用成员没有把它制成类型库,并且COM 客户端不能使用专用成员。类型的公共属性和领域被转换成IDL propget/propput类型。我们例子中的Temperature属性有被定义了的set以及get访问器,因此,propset 和propget中都没有这个属性。这里也有一个被称为_对象的接口,此接口被添加到了coclass。这个类型接口也包括其它4个方法。它们就是:
• ToString
• Equals
• GetHashCode
• GetType

    这些方法补充到了默认类型接口,因为它是暗中从System.对象类型继承而来的。其中被补充到接口的每一个方法和属性得到一个DISPID,此DISPID是自动生成的。通过使用DispId属性,你可以用一个用户定义的DISPID覆盖这个DISPID。你将注意到ToString方法早已被指定了一个0的DISPID,以此暗示它是类型接口里的默认方法。这意味着如果你不考虑这个方法名称,就会调用这个ToString方法。
Dim objTemperature As New TemperatureComponent 

MsgBox objTemperature
    让我们检查推动隐式类型接口生成的方法。首先要看一下将ClassInterfaceType.AutoDual值应用到ClassInterface属性所带来的效果。

[ClassInterface(ClassInterfaceType.AutoDual)] 
public class TemperatureComponent
{
....
}
    注意:被指定到ClassInterface 属性的值是ClassInterfaceType.AutoDual.这个选择告诉类型库生成工具怎样将类型接口 作为双重接口,怎样把所有类型信息(为了方法,属性等等以及与它们相应的调度ID)转移到类型库中。想象一下:如果决定将另一个公共方法增添到类型,将会发生什么。这样会转变被生成的类型接口 ,打破COM里的基本接口稳定性规则,因为虚拟函数表结构改变了。当使用这个组件时,晚期绑定客户端也会分享这种痛苦。因为增加了新的方法,从而生成了调度ID (DISPIDs),这样也会打破晚期绑定客户端。作为一个基本规则,使用ClassInterfaceType.AutoDual是有害的,因为它根本不了解COM译本.让我们看一下下一个你能为你的ClassInterface属性而设置值。用ClassInterfaceType.AutoDispatch的值标记你的ClassInterface属性会迫使类型库生成工具避免在类型库里生成类型信息。因此,如果你像下面所表示的一样,为你的TemperatureComponent类型标记了ClassInterface属性

[ClassInterface(ClassInterfaceType.AutoDispatch)]
public class TemperatureComponent
{
.....
}

   然后,由RegAsm.exe生成的相应的类型库将有一个这样的IDL结构。
[ 
uuid(A9F20157-FDFE-36D6-90C3-BFCD3C8C8442),
version(1.0)
]
library Temperature
{
......

[
uuid(01FAD74C-3DC4-3DE0-86A9-8490FAEE8964),
version(1.0),
custom({0F21F359-AB84-41E8-9A78-36D110E6D2F9},
"TemperatureComponent")
]
coclass TemperatureComponent {
[default] interface IDispatch;
interface _ Object;
};
};
    你将注意到默认接口是一个IDispatch 接口,在类型库中既没有DISPIDs 也没有方法类型信息。这样只剩下COM 感知客户端使用仅仅使用晚期绑定的.NET 组件 。而且,在类型中,因为DISPID 细节没有被储存作为信息的一部分,通过使用像IDispatch::GetIDsOfNames一样的事物,客户端按要求获得这些DISPIDs。这允许客户端在没有打破现有代码的情况下,使用更加新组件的版本。当发行了更加新组件的版本时,虽然前者仅仅允许晚期绑定,因为它没有打破现有的客户端代码,所以使用ClassInterfaceType.AutoDispatch比使用ClassInterfaceType.AutoDual更加安全。模拟你的.NET组件,并把它展示给COM 感知客户端,完成这一程序值得推荐的方法就是去除类型接口本身,相反,明确地把你正在展示的方法转成分离接口,并且让.NET组件执行那个接口。使用一个类型接口,将你的.NET组件展示给COM 感知客户端,这一方法非常简单,而且非常迅速。但是并不是一种值得推荐的方法。明确地把方法转变成接口,让我们试图重新编写我们的TemperatureComponent,看类型库生成有什么不同:
Collapse 
// 定义ITemperature 接口
public interface ITemperature {

float Temperature { get; set; }
void DisplayCurrentTemperature();
WeatherIndications GetWeatherIndications();

}/* 结束 Itemperature接口 */



[ClassInterface(ClassInterfaceType.None)]
public class TemperatureComponent : ITemperature
{
......

public float Temperature
{
get { return m_fTemperature; }
set { m_fTemperature = value;}

public void DisplayCurrentTemperature() {
.....
public WeatherIndications GetWeatherIndications() {
....

    下面是被生成的类型库中相应的IDL文件的例子。注意,现在TemperatureComponent类型默认接口是由类型执行的ITemperature接口。
Collapse 

[
uuid(A9F20157-FDFE-36D6-90C3-BFCD3C8C8442),
version(1.0)
]
library Temperature
{
......

[
odl,
uuid(72AA177B-C6B2-3694-B083-4FF535B40AD2),
version(1.0),
dual,
oleautomation,
custom({0F21F359-AB84-41E8-9A78-36D110E6D2F9}, "ITemperature")
]
interface ITemperature : IDispatch {
[id(0x60020000), propget]
HRESULT Temperature([out, retval] single* pRetVal);
[id(0x60020000), propput]
HRESULT Temperature([in] single pRetVal);
[id(0x60020002)]
HRESULT DisplayCurrentTemperature();
[id(0x60020003)]
HRESULT GetWeatherIndications([out, retval]
WeatherIndications* pRetVal);
};

[
uuid(01FAD74C-3DC4-3DE0-86A9-8490FAEE8964),
version(1.0),
custom({0F21F359-AB84-41E8-9A78-36D110E6D2F9},
"TemperatureComponent")
]
coclass TemperatureComponent {
interface _ Object;
[default] interface ITemperature;
};
};
    将.NET类型的方法明确地转变成接口的方法,以及从接口派生类型和执行类型的方法都是非常值得推荐的方法,以此来向你的COM 感知客户端展示你的.NET 组件。ClassInterfaceType.None选择告诉类型库生成工具不需要一个类型接口。这就保证ITemperature接口就是默认接口。不为类型接口属性指定ClassInterfaceType.None值,然后类型接口就会创建成默认接口。这是我们在这章所学的要点。

   • 附带有类型接口的ClassInterfaceType.AutoDual不了解COM版本。尽量避免使用。
   • 附带有ClassInterfaceType.AutoDispatch的类型接口不会把类型信息和DISPIDs转移到类型库。它们是友好COM 译本。仅仅通过晚期绑定,可以从COM 感知客户端的访问。
   • 类型接口是一个低劣的接口。可能的话,尽量避免。相反使用显式接口。
   • 使用ClassInterfaceType.None,将显式接口转变成默认接口。

    另外一个有趣的发现就是:在.NET类型或者接口(正在将此.NET类型或者接口转到类型库中)超载方法时,通过添加一个'_'('_'后面是一序列数字),REGASM 和TLBEXP在IDL中生成mangled方法名称的方法。例如:如果在.NET 组件中展示了下面的接口。
public interface MyInterface
{
String HelloWorld();
String HelloWorld(int nInput);
}
   然后,与类型库相对应的IDL(此IDL是由REGASM/TLBEXP生成的)如下:
[ 
.....
]
interface MyInterface : IDispatch {

[id(0x60020000)]
HRESULT HelloWorld([out, retval] BSTR* pRetVal);

[id(0x60020001)]
HRESULT HelloWorld_2([in] long nInput,
[out,retval] BSTR* pRetVal);
};
    注意,要把被添加到第二个HelloWorld方法的'_2'与IDL文件中的第一个'_2'相区分开来。
0
相关文章