Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Simulate COM Connection Points from a .NET Library

0.00/5 (No votes)
9 Mar 2006 1  
An article on accessing a VB.NET library from a MFC/ATL COM client.

Introduction

This article demonstrates how to access a .NET library from a COM client. It's different from other COM Interop articles in that it simulates connection points for consumption by a COM client implemented using MFC and ATL libraries. The .NET library that we use in this article makes heavy use of the System.Collections.Hashtable class, so we'll also demonstrate a reusable unmanaged C++ wrapper for this class.

Background

At TrayGames, we needed to add support for non .NET game clients to the multi-player online game development SDK (TGSDK) provided to third party developers. Although we prefer the .NET environment because it offers rich capabilities and rapid development we didn't want to exclude developers who have not yet adopted .NET from writing fun games. Since these are turn-based games, our libraries make use of the .NET Framework delegate-based event system so we will go into detail on how to expose this to COM. On the client side we wrote a COM version of our Tic-Tac-Toe sample game (also available in C# and VB.NET) to demonstrate how you can go about accessing our .NET library, but this example can be applied to any .NET library with some small changes.

Using the code

The .NET Framework provides a delegate-based event system to connect an event sender (source) to an event receiver (sink). When the sink is a COM client however, the source must include additional elements to simulate connection points. This article will demonstrate these modifications, and then show how a COM client can register its event sink interface in the normal way by calling the IConnectionPoint::Advise method (we actually use an ATL wrapper method). What also makes this COM client particularly interesting is the wrapper that it uses to access a .NET Hashtable, we will explore that as well as the COM Interop in general.

Our .NET library code is proprietary so I will only provide code snippets showing the changes that need to be made to this (or any) .NET library to make it accessible via COM. The downloadable demo will include only the binaries for the VB.NET library under the "COM Demo" folder. A copy of the game server source code is included with the downloadable source however, under the "CPP Server" folder. On the client side full source code for the sample will be provided, which demonstrates how to access the .NET library. The COM client is implemented in C++ using the MFC and ATL libraries. Also included is the source code for the custom Hashtable project. Both of these projects can be found under the "COM Client" folder in the downloadable source archive.

In order to play the sample game you need to load the game daemon in the Daemon Harness server, "TGDaemonHarnes.exe". Run it from the "Com Demo" folder, click the "Load Game Daemon" button and navigate to the "TicTacDoeDaemon.dll". Select the file and open it. The Daemon Harness should report that the game daemon was started in Test Harness mode, and the number of players required. Now we need to get a couple of games connected to this daemon. We'll need to run two game clients, because it's a two player game. Run one instance of the �MFCTicTacToe.exe� game application, and then another. Upon running the second instance of the game the game daemon should realize it has enough players and proceed to start the game.

Exposing the managed library

We will be using a library called TGGameLib for this example. Understanding what this library does is not important for understanding how to expose it to COM, or how to simulate connection points. Suffice to say it's one of our core libraries for developing a multi-player game. The library is written in VB.NET, if you are using C# or another .NET language the modifications will be the same but the syntax will be slightly different. So let's dig in!

We will be exposing our classes using the ClassInterfaceAttribute class. This class will automatically generate a class interface for an attributed class. A class interface will have the same name as the class itself, but the name will be prefixed with an underscore character. When exposed, the class interface contains all the public members of the managed class, including members inherited from its base class, if any. Although class interfaces eliminate the job of explicitly defining interfaces for each class, their use in production applications is not recommended. Dual class interfaces allow clients to bind to a specific interface layout that is subject to change as the class evolves. That said, this is a much faster way to expose your .NET library to COM and since the TGSDK is still in beta form this was not an issue for us. You can always explicitly define your interfaces at a later point (and have your class implement that interface) after you are up and running, and you know it won't be changing. Strong names are also recommended, but we aren't doing that here either.

We need to expose the various classes, interfaces, enums and events that make up the library. We'll start with the TGGameClass class, just your everyday VB.NET class. Here's what it looks like before being exposed to 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

In order to expose this class we need to add the interop namespace to the file. We must also add the Guid, ClassInterface, and ComVisible attributes to the class. Also, because we want to connect an event sink interface to this class, we must add the ComSourceInterfaces attribute.

The Guid attribute lets you add a Globally Unique Identifier (GUID), which is its unique name, so that your class can be distinguished by COM. You must specify a GUID as a parameter to this attribute, and each type that you want to expose must have a different GUID. Use the "Create GUID" tool under the "Tools" menu in Visual Studio .NET to create a GUID. Paste the given GUID as a parameter to the Guid attribute. Remove the braces from the GUID, and you should have something like this:

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

