다음을 통해 공유


CLR Inside Out

COM Connection Points

Thottam R. Sriram

Code download available at:CLRInsideOut2007_09.exe(252 KB)

Contents

The Sample Scenario
Creating a Connection Point on a COM Server
The Client
Get the Add and ConnectionPoint Interface
Managed Client
Wrap-Up

Atypical scenario in COM has client objects instantiating server objects and then making calls to those objects. Without a special mechanism, however, it would be very difficult for those server objects to turn around and make calls back to the client objects. COM connection points provide this special mechanism, enabling two-way communication between the server and client. Using connection points, the server can call the client when certain events happen on the server.

With connection points, the server specifies the events that it is capable of raising by defining an interface. Clients that have actions to be taken when these events are raised on the server register themselves with the server. The clients subsequently provide the implementation for the interface defined by the server.

There are standard mechanisms through which clients can register themselves with the server. COM provides IConnectionPointContainer and IConnectionPoint interfaces for this.

The clients for a COM connection point server can be written in C++ and in C# managed code. The C++ client registers an instance of a class that provides the implementation for the sink interface. The managed client registers delegates for individual events, thus creating a single sink per event notification method. In the managed world, there are two ways by which the client can register itself—I detail both of these methods later in this column.

There are very few working samples for eventing and interop on the Web. In this column, I focus on creating an Active Template Library (ATL) connection point server. This involves exposing a COM method, defining an event interface that will be implemented by the client, and implementing the code that raises the events from the server. I also show you a sample C++ client that provides an implementation of the sink plus a sample C# client and the two ways you can register and listen to events from the server. Finally, I talk about the recommended way to implement a managed event sink.

The Sample Scenario

In my scenario, the server exposes a COM method:

HRESULT Add(int nFirst, int nSecond)

The server also defines ConnectionPointContainer and connection points so that clients can register with it. In addition, the server defines an interface, _IAddEvents, which has two methods in it:

HRESULT AdditionStarted() HRESULT AdditionCompleted(int nResult)

The client provides the implementation for _IAddEvents and invokes the Add method on the server. The server fires the AdditionStarted and AdditionCompleted methods on the client to notify it appropriately. Then the client performs the appropriate actions associated with these events.

Creating a Connection Point on a COM Server

In the January 2007 installment of CLR Inside Out, I described in detail how to create a simple ATL COM server (see msdn.microsoft.com/msdnmag/issues/07/01/CLRInsideOut). Today's column assumes you've already gone through the process of creating an ATL COM server called ATLConnectionPointServer. So if you haven't done so, you may want to read the earlier column before proceeding.

So now you need to define a COM interface implemented by the server and make it a connection point. The process of creating a connection point on top of this COM server is straightforward. To do this, open the Class View in Visual Studio® and create a simple ATL object. Just right-click on ATLConnectionPointServer and add a class, select a simple ATL object, and provide the name of the class as Add. Be sure to select Supports: Connection Points when you walk through the wizard.

Now you have a server interface IAdd that can be called from the client. If you build the server, you will notice that there are two interfaces defined here. One is IAdd, which implements IDispatch, and the other is the dispinterface _IAddEvents.

Next add a new method, called Add, to the interface IAdd. This takes in two integers and returns an HRESULT. To do this, right-click on IAdd and select Add Methods. The signature of the methods will be:

HRESULT Add([in] int nFirst, [in] int nSecond)

Now open ATLConnectionPointServer.idl and add methods AdditionStarted and AdditionCompleted to the _IAddEvents interface as shown in Figure 1.

Figure 1 Adding AdditionStarted and AdditionCompleted

library ATLConnectionPointServerLib { importlib("stdole2.tlb"); [ uuid(7F45FEA6-4D7C-489C-A852-19BA8B29D8AB), helpstring("_IAddEvents Interface") ] dispinterface _IAddEvents { properties: methods: [id(1), helpstring("AdditionStarted")]HRESULT AdditionStarted(); [id(2), helpstring("AdditionStarted")] HRESULT AdditionCompleted(int nResult); }; [ uuid(15B6C26A-0416-4C8F-9533-89F318355E31), helpstring("Add Class") ] coclass Add { [default] interface IAdd; [default, source] dispinterface _IAddEvents; }; };

If you compile the project at this point, you'll notice an autogenerated file called _IAddEvents_CP.h. This file, which is generated by ATL, contains an empty CProxy_IAddEvents class. This is the class that provides the firing of the events once the connection point is complete and hooked up.

