创建根对象指南

顾名思义,根对象是对象模型的基础。因此可将其看成包含对象模型中所有其它对象的对象。根对象通常都是外部可创建的— 也就是说,客户可利用 New 操作符或 CreateObject 函数来创建根对象的实例。

例如,一个 billing(记帐)系统的根对象可有一个 Invoices(发货单)集合作为其属性之一。集合中每一个 Invoice 对象可以包含一个 LineItems 集合,这样可用如下方法访问某一特定 LineItem 对象:

Dim li As LineItem
Set li = <root object>.Invoices(1138).LineItems(3)

根对象的命名

根对象的名称因部件类型而异。如果部件是一个提供对象的独立的桌面应用程序,象 Microsoft Excel 那样,则“应用程序”可能看上去象根对象的逻辑名。

另一方面,如果部件在进程内运行,提供对象给客户但没有自己的用户界面,“应用程序”做根对象的名字则可能不太恰当。应该用一个能反映部件功能的名称,或类似于部件在类型库中的名称。

提示 如此多的对象模型用“应用程序”作为其根对象的名字,以至于它已成为全局名字空间的污染源。也就是说,为了要得到正确的 Application 对象,必须经常用类型库的名称来限定它。让我们有创意一点,使用能反映部件根对象的唯一特征的名称。

注意 尽管“应用程序”通常被认为是典型的根对象名,本节的代码示例都用“根”, 只因为“应用程序”实在被用得过滥。

根对象属性和方法

根对象是定义影响整个部件的属性和方法的逻辑地点。例如,根对象可能有只读的 Version 属性。根据定义,根对象也会包含集合或下一级对象的对象属性。

全局根对象和全局帮助对象

作为全局对象的根对象是很常见的。通过将根对象的类模块的 Instancing 属性设置为 GlobalMultiUse 可以做到这一点。“建立代码部件”中的“全局对象””对此曾讨论过。这使根对象的属性和方法看上去象全局过程,就象 VB 和 VBA 库的成员一样。

如果根对象是全局的,则可以允许用户通过在代码中引用该根对象来启动部件实例。提供带有同名的只读属性的根对象,它返回一个自引用。以下是寻找名为“根”的根对象的代码。

Public Property Get Root() As Root
   Set Root = Me
End Sub

自引用的作用是— 当客户在代码中引用 Root 时,该全局对象在 Property Get 被调用之前就创建了。

用 Golbal 对象备份根对象

对有些部件来说,将根对象的所有属性和方法都转储到全局名字空间中可能不太恰当。在这种情况下,不要将根对象定义为全局对象,而应创建一个名为 Globals 的类,并将其 Instancing 属性置为 GlobalMultiUse。

可将要放入全局名字空间的属性和方法添加到这个 Globals 类中。在需要显露根对象的方法的情况下,将具有同样参数的方法添加到 Globals 类中,并使用在根对象中同名的方法。

仍然可以允许用户通过在代码中引用根对象来启动部件;只需在 Globals 类中添加一个属性如下所示:

Public Property Get Root() As Root
   If gRoot Is Nothing Then
      Set gRoot = New Root
   End If
   Set Root = gRoot
End Property

该例假设根对象名为 “根”,并在全局变量 gRoot 中保持对它的引用。

Globals 类是放置用户能全局访问的通用过程的好地方。

提示 应使用 Property Get 过程往到 Globals 类中添加任何对象属性,而不应使用公用的对象变量。Property Get 定义了一个只读属性,以免某些公用对象变量会被用户意外地设置为 Nothing。

GetObject 函数

《程序员指南》的“用部件编程”中描述了如何用 GetObject 函数获取对正在运行的部件实例的引用,或测试是否有实例正在运行客户应用程序不能自动利用 GetObject 函数来获取对 Visual Basic 部件中定义的类的已有实例的引用。

可以通过将根对象放到 Running Object Table (ROT) 中,来完成这项工作。COM 提供 ROT 作为正在运行的对象自己进行注册的地方,这样就可利用诸如 GetObject 的函数来查找它们。

另一种可用的技术是使根对象成为 PublicNotCreatable,这样客户就不能直接创建实例了。可以提供一个客户可创建的 MultiUse Connector 对象。该 Connector 对象可提供一个属性(通常与根对象同名),返回对单一的共享根对象的引用。

“建立代码部件”的“异步的回调与事件”中描述了该技术,并用该章的应用程序示例进行了演示。

提示 本主题前面叙述的 Globals 对象以一种不引人注目的方式提供与 Connector 对象一样的功能。

详细信息 ROT 在许多关于 COM 的书中都有解释,如 Kraig Brockschmidt 的Inside OLE 或 OLE Programmer’s Reference,这两本书都可从 Microsoft 出版社获得。

根对象的 Instancing 属性值

根对象提供的功能很大程度依赖于其类模块的 Instancing 属性值:PublicNotCreatable、SingleUse、GlobalSingleUse、MultiUse 或 GlobalMultiUse。

PublicNotCreatable 根对象