The ClassInterface attribute controls whether the Type Library Exporter (Tlbexp.exe) automatically generates a class interface for the attributed class. You can use the "Register for COM Interop" project option in Visual Studio .NET (under "Properties->Configuration Properties->Build"), Tlbexp.exe, or Regasm.exe to generate the type library. Regasm.exe is basically a superset of the functionality in Tlbexp.exe. The Visual Studio .NET option is only available if you have the full source. Class interfaces can be dual or dispatch-only interfaces, as stated earlier we are using dual interfaces to get up and run quickly:

  <ClassInterface(ClassInterfaceType.AutoDual)>

The ComVisible attribute controls the accessibility of an individual managed type or member, or of all the types within an assembly, to COM. You can apply this attribute to assemblies, interfaces, classes, structures, delegates, enumerations, fields, methods, or properties. Only public types can be made visible, furthermore this attribute is not needed to make public managed assemblies and types visible, they are visible to COM by default. We use it in our libraries because our assemblies set ComVisible to False. This is a recommendation from FxCop, a tool that we run against our code to ensure some level of compatibility with Microsoft's best practices:

  <ComVisible(True)>

The ComSourceInterfaces attribute defines event interfaces that will be exposed as COM connection points. We must pass the Type of the source interface which we can get easily by using the GetType() method. Note that the class event name and the outgoing interface method name must be the same. We will look at TGGameEvents in a minute but basically we are connecting the event sink interface (outgoing), TGGameEvents, to the TGGameClass class by passing the namespace and event sink interface:

  <ComSourceInterfaces(GetType(TGGameEvents))>

Putting all of these changes together and applying them to TGGameClass will make it available to COM clients, here's what it looks like after applying the attributes:

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

Below we define the event sink interface TGGameEvents to be implemented by the COM sink (discussed in the client section of this article). Unlike the other types we expose from this library, our event interface must be exposed explicitly as an IDispatch interface; you use the InterfaceType attribute to do this. This attribute will allow you to override the way the Type Library Exporter exposes a managed interface to COM by default (as a dual interface). InterfaceType allows you to specify InterfaceIsDual (the default), InterfaceIsIDispatch (a dispinterface for late binding only), or InterfaceIsUnknown (IUnknown derived for early binding only).

In this interface, we expose the event methods by assigning arbitrary DispId values. We've already seen how this event sink interface gets connected to TGGameClass, and we've also defined events representing each of the delegates that we define for the methods in this outgoing interface. Later we will see how the unmanaged client creates an instance of TGGameClass and implements the event sink interface:

<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

The GameEventArgs and the AdminEventArgs classes referenced in the code snippet above are nothing special:

<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

Finally, we have several enumerations that we want to expose to COM as well. We need to apply only the Guid and ComVisible attributes for enums:

<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

An issue we ran into was in our TGDxGameLib library. This library attempts to expose a single interface, IGameClass. Unfortunately, when we tried to register the project output for COM Interop we got the following error: "COM Interop registration failed. There are no registrable types in the built assembly". To solve this problem you can either create a class that implements the interface you are trying to expose, or simply add a dummy class to the project, which is what we chose to do:

' 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

Once you are finished, you can register the assemblies through RegAsm.exe to register them with COM and generate type libraries. Alternatively, you can use the "Register for COM Interop" project option in Visual Studio .NET to register if you have the full source. The type libraries can then be referenced in a client that will sink the events:

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

Creating the unmanaged COM client

For our COM client we decided to do a version of our Tic-Tac-Toe sample game. To create the COM client that will use the .NET library and sink the event interface we created a new "MFC application" using the "New Project..." option in Visual Studio .NET and named it "MFCTicTacToe". After creating the application we chose the "Add Class..." project option and added "ATL Support to MFC". This gave us the boiler plate code we needed to proceed. We started with an MFC application to make doing GUI stuff easier in our sample since it's a game, but you don't necessarily have to. The file TGGameClass.h contains the CTGGameClassEventWrapper class. We created this class as a sink for the outgoing event interface TGGameEvents. Since the client side sink is the highlight of the article we'll look at it first. The basic steps for implementing a connection point sink in our client are:

  • Import the type libraries for each external object using the #import directive.
  • Declare the IDispEventImpl interfaces.
  • Declare an event sink map.
  • Advise and unadvise the connection points.