Go to Class View and right-click on CAdd then select Add | Add Connection Point. In the following wizard, select _IAddEvents. If you open the _IAddEvents_CP.h file now, it will contain auto-generated code for two methods, namely Fire_AdditionStarted and Fire_AdditionCompleted. This is the code that calls back to the client sink objects when they register with the server.

You are now close to completing the implementation of the server. All you have remaining is the implementation of the Add method on the server and the trigger points for firing events from the server.

Open Add.cpp and provide an implementation for the Add method that you added. The implementation looks like this:

STDMETHODIMP CAdd::Add(int nFirst, int nSecond) { // Fire AdditionStarted event Fire_AdditionStarted(); int nResult = nFirst + nSecond; Sleep(1000); // simulate the addition taking a long time // Fire AdditionCompleted event Fire_AdditionCompleted(nResult); return S_OK; }

Now just compile the solution and your server is ready.

The Client

Now you can move onto the client. I will start by looking at a C++ client and then I'll move on to a managed client.

The client is responsible for five main tasks:

  • It must provide you with the implementation for the _IAddEvents interface.
  • It must provide an interface pointer to the Add interface on the server.
  • It must get the ConnectionPoint of ConnectionPoinContainer of the Add interface and add the sink interface.
  • It must call the Add method and waits for the events to be fired from the server.
  • It must close cleanly and exit.

To implement the client, open a new C++ project called ConnectionPointClient and add a new C++ source file to the project. Add the ATLConnectionPointServer.h and ATLBase.h file to the project. The sink implements the _IAddEvents that the server has defined. There are two methods in this interface: AdditionStarted and AdditionCompleted. An implementation for these two methods is shown in Figure 2.

Figure 2 AdditionStarted and AdditionCompleted

class CSink : _IAddEvents { private: DWORD m_dwRefCount; public: CSink::CSink() {m_dwRefCount = 0;} CSink::~CSink() {} HRESULT STDMETHODCALLTYPE AdditionStarted() { printf("C++ SINK: Addition started event fired ... \n"); return S_OK; }; HRESULT STDMETHODCALLTYPE AdditionCompleted(int nResult) { printf("C++ SINK: Addition completed event fired ... \n"); printf("C++ SINK: Addition result: %d \n",nResult); return S_OK; }; ...

For simplicity, I have implemented the dispinterface on the client; the sample code provides an ATL client that does this automatically. The sink in this implementation merely prints the fact that it has been called and the result when the addition is completed. Your sink is now implemented and ready to go.

Get the Add and ConnectionPoint Interface

Now that the sink has been implemented, let's work on the client that will register this sink with the server. The client handles three main tasks.

  • It gets the interface pointer to the server Add interface.
  • It gets the ConnectionPoint of ConnectionPointContainer from the Add interface.
  • It registers the Sink interface with the server.

First, you get the interface IAdd to the server as follows:

CoInitialize(NULL); hr = CoCreateInstance( CLSID_Add, NULL, CLSCTX_ALL, IID_IAdd, (void **)&pAdd); if(hr != S_OK) { return; }

Next, you must get the connection point on the server so you can register the sink implementation with it. To do this, get the ConnectionpointContainer from the IAdd interface as follows:

// Using the interface for add, // query for IConnectionPointContainer interface hr = pAdd->QueryInterface( IID_IConnectionPointContainer,(void **)&pCPC); if ( !SUCCEEDED(hr) ) { return; }

Now, you can get to the ConnectionPoint:

// Using the IConnectionPointContainer, // get the IConnectionPoint interface hr = pCPC->FindConnectionPoint(DIID__IAddEvents,&pCP); if ( !SUCCEEDED(hr) ) { return; }

The client now has to create an instance of its sink implementation and register it with the server. To do this, the client creates an instance of the sink and gets its IUnknown interface pointer as follows:

// Create an instance of the sink object to pass // to the server pSink = new CSink(); if ( NULL == pSink ) { return; } // Get the interface pointer to CSink's IUnknown pointer, which you // will pass to the server hr = pSink->QueryInterface (IID_IUnknown,(void **)&pSinkUnk); if(!SUCCEEDED(hr)) { return; }

You are close to completing the client. All that remains is to register the sink with the server, call the server, and clean up. The client registers the instance of the sink with the server:

// Pass the sink interface to the server through the Advise hr = pCP->Advise(pSinkUnk,&dwAdvise); if(!SUCCEEDED(hr)) { return; }

You have now registered the client sink interface with the server.

The client calls the Add method on the server and passes it two arguments as required by the method. The result of the addition is returned through the AdditionCompleted event, not directly from the Add call. Now call the Add method on the IAdd interface pointer you obtained:

pAdd->Add(1, 5);

This call should trigger the firing of the events which in turn should call into the client. At this point, you can clean up the client by releasing all the interfaces you've obtained (see Figure 3).

Figure 3 Clean Up the Client

// Release the IConnectionPointContainer interface. if(pCPC != NULL) pCPC->Release(); // Unadvise the event call back we registered. if(pCP != NULL) { pCP->Unadvise(dwAdvise); } if(pSinkUnk != NULL) { pSinkUnk->Release(); } // Disconnect from the server. if(pCP != NULL) { pCP->Release(); } // Release interfaces. if(pAdd != NULL) { pAdd->Release(); } CoUninitialize(); return;

You're finally done with your client. You can now compile the client and execute it:

cl COMConnectionPointClient.cpp

On execution, you should see the following output:

C++ SINK: Addition started event fired ... C++ SINK: Addition completed event fired ... C++ SINK: Addition result: 6

Managed Client

Now I want to discuss using the same ConnectionPointServer from managed code. The managed client is much simpler than the COM client. There are two ways you can implement the client. First, I'll focus on the recommended approach.

To begin, import the server DLL to managed code to obtain ATLConnectionPointServerLib.dll by using the Microsoft® .NET Framework Type Library to Assembly Converter tool, tlbimp.exe, running the following command:

tlbimp ATLConnectionPointServer.dll

You need to reference the resulting assembly in your managed project and then provide an implementation for the sink interface on the client, such as the one shown in Figure 4. The ManagedSink class implements two methods, AdditionStarted and AdditionCompleted, as defined in the _IAddEvents interface. With that, your sink event handler is now complete and ready. (Seems almost too simple compared to the COM client, doesn't it?)

Figure 4 Provide Implementation for Sink Interface

public class ManagedSink :_IAddEvents { public void AdditionStarted() { Console.WriteLine("C# SINK: Addition started event fired ..."); } public void AdditionCompleted(int nResult) { Console.WriteLine("C# SINK: Addition completed event fired ..."); Console.WriteLine("C# SINK: Addition result: {0}", nResult); return; } };

As with the COM client, you have to register the sink with the server so the server can invoke the sink when firing the events. There are, however, some differences in the way the managed client registers itself with the server.

The COM client registered the instance of its sink object that implemented the interface _IAddEvents with the server. As a reminder, the following call registered the COM client:

// Pass the sink interface to the server through the Advise hr = pCP->Advise(pSinkUnk,&dwAdvise); if(!SUCCEEDED(hr)) { return;}

With the managed client, you register individual methods as delegates with the server. To accomplish this, you create an instance of the sink object:

ManagedSink ms = new ManagedSink();

Create an instance of the server object and add the AdditionStarted and AdditionCompleted event handlers separately, like this:

AddClass a = new AddClass(); a.AdditionStarted += ms.AdditionStarted; a.AdditionCompleted += ms.AdditionCompleted;

The client registers two different interfaces for each of the event handlers with the server. The previous calls to add the delegates on the client add a ref count on the Runtime Callable Wrapper (RCW) on the client. The ref count has to be released by removing the event handler once the call is completed, like so:

a.Add(1, 5); a.AdditionStarted -= ms.AdditionStarted; a.AdditionCompleted -= ms.AdditionCompleted;

Finally, compile ManagedClient.cs:

csc /r:ATLConnectionPointServerLib.dll ManagedClient.cs

And run the executable. You should see the following output:

C# SINK: Addition started event fired ... C# SINK: Addition completed event fired ... C# SINK: Addition result: 6

Wrap-Up

Writing a client implementation for dispinterface in ATL can be slightly complicated. The sample I've discussed here deliberately works around the complexities by having its own Invoke implementation.

I want to thank Cosmin Radu, Ladi Prosek, Mason Bendixen, Varun Sekhri, and Claudio Caldato for all their help and suggestions in making this column happen.

Send your questions and comments to clrinout@microsoft.com.

Thottam R. Sriram has a master's degree in computer science from Concordia University, Montreal, Canada. He is currently a Program Manager on the CLR team working on COM interop. Before joining the CLR team, Thottam worked with the Windows team. Visit his blog at blogs.msdn.com/thottams.