如果根对象标记为 PublicNotCreatable,则客户应用程序不能利用 New 操作符或 CreateObject 函数来创建其实例。尽管这看上去有些颠倒— 因为大部分人希望根对象是获取对象模型中所有其它对象的途径─ 但在和全局对象或 Connector 对象的连接中,正如本主题前面所讨论的那样,它是允许客户共享根类中的单个实例的一种方法。

在这种情况下,为了让客户访问根对象,则必须给公共的可创建的对象(如 Connector 对象或 Globals 对象)赋予能返回对此根对象引用的属性,如下所示:

Property Get Root() As Root
   Set Root = gRoot
End Property

注意这并不创建循环引用,因为相关对象不拥有对 Root 对象的真正引用;它们只是利用 Property Get 返回一个全局引用。

也可以用这样一个属性实现全局对象,如本主题前面所述。

SingleUse 根对象

如果定义根对象的类的 Instancing 属性值是 SingleUse 或 GlobalSingleUse,则每当客户应用程序使用 New 操作符或 CreateObject 函数创建根对象时,将启动一个新运行的部件实例。

在它的初始化事件过程中,根对象可能设置对其自身的全局引用,这样其它对象就可以用这个全局引用来获得对它的访问,如下列代码段所示:

Private Sub Class_Initialize()
   '变量 gRoot 被声明为标准模块中的公共变量。
   Set gRoot = Me
   '...其它初始化代码...
End Sub

如果不保持对根对象的全局引用,并且客户也释放了它的引用— 尽管保存了对一个或多个相关对象的引用— 则部件可能进入有相关对象但无根的情况。

注意 由于 SingleUse 根对象为每次客户请求加载一个新的部件实例,保存全局引用不会造成孤立对象(如“ActiveX 部件关闭”中所述),除非客户端互相传递对根对象的引用。

SingleUse 根对象和部件的独占使用

根据是否将附加的类模块标记为外部可创建的,将根对象标记为 SingleUse 还可以使客户应用程序独占使用部件的一份副本。

如果部件中唯一的外部可创建的类是根对象,则每个客户应用程序都有它自己的部件实例。因为 Visual Basic 的类不支持用 GetObject 函数来获取对已存在的对象的引用,所以其他客户无法访问正在运行的部件副本。

这样做的不利之处是每个客户都必须启动一个部件的实例,因此会占用更多的内存和系统资源。如果需要让每个客户应用程序以独占方式使用部件副本,则写一个进程内部件效率更高些。

SingleUse 根对象及共享部件

如果将部件中其它类标记为 MultiUse 或 GlobalMultiUse,则客户可从这些类中创建对象,而不用先创建根对象。

这与 Microsoft Excel 的外部可创建对象的工作方式类似。如果创建 Microsoft Excel 应用程序对象,则启动新的 Microsoft Excel 的运行实例。然而,如果创建新的 Sheet 或 Chart 对象,则它们由先前运行的 Microsoft Excel 实例提供,如果这个实例存在的话。

为了处理附加的外部可创建的对象,则所有外部可创建对象的 Initialize 事件过程必须测试根对象是否存在,如果根对象不存在就创建一个根对象:

'来自任何外部可创建类的代码段。
Private Sub Class_Initialize()
   If gRoot Is Nothing Then
      Set gRoot = _
         CreateObject("MyComponent.Root")
   End If
   ' ...此对象的其它初始化代码...
End Sub

使用 CreateObject 函数,而不用 New 操作符,来创建根对象是很重要的。否则将来客户就可在部件的同一实例中创建另一个根对象。详细信息请参阅“建立代码部件”中的“多进程的可扩展性:SingleUse 对象”和“在 Visual Basic 部件中如何创建对象”。

提示 为什么不在 Sub Main 过程中创建根对象呢?一方面,在 Sub Main 中创建对象不是个好主意。它可能造成死锁和对象创建超时。另一方面,如果部件是为响应客户请求而启动的,则无法知道正在创建的是什么对象。如果当客户请求根对象时在 Sub Main 中创建一个 SingleUse 根对象,则此客户请求将失败。

注意 将部件中的多个类标记为 SingleUse 或 GlobalSingleUse 可能引起无法预料的结果,如“建立代码部件”中“多进程的可扩展性:SingleUse 对象”所述。

MultiUse 根对象

如果将根对象标记为 MultiUse 或 GlobalMultiUse,则每个客户将获得自己的根对象,但所有的实例可能来自同一个正在运行的部件实例。

用户在实现中面临的决定是:让所有实例共享相关对象的同一集合,还是让每个根对象都有自己的集合呢?这完全依赖于部件的用途。如果它是一个进程外部件,同时又是独立的桌面应用程序,则可能需要每个根类实例能象 Application 对象一样运行— 这种情况下它们必须共享相关对象的全局集合。

独立根对象

另一方面,如果每个根对象及其包含的相关对象代表一个功能单元,而又希望客户能使用不止一个单元,则根类实例必须彼此无关,且每个都有自己的相关对象的集合。

另外仍然可以象本主题前面所述的那样用一个 Globals 对象来支持根对象,只有在这种情况下 Globals 对象需要包含根对象的集合,而不是单个实例。

正在运行的部件实例的全局数据,如版本号,也可用 Globals 对象的只读属性显露。