65.9K
CodeProject 正在变化。 阅读更多。
Home

从 .NET 库模拟 COM 连接点

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (23投票s)

2005 年 8 月 22 日

CPOL

14分钟阅读

viewsIcon

64678

downloadIcon

1364

一篇关于从 MFC/ATL COM 客户端访问 VB.NET 库的文章。

引言

本文演示了如何从 COM 客户端访问 .NET 库。它与其他 COM 互操作文章不同之处在于,它模拟了供使用 MFC 和 ATL 库实现的 COM 客户端使用的连接点。本文使用的 .NET 库大量使用了 System.Collections.Hashtable 类,因此我们还将演示一个可重用的非托管 C++ 包装器来处理此类。

背景

TrayGames,我们需要为提供给第三方开发者的多人在线游戏开发 SDK (TGSDK) 添加对非 .NET 游戏客户端的支持。尽管我们更喜欢 .NET 环境,因为它提供了丰富的功能和快速的开发,但我们不想排除尚未采用 .NET 的开发人员编写有趣游戏。由于这些是回合制游戏,我们的库利用了 .NET Framework 基于委托的事件系统,因此我们将详细介绍如何将其公开给 COM。在客户端,我们编写了一个 COM 版本的井字棋示例游戏(也有 C# 和 VB.NET 版本),以演示如何访问我们的 .NET 库,但此示例稍作修改即可应用于任何 .NET 库。

使用代码

.NET Framework 提供了一个基于委托的事件系统,用于将事件发送者(源)连接到事件接收者(接收器)。但是,当接收器是 COM 客户端时,源必须包含其他元素来模拟连接点。本文将演示这些修改,然后展示 COM 客户端如何通过调用 IConnectionPoint::Advise 方法(我们实际上使用 ATL 包装器方法)以通常的方式注册其事件接收器接口。这个 COM 客户端特别有趣之处还在于它用于访问 .NET Hashtable 的包装器,我们将对此以及 COM 互操作进行探讨。

我们的 .NET 库代码是专有的,因此我只会提供代码片段,展示对该(或任何).NET 库进行更改以使其可以通过 COM 访问。下载的演示在“COM Demo”文件夹中仅包含 VB.NET 库的二进制文件。但是,游戏服务器的源代码包含在下载的源代码中,位于“CPP Server”文件夹下。客户端提供示例的完整源代码,演示了如何访问 .NET 库。COM 客户端使用 MFC 和 ATL 库以 C++ 实现。还包含自定义 Hashtable 项目的源代码。这两个项目都可以在下载的源代码存档的“COM Client”文件夹下找到。

为了玩示例游戏,您需要在服务器“TGDaemonHarnes.exe”的 Daemon Harness 中加载游戏守护进程。在“Com Demo”文件夹中运行它,单击“Load Game Daemon”按钮,然后导航到“TicTacDoeDaemon.dll”。选择文件并打开它。Daemon Harness 应该报告游戏守护进程已在 Test Harness 模式下启动,并显示所需玩家数量。现在我们需要连接几个游戏到这个守护进程。由于这是两人游戏,我们将需要运行两个游戏客户端。运行一个“MFCTicTacToe.exe”游戏应用程序实例,然后再运行一个。运行游戏的第二个实例后,游戏守护进程应该会意识到它拥有足够的玩家,并继续开始游戏。

公开托管库

在此示例中,我们将使用一个名为 TGGameLib 的库。理解该库的功能对于理解如何将其公开给 COM 或如何模拟连接点并不重要。只需知道它是我们用于开发多人游戏的核心库之一。该库是用 VB.NET 编写的,如果您使用的是 C# 或其他 .NET 语言,修改将是相同的,但语法会略有不同。那么,让我们深入了解一下!

我们将使用 ClassInterfaceAttribute 类来公开我们的类。此类将为带属性的类自动生成一个类接口。类接口的名称与类本身的名称相同,但名称前面会加上下划线字符。公开时,类接口包含托管类的所有公共成员,包括从其基类继承的成员(如果有)。尽管类接口消除了为每个类显式定义接口的工作,但它们在生产应用程序中的使用并不推荐。双类接口允许客户端绑定到特定的接口布局,该布局可能会随着类的演进而更改。话虽如此,这是将 .NET 库公开给 COM 的**更快**的方式,而且由于 TGSDK 仍处于 beta 阶段,这对我们来说不是问题。您始终可以在以后(并让您的类实现该接口)在您已启动并运行并且知道它不会更改之后,显式定义您的接口。强名称也推荐使用,但我们在这里也没有这样做。

我们需要公开构成库的各种类、接口、enums 和事件。我们将从 TGGameClass 类开始,这只是一个普通的 VB.NET 类。这是它在公开给 COM **之前**的样子

Public Class TGGameClass
    Implements IDisposable
    
    Public Event GameEvent As TGGameEventHandler
    Public Event AdminEvent As TGAdminEventHandler
    Public Event DataEvent As EventHandler

    Public Delegate Sub TGGameEventHandler(ByVal sender As Object, _
                                            ByVal e As GameEventArgs)
    Public Delegate Sub TGAdminEventHandler(ByVal sender As Object, _
                                            ByVal e As AdminEventArgs)    
    
    Public Sub Start(ByVal gameQSize As Integer, _
                    ByVal retrieveMessagesManually As Boolean)
     ' method implmentation is here
    End Sub
    
    Public Sub RetrieveMessages()
     ' method implmentation is here
    End Sub
    
    Public Function IsConnected() As Boolean
     ' method implmentation is here
    End Function
    
    Public Function SendToServer(ByVal data As Hashtable) _
                                                  As Boolean
     ' method implmentation is here
    End Function
End Class

为了公开此类,我们需要向文件添加互操作命名空间。我们还必须将 GuidClassInterfaceComVisible 属性添加到类中。此外,因为我们要将事件接收器接口连接到此类,所以必须添加 ComSourceInterfaces 属性。

Guid 属性允许您添加全局唯一标识符 (GUID),这是其唯一名称,以便 COM 可以区分您的类。您必须将 GUID 指定为该属性的参数,您想公开的每个类型都必须有一个不同的 GUID。使用 Visual Studio .NET 中“工具”菜单下的“Create GUID”工具来创建 GUID。将给定的 GUID 作为 Guid 属性的参数粘贴。删除 GUID 中的大括号,您应该会得到类似以下内容

  <Guid("F75C54A9-616F-4090-BC02-8B4795B6F499")>

ClassInterface 属性控制类型库导出器 (Tlbexp.exe) 是否自动为带属性的类生成类接口。您可以使用 Visual Studio .NET 中的“Register for COM Interop”项目选项(在“Properties->Configuration Properties->Build”下)、Tlbexp.exeRegasm.exe 来生成类型库。Regasm.exe 基本上是 Tlbexp.exe 功能的超集。仅当您拥有完整源代码时,Visual Studio .NET 选项才可用。类接口可以是双接口或仅调度接口,如前所述,我们使用双接口来快速启动并运行。

  <ClassInterface(ClassInterfaceType.AutoDual)>

ComVisible 属性控制单个托管类型或成员,或程序集中的所有类型对 COM 的可访问性。您可以将此属性应用于程序集、接口、类、结构、委托、枚举、字段、方法或属性。只有公共类型才能设为可见,此外,此属性**不**需要使公共托管程序集和类型可见,它们默认对 COM 是可见的。我们在库中使用它,因为我们的程序集将 ComVisible 设置为 False。这是 FxCop 的一项建议,这是一个我们运行的代码以确保与 Microsoft 的最佳实践在一定程度上的兼容性。

  <ComVisible(True)>

ComSourceInterfaces 属性定义将作为 COM 连接点公开的事件接口。我们必须传递源接口的 Type,通过使用 GetType() 方法可以轻松获得它。请注意,类事件名称和传出接口方法名称必须相同。我们稍后会看 TGGameEvents,但基本上我们是通过传递命名空间和事件接收器接口,将事件接收器接口(传出)TGGameEvents 连接到 TGGameClass 类。

  <ComSourceInterfaces(GetType(TGGameEvents))>

将所有这些更改放在一起并应用于 TGGameClass 将使其可供 COM 客户端使用,这是它**应用**属性后的样子

Imports System.Runtime.InteropServices

<Guid("F75C54A9-616F-4090-BC02-8B4795B6F499"), _
ClassInterface(ClassInterfaceType.AutoDual), _
ComSourceInterfaces(GetType(TGGameEvents)), _
ComVisible(True)> _
Public Class TGGameClass
    Implements IDisposable
    
    Public Event GameEvent As TGGameEventHandler
    Public Event AdminEvent As TGAdminEventHandler
    Public Event DataEvent As EventHandler

    Public Delegate Sub TGGameEventHandler(_
       ByVal sender As Object, ByVal e As GameEventArgs)
    Public Delegate Sub TGAdminEventHandler(_
       ByVal sender As Object, ByVal e As AdminEventArgs)    
    
    Public Sub Start(ByVal gameQSize As Integer, _
           ByVal retrieveMessagesManually As Boolean)
     ' . . .
    End Sub
    
    Public Sub RetrieveMessages()
     ' . . .
    End Sub
    
    Public Function IsConnected() As Boolean
     ' . . .
    End Function
    
    Public Function SendToServer(_
         ByVal data As Hashtable) As Boolean
     ' . . .
    End Function

End Class

下面我们定义了 TGGameEvents 事件接收器接口,供 COM 接收器实现(在本文的客户端部分讨论)。与其他从该库公开的类型不同,我们的事件接口必须显式公开为 IDispatch 接口;您可以使用 InterfaceType 属性来执行此操作。此属性允许您覆盖类型库导出器默认将托管接口公开给 COM 的方式(作为双接口)。InterfaceType 允许您指定 InterfaceIsDual(默认)、InterfaceIsIDispatch(仅用于后期绑定的 dispinterface)或 InterfaceIsUnknown(仅用于早期绑定的 IUnknown 派生)。

在此接口中,我们通过分配任意 DispId 值来公开事件方法。我们已经看到了此事件接收器接口如何连接到 TGGameClass,并且我们还定义了代表传出接口中方法定义的每个委托的事件。稍后我们将看到非托管客户端如何创建 TGGameClass 实例并实现事件接收器接口。

<Guid("7D809BD7-13D2-4b47-9B74-0236E9F01A99"), _
InterfaceType(ComInterfaceType.InterfaceIsIDispatch), _
ComVisible(True)> _
Public Interface TGGameEvents
    <DispId(1)> _
    Sub DataEvent(ByVal sender As Object, _
                        ByVal e As EventArgs)
    <DispId(2)> _
    Sub GameEvent(ByVal sender As Object, _
                    ByVal e As GameEventArgs)
    <DispId(3)> _
    Sub AdminEvent(ByVal sender As Object, _
                   ByVal e As AdminEventArgs)
End Interface

代码片段中引用的 GameEventArgsAdminEventArgs 类没什么特别的。

<Guid("8B7134E9-C428-42d4-AF84-2E803A6F2099"), _
ClassInterface(ClassInterfaceType.AutoDual), _
ComVisible(True)> _
Public Class GameEventArgs
    Inherits EventArgs

    Private mValue As Hashtable

    Public Sub New(ByVal value As Hashtable)
        mValue = value
    End Sub

    Public ReadOnly Property Value() As Hashtable
        Get
            Return mValue
        End Get
    End Property
End Class
<Guid("B5085EE9-0C98-47ff-AE2A-4B0F9D960B99"), _
ClassInterface(ClassInterfaceType.AutoDual), _
ComVisible(True)> _
Public Class AdminEventArgs
    Inherits EventArgs

    Private mType As AdminEventType
    Private mData As Hashtable

    Public Sub New(ByVal type As AdminEventType, _
                            ByVal data As Hashtable)
        mType = type
        mData = data
    End Sub

    Public ReadOnly Property Type() As AdminEventType
        Get
            Return mType
        End Get
    End Property

    Public ReadOnly Property Data() As Hashtable
        Get
            Return mData
        End Get
    End Property
End Class

最后,我们还有几个枚举想要公开给 COM。我们只需要为 enums 应用 GuidComVisible 属性。

<Guid("D5566638-49CD-4664-8DD2-BC49486A8799"), _
ComVisible(True)> _
Public Enum AdminEventType
    Connected
    Disconnected
    Shutdown
    Minimize
    Restore
End Enum

<Guid("FAB7367B-EDA2-4447-B8C6-6869B2377699"), _
ComVisible(True)> _
Public Enum TGGameGetError
    NoError
    MissingSubtypeInGameMsg
    MissingPlayerNumberInAdminConnectedMsg
    MissingPlayerDataInAdminConnectedMsg
    MissingDataInGameMsg
    ExceptionDeserializingDataInGameMsg
End Enum

<Guid("97567966-C6ED-4249-BAA0-3BBDDCA3BB99"), _
ComVisible(True)> _
Public Enum TGGameSendError
    NoError
    NotConnected
    ExceptionSerializingData
End Enum

我们在 TGDxGameLib 库中遇到的一个问题是。该库尝试公开一个接口 IGameClass。不幸的是,当我们尝试注册项目输出以进行 COM 互操作时,我们收到了以下错误:“COM Interop registration failed. There are no registrable types in the built assembly”。要解决此问题,您可以创建一个实现您尝试公开的接口的类,或者只需向项目中添加一个虚拟类,这是我们选择的。

' Dummy class to get around an 
' error exporting the Interface below
<Guid("D93A0FE1-1B0E-481d-90F6-C61D56E36499"), _
ClassInterface(ClassInterfaceType.AutoDual), _
ComVisible(True)> _
Public Class DxGameEventArgs
    Inherits EventArgs

    Public Sub New()
    End Sub
End Class

<Guid("F5133BF0-77DB-4f43-96A2-347D978A1699"), _
InterfaceType(ComInterfaceType.InterfaceIsDual), _
ComVisible(True)> _
Public Interface IGameClass
    ' Declare an interface.
    Sub Animate(ByVal time As Single)
    Sub PreSkinPaint()
    Sub Render(ByVal device As _
         Microsoft.DirectX.Direct3D.Device)
    Sub Init(ByVal gc As GraphicsClass)
    Sub Dispose()
    ' etc. . .

    ' properties
    ReadOnly Property GameName() As String
    ReadOnly Property GameSize() As Drawing.Size
    ReadOnly Property DisplayFPS() As Boolean
    ReadOnly Property MaxActiveFPS() As Integer
    ReadOnly Property MaxInactiveFPS() As Integer
End Interface

完成后,您可以通过 RegAsm.exe 注册程序集,将它们注册到 COM 并生成类型库。或者,如果您有完整源代码,可以使用 Visual Studio .NET 中的“Register for COM Interop”项目选项进行注册。然后,类型库可以在接收事件的客户端中被引用。

c:\Windows\Microsoft.NET\Framework\v1.1.4322\RegAsm.exe 
                             TGGameLib.dll /codebase /tlb

创建非托管 COM 客户端

对于我们的 COM 客户端,我们决定制作一个我们井字棋示例游戏的版本。要创建使用 .NET 库并接收事件接口的 COM 客户端,我们使用 Visual Studio .NET 中的“New Project...”选项创建了一个新的“MFC application”,并将其命名为“MFCTicTacToe”。创建应用程序后,我们选择了“Add Class...”项目选项并添加了“ATL Support to MFC”。这为我们提供了继续进行所需的样板代码。我们从 MFC 应用程序开始,以便在我们的示例游戏中更容易地进行 GUI 操作,因为它是一个游戏,但这并非必需。文件 TGGameClass.h 包含 CTGGameClassEventWrapper 类。我们创建此类的目的是作为传出事件接口 TGGameEvents 的接收器。由于客户端接收器是本文的重点,我们将首先对其进行介绍。在我们的客户端中实现连接点接收器的基本步骤是

  • 使用 #import 指令导入每个外部对象的类型库。
  • 声明 IDispEventImpl 接口。
  • 声明事件接收器映射。
  • 建议并撤销连接点。

让我们更详细地看一下这些步骤。在文件顶部,我们必须添加必要的 import 语句。我们必须导入我们想要处理的每个外部对象的类型库。这将定义可以处理的事件,并提供声明事件接收器映射时使用的信息(稍后会详细介绍)。请注意,我们使用 rename 选项来避免一些名称冲突。

#import "c:\\WINDOWS\\Microsoft.NET\\Framework\\v1.1.4322\\mscorlib.tlb" 
                                    rename("ReportEvent", "Report_Event")
#import "..\\..\\Com Demo\\TGGameLib.tlb" rename("GetType", "Get_Type")

由于我们只想提供对我们感兴趣处理的事件的实现(而不是所有 IDispatch 的内容),因此我们选择使用 IDispEventImpl 模板类来为我们的 ATL 类提供连接点接收器的支持。该接收器将允许我们处理从 TGGameClass 类引发的事件。此模板类与我们 CTGGameClassEventWrapper 类中的事件接收器映射结合使用,以将事件路由到适当的处理程序方法。稍后会详细介绍这些方法,现在让我们看一下此模板类的构造函数调用。IDispEventImpl 构造函数的第三个参数是我们想要实现的事件 dispinterface。此 dispinterface 必须在第四个参数指向的类型库中进行描述;这就是 IDispEventImpl 获取接口类型信息的地方。这就是为什么我们必须在 .NET 库上运行 RegAsm.exe。请注意,在 .NET 库中,我们为所有我们想公开给 COM 的类型分配了 GUID。在 COM 端,我们使用 C++ __uuidof 运算符来检索这些 GUID。

class ATL_NO_VTABLE CTGGameClassEventWrapper
  : public IDispEventImplDbg<0, CTGGameClassEventWrapper, 
        &__uuidof(TGGameLib::TGGameEvents), 
        &__uuidof(TGGameLib::__TGGameLib), 
        1, 0>

我们还声明了几个特定于此示例逻辑的数据成员。

  HANDLE m_hDataEvent;
  TGGameLib::_TGGameClassPtr TheGameClass;

为了让 TGGameClass 的事件通知由适当的方法处理,我们必须将每个事件路由到一个处理程序。ATL 提供了宏 BEGIN_SINK_MAPEND_SINK_MAPSINK_ENTRY,这使得映射变得简单。我们使用这些宏为我们想要作为事件处理的每个事件(在每个对象上)创建一个事件接收器映射。

BEGIN_SINK_MAP(CTGGameClassEventWrapper)
    SINK_ENTRY_EX(0, __uuidof(TGGameLib::TGGameEvents), 
                              1 /* DISPID */, DataEvent)
    SINK_ENTRY_EX(0, __uuidof(TGGameLib::TGGameEvents), 
                              2 /* DISPID */, GameEvent)
    SINK_ENTRY_EX(0, __uuidof(TGGameLib::TGGameEvents), 
                             3 /* DISPID */, AdminEvent)
END_SINK_MAP()

CTGGameClassEventWrapper 可见之前,CTGGameClassEventWrapper 支持的每个外部 dispinterface 都会被查询传出接口。建立连接并使用传出接口的引用来处理源对象(在此例中为 TGGameClass)的事件。此过程称为“建议”。完成外部接口后,应该通知传出接口它们不再被我们的类使用。此过程称为“撤销”。我们在下面的构造函数和析构函数代码中调用 DispEventAdvise 来建立 TGGameClassCTGGameClassEventWrapper 之间的连接,并调用 DispEventUnadvise 来断开连接。

  CTGGameClassEventWrapper(
    TGGameLib::_TGGameClassPtr GameClass)
  {
    Advise(GameClass);
  }

  CTGGameClassEventWrapper()
    : TheGameClass(__uuidof(TGGameLib::TGGameClass))
  {
    m_hDataEvent = 
        CreateEvent(NULL, FALSE, FALSE, NULL);
    
    HRESULT _hr = Advise(TheGameClass);
    if (FAILED(_hr)) _com_issue_error(_hr);
    
    _hr = TheGameClass->Start(8000, VARIANT_TRUE);
    if (_hr != S_OK)
    {
      ATLASSERT(false && 
          "The Game Class could not be started.");
    }
  }

  ~CTGGameClassEventWrapper()
  {
    if (TheGameClass != NULL)
    {
      DispEventUnadvise(TheGameClass);
    }
  }
  
  HRESULT Advise(
      TGGameLib::_TGGameClassPtr GameClass)
  {
    HRESULT hr = E_FAIL;  
    TheGameClass = GameClass;
  
    // don't advise if it's already been done
    if (m_dwEventCookie == 0xFEFEFEFE) 
    {  
      hr = DispEventAdvise(TheGameClass);  
    } 

    return hr;
  }

完成事件接收器还剩下实际的事件处理程序。

  STDMETHOD_(void, DataEvent)(
           VARIANT sender, 
           mscorlib::_EventArgs* e)
  {
    SetEvent(m_hDataEvent);
  }

  STDMETHOD_(void, GameEvent)(
           VARIANT sender, 
           TGGameLib::_GameEventArgs* e) = 0;

  STDMETHOD_(void, AdminEvent)(
                  VARIANT sender, 
                  TGGameLib::_AdminEventArgs* e) = 0;

您可能会注意到其中两个事件处理程序是纯虚函数。这些纯虚函数在派生类 CMyTicTacToeGameClass 中实现。此类及其事件处理程序特定于此游戏示例的逻辑,因此我将不在此深入探讨。然而,.NET System.Collections.Hashtable 类的包装器是普遍感兴趣的,所以我们现在来看看它。在最初开发此示例时,我们不得不编写大量 COM 代码来从 .NET Hashtable 中提取信息。即使有 ATL 提供的许多免费 COM 代码,这也被证明很麻烦。首先,让我们看一下由我们的服务器代码准备的典型 Hashtable,注意内部的 Hashtable

Dim PlayerData As New Hashtable ' Data for all players
For Each Player As GamePerson In Group.Players.Values
    ' Info on a particular player
    Dim PlayerInfo As Hashtable = New Hashtable 
    PlayerInfo.Add("Nick", Player.Nick)
    PlayerInfo.Add("Rank", Player.Rank)
    PlayerInfo.Add("TimesPlayed", Player.TimesPlayed)
    PlayerInfo.Add("HasBoughtGameLevel", 
              CType(Player.BoughtGameLevel, Integer))
    PlayerData.Add(player.PlayerNumber, PlayerInfo)
Next

Msg.Add("PlayerData", PlayerData)

For Each player As GamePerson In Group.Players.Values
    Msg("PlayerNum") = player.PlayerNumber
    If IsTestHarnessMode Then
        TestMsgToPlayer(Group.Id, player.Id, Msg)
    Else
        SendMsgToPlayer(Group.Id, player.id, Msg)
    End If
Next

从 COM 客户端大量使用 .NET Hashtable 类可能很重复且难以维护,特别是当您像我们的库那样嵌套此类时。因此,我们编写了一个方便的可重用包装器来处理 Hashtable 类,以使其更清晰、更易于使用。这是我们 COM 代码使用 CustomHashtable 包装器访问此 Hashtable 的示例。

// Extract the Player Data from the EventArgs
m_LocalPlayerIndex = pHash->Item["PlayerNum"];

ITGHashtablePtr pWrapPlayerData = 
     ITGHashtablePtr(__uuidof(TGHashtable));
pWrapPlayerData->InnerHashtable = 
  (mscorlib::_HashtablePtr)pHash->Item["PlayerData"];

// Here we get an actual IEnumerator
mscorlib::IEnumeratorPtr playerDataEnum = 
             pWrapPlayerData->GetEnumerator();
if (playerDataEnum == NULL)
  return;

// And this is the representation 
// of IDictionaryEnumerator
mscorlib::IDictionaryEnumeratorPtr playerDataEnumDict = 
                                          playerDataEnum;

while(playerDataEnum->MoveNext())
{
  // Get player info
  ITGHashtablePtr pWrapPlayerInfo = 
                  ITGHashtablePtr(__uuidof(TGHashtable));
  pWrapPlayerInfo->InnerHashtable = 
    (mscorlib::_HashtablePtr)playerDataEnumDict->value;

  // Do something . . .
}

可重用 CustomHashtable 包装器的完整源代码可以在下载的源代码中找到。如果我们快速看一下 InnerHashtable 方法,我们可以看到它们通过设置适当的数据成员为我们节省了一些工作。

STDMETHODIMP CTGHashtable::
    get_InnerHashtable(_Hashtable** pVal)
{
  *pVal = m_pInnerHash;
  if(*pVal)
  {
    (*pVal)->AddRef();
  }
  return S_OK;
}

STDMETHODIMP CTGHashtable::
   put_InnerHashtable(_Hashtable* newVal)
{
  m_pInnerHash = newVal;
  m_pCollection = m_pInnerHash;
  m_pDictionary = m_pInnerHash;
  return S_OK;
}

这样我们就可以像这样包装 ICollectionIDictionary 调用。

// ICollection Methods
public:
  STDMETHOD(CopyTo)(_Array * Array, long index)
  {
    return m_pCollection->CopyTo(Array,index);
  }
  STDMETHOD(get_SyncRoot)(VARIANT * pRetVal)
  {
    return m_pCollection->get_SyncRoot(pRetVal);
  }
  STDMETHOD(get_IsSynchronized)(VARIANT_BOOL * pRetVal)
  {
    return m_pCollection->get_IsSynchronized(pRetVal);
  }

  // IDictionary Methods
public:
  STDMETHOD(get_Count)(long * pRetVal)
  {
    return m_pCollection->get_Count(pRetVal);
  }

  STDMETHOD(get_Item)(VARIANT key, VARIANT *pRetVal)
  {
    return m_pDictionary->get_Item(key,pRetVal);
  }

  STDMETHOD(putref_Item)(VARIANT key, VARIANT pRetVal)
  {
    return m_pDictionary->putref_Item(key,pRetVal);
  }
  STDMETHOD(get_Keys)(ICollection * * pRetVal)
  {
    return m_pDictionary->get_Keys(pRetVal);
  }
  STDMETHOD(get_Values)(ICollection * * pRetVal)
  {
    return m_pDictionary->get_Values(pRetVal);
  }
  STDMETHOD(Contains)(VARIANT key, VARIANT_BOOL * pRetVal)
  {
    return m_pDictionary->Contains(key,pRetVal);
  }
  STDMETHOD(Add)(VARIANT key, VARIANT value)
  {
    return m_pDictionary->Add(key,value);
  }
  STDMETHOD(Clear)()
  {
    return m_pDictionary->Clear();
  }
  STDMETHOD(get_IsReadOnly)(VARIANT_BOOL * pRetVal)
  {
    return m_pDictionary->get_IsReadOnly(pRetVal);
  }
  STDMETHOD(get_IsFixedSize)(VARIANT_BOOL * pRetVal)
  {
    return m_pDictionary->get_IsFixedSize(pRetVal);
  }
  STDMETHOD(GetEnumerator)(IDictionaryEnumerator * * pRetVal)
  {
    return m_pDictionary->GetEnumerator(pRetVal);
  }
  STDMETHOD(Remove)(VARIANT key)
  {
    return m_pDictionary->Remove(key);
  }

由于 CustomHashtable 是一个被主“MFCTicTacToe”项目使用的 COM 库,它必须在您的计算机上注册。我们在项目中添加了一个“Post Build Step”来执行所需的注册命令。如果您只是运行演示,可以手动运行以下命令:

  regsvr32 /s /c CustomHashtable.dll

我们第一次尝试编译此 COM 客户端时遇到的一个问题是,我们会收到“Class not registered”错误。这是因为在 .NET 1.0/1.1 中,mscorlib 不是全局程序集缓存 (GAC) 的一部分,根据我所读到的内容,它将在 .NET 2.0 中。现在解决此问题,您可以运行以下命令:

c:\WINDOWS\Microsoft.NET\Framework\v1.1.4322\RegAsm.exe mscorlib.dll

关注点

如果您想了解一种实际方法来将您的托管库公开给 COM 世界,那么本文非常有趣。它尤其有趣,因为它展示了如何模拟 COM 连接点,并且有一个很棒的包装器用于访问 .NET Hashtable 类。如果您有兴趣查看完整的 TGSDK 以制作您自己的多人在线游戏,您可以在 TrayGames 网站上找到它。我想强调的是,使用 TGSDK 编写游戏时,托管代码绝对是首选。如果您想查看一篇关于如何使用 TGSDK 用托管代码编写游戏的文章,请查看我的 Navy Battle 文章。

致谢

特别感谢 Ryan Schneider 为启动此项目付出了大量时间。

修订历史

  • 2005 年 8 月 22 日 - 初始修订。
© . All rights reserved.