Let's look at these steps in more detail. At the top of the file we have to add the necessary import statements. We must import the type library for every external object that we want to handle. This will define the events that can be handled and provide information that is used when declaring the event sink map (more on that later). Note that we use the rename option to avoid some name conflicts:

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

Since we only want to supply implementations for the events that we are interested in handling (and not all of the IDispatch stuff) we chose to use the IDispEventImpl template class to provide support for a connection point sink in our ATL class. The sink will allow us to handle events fired from the TGGameClass class. This template class works in conjunction with the event sink map in our CTGGameClassEventWrapper class to route events to the appropriate handler methods. More on those methods later, for now let's look at the constructor call for this template class. The third parameter to the IDispEventImpl constructor is the event dispinterface that we want to implement. This dispinterface must be described in the type library pointed to by the fourth parameter; this is where IDispEventImpl gets its type information about the interface. This is why we had to run RegAsm.exe on the .NET library. Note that in the .NET library we assigned GUIDs to all of the types we wanted to expose to COM. On the COM side, we use the C++ __uuidof operator to retrieve those GUIDs:

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

We also declare a couple of data members that are specific to the logic of this sample:

  HANDLE m_hDataEvent;
  TGGameLib::_TGGameClassPtr TheGameClass;

In order for the event notifications from TGGameClass to be handled by the proper methods, we must route each event to a handler. ATL provides macros, BEGIN_SINK_MAP, END_SINK_MAP, and SINK_ENTRY, which make this mapping simple. We use these macros to create an event sink map for each event (on each object) that we want to handle as such:

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()

Before CTGGameClassEventWrapper becomes visible, each external dispinterface supported by CTGGameClassEventWrapper is queried for outgoing interfaces. A connection is established and a reference to the outgoing interface is used to handle events from the source object, TGGameClass in this case. This process is referred to as "advising". After we're finished with the external interfaces, the outgoing interfaces should be notified that they are no longer used by our class. This process is referred to as "unadvising." We call DispEventAdvise to establish the connection between TGGameClass and CTGGameClassEventWrapper, and DispEventUnadvise to break the connection. We do this in the constructor and destructor code below:

  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;
  }

All that's missing to complete the event sink are the actual event handlers:

  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;

You may notice that two of these event handlers are pure virtual methods. These pure virtual methods are implemented in a derived class CMyTicTacToeGameClass. This class and its event handlers are specific to the logic of this game sample so I'm not going to get into the details here. However, what is of general interest is the wrapper to the .NET System.Collections.Hashtable class, so let's look at it now. When first developing this sample we had to write lots of COM code to pull out information from a .NET Hashtable. This proved to be a pain even with a lot of the free COM code provided by ATL. First let's look at a typical Hashtable as prepared by our server code, note the inner 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

Using the .NET Hashtable class a lot from a COM client can be repetitive and hard to maintain, especially when you are nesting this class as our library does. So, we wrote a handy reusable wrapper for the Hashtable class to make this cleaner and easier. Here is an example of our COM code accessing this Hashtable with our CustomHashtable wrapper:

// 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 . . .

}

The full source code for the reusable CustomHashtable wrapper can be found in the downloadable source code. If we take a quick look at the InnerHashtable methods we can see that they save us some work by setting the appropriate data members:

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;
}

So that we can wrap ICollection and IDictionary calls like this:

// 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);
  }

Since CustomHashtable is a COM library used by the main "MFCTicTacToe" project it will have to be registered on your computer. We've added a "Post Build Step" to the project in order to execute the required register command. If you are just running the demo you can do it manually by running the following command:

  regsvr32 /s /c CustomHashtable.dll

An issue that came up for us when we first tried to compile this COM client was that we would get a "Class not registered" error. This is because in .NET 1.0/1.1 mscorlib isn't part of the Global Assembly Cache (GAC), from what I've read it will be in .NET 2.0. To solve the problem now you can run the following command:

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

Points of interest

This article is interesting if you would like to see a hands on approach to exposing your managed library to the world of COM. It's especially interesting as an example of how to simulate COM connection points and has a nifty wrapper for accessing the .NET Hashtable class. If you are interested in checking out the full TGSDK for producing your own multi-player online games, you can get it at the TrayGames web site. I would like to emphasize that managed code is definitely the preference for writing games using the TGSDK. If you would like to see an article on how to use the TGSDK for writing games in managed code check out my Navy Battle article.

Acknowledgments

An extra special thanks to Ryan Schneider for putting in a considerable amount of his time in getting this project started.

Revision history

  • 22nd August, 2005 - Initial revision.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here