Using COM Events in Managed Code

Team-Fly    

 
.NET and COM Interoperability Handbook, The
By Alan Gordon
Table of Contents
Chapter Seven.  Advanced .NET to COM Interop


One of the most complicated aspects of using COM Interop is its support for connection point events. The Type Library Importer generates a rather complex Interop assembly if your COM server uses connection points. The reason for this complex Interop assembly is the rather convoluted mapping that occurs between delegate-based events in the managed world and connection point events in the COM world. I'm going to assume that you already know about connection points (if you don't, see Chapter 8 of my book, The COM and COM+ Programming Primer ), but, before I get started on handling COM events using Interop, let's talk about .NET delegates and events.

In some cases, the typical client/server model where a server component or application waits passively for a client to make requests on it is not sufficient. In some cases, you need to enable two-way communication between the client and server. In these cases, you usually implement a callback that allows the server to notify the client when some event of interest to the client occurs. This may be anything from notifying the client that an asynchronous operation that the client initiated earlier has succeeded or that a stock has changed price or that a user has pressed a button on a user interface. The basic idea with callbacks is that the client implements a method that performs the logic that the client wants to have performed in response to the event from the server. The client then passes some sort of pointer to this method to the server. The server holds on to this method pointer and calls the method whenever it wants to notify the client that the event occurs. This is exactly how COM connection points work.

Understanding Delegates

With .NET, you use delegates to implement the callbacks that I just described. You can think of a delegate as a type-safe function pointer. After you have declared a delegate, you can point it to any static or per-instance method that matches the argument list and return value of the delegate. For instance, the following code defines a delegate called PlayByPlayHandler:

 public delegate void PlayByPlayHandler(string strLines); 

You can point an instance of this delegate to any method that has a single, string parameter and returns void. I can create an instance of this delegate as shown here:

 PlayByPlayHandler microphone=new         PlayByPlayHandler(ReceivePlayByPlay); 

Where ReceivePlayByPlay is a method that matches the prototype of the delegate as follows :

 void ReceivePlayByPlay(string str) {   //... } 

Now that I have created an instance of this delegate, I can pass it to an object that uses a delegate of this type to issue callbacks. As an example, I created a class called LakerAnnouncer , which issues callbacks using a PlayByPlayHandler delegate:

 1.  public delegate void PlayByPlayHandler(2.      string strLines); 3.  public class LakerAnnouncer 4.  { 5.    public LakerAnnouncer() 6.    { 7.      mLines=new StringCollection(); 8.      mLines.Add("It's in the refrigerator"); 9.      mLines.Add("The butter's getting hard"); 10.       mLines.Add("the jello is jigglin"); 11.       mLines.Add("Slaaaam dunk!"); 12.     mLines.Add("Since Hector was a pup"); 13.     mLines.Add("Yo yoing up and down"); 14.     mRandNumGen=new Random(); 15.   } 16.   public void Announce(int numLines) 17.   { 18.     int r; 19.     string strLine; 20.     if (Microphone != null) 21.     { 22.       for (int i=0;i<numLines;i++) 23.       { 24.         r=mRandNumGen.Next(); 25.         strLine=mLines[r % mNumLines]; 26.         Microphone(strLine + "\r\n"); 27.       } 28.     } 29.   } 30.   private StringCollection mLines; 31.   public PlayByPlayHandler Microphone; 32.   private Random mRandNumGen; 33. } 

This class stores some of the most famous signature lines of Los Angeles Lakers play-by-play announcer Francis "Chick" Hearn, such as "It's in the refrigerator" and "The Jell-O's jiggling." The constructor for this class, which is shown on lines 5 through 15, creates a collection that's typed as a StringCollection and then adds six of Chick Hearn's most famous lines to the collection. It then initializes a counter that stores the number of lines in the collection. The Announce method on lines 16 through 29 takes an integer parameter called numLines that contains the number of play-by-play lines that you want to send to the client. If the Microphone delegate is not null, this method will loop numLines times, and it will generate a random number between 0 and 5 (inclusive), use that number to extract one of Chick Hearn's famous lines from the collection, and then call the Microphone delegate, passing in the selected line. On the client, this will invoke the method that the client assigned to the delegate. Notice also that I defined a public field called Microphone on line 32 that holds the instance of the delegate that the LakerAnnouncer object will call back on. I could have defined this class so that the client passed its delegate as a second parameter to the Announce method. However, with this way, the client is freed from having to maintain a pointer to the delegate if it wants to call the Announce method several times.

Note

The StringCollection class is defined in the System.Collection.Specialized namespace.


When a client wants to receive play-by-play commentary from the LakerAnnouncer, it first has to create a method that takes a string parameter and returns void, which is the prototype that matches the PlayByPlayHandler delegate. This method can be either static or a per-instance method. The client must then create a new PlayByPlayHandler delegate instance and set the Microphone member in the LakerAnnouncer class equal to this new delegate. The client then calls the Announce method specifying the number of lines it would like to receive, and the LakerAnnouncer instance will call back on the method specified in the delegate.

Note

This example is my personal tribute to long-time Los Angeles Lakers play-by-play announcer Chick Hearn who died on August 5, 2002. Chick Hearn is acknowledged by most people to be the best play-by-play announcer of all time, and perhaps the greatest announcer in any sport. He is the creator of such phrases as "slam dunk," "air ball," and "dribble drive." Before he underwent heart surgery in 2002, Chick also had an improbably long streak of 3,338 consecutive games broadcasted. To appreciate how staggering a feat Chick's consecutive games streak is, imagine not missing a day of work since 1965. Some of Chick's other signature lines are putting a game "in the refrigerator," when a team builds up an insurmountable lead. This is usually followed by "the butter's getting hard" and "the Jell-O's jiggling."


The following code shows what this client code will look like:

 1.  public class Form1 : System.Windows.Forms.Form 2.  { 3.  //... additional code in this class is omitted 4.    void ReceivePlayByPlay(string str) 5.    { 6.      txtLines.AppendText(str + "\r\n"); 7.    } 8.    private void cmdAnnounce_Click(object sender, 9.    System.EventArgs e) 10.   { 11.     PlayByPlayHandler microphone1=new 12.       PlayByPlayHandler(ReceivePlayByPlay); 13.     LakerAnnouncer chickHearn=new 14.       LakerAnnouncer(); 15.     chickHearn.Microphone=microphone1; 16.     chickHearn.Announce(5); 17.   } 

Notice that on lines 4 through 7 I declare the ReceivePlayByPlay method, which will just append the play-by-play text to a multiline text box. The key piece of code is on lines 8 through 17. On lines 11 and 12, I declare a delegate called microphone1 that points to the ReceivePlayByPlay method. On lines 13 and 14, I declare an instance of the LakerAnnouncer class. On line 15, I assign the Microphone member of the LakerAnnouncer class to the delegate that I created on lines 11 and 12. Finally, on line 16, I call the Announce method with a parameter of 5, meaning that the LakerAnnouncer instance will call the ReceivePlayByPlay method with five randomly selected Chick Hearn quotes. In this example, the LakerAnnouncer object will call back on the ReceivePlayByPlay method five times.

So far, I have only shown the case where the server calls back on a single method, but a delegate can actually point to several methods. All of these methods will be called when the server calls back on the delegate, which is called multicasting.

Note

If you look at the MSIL code that the C# compiler generated when I declared the PlayByPlayHandler, you will see that the generated PlayByPlayHandler class derives from a class called MulticastDelegate. This class provides all the logic that you need to support multicasting.


A client can add multiple callbacks by using the "+=" operator to add multiple delegates to the Microphone member of the LakerAnnouncer class. The following code shows how you would add two callbacks to a LakerAnnouncer, one that appends to a multiline textbox and another that displays a message box:

 1.  void ReceivePlayByPlay(string str) 2.  { 3.    txtLines.AppendText(str + "\r\n"); 4.  } 5.  void ReceivePlayByPlay2(string str) 6.  { 7.    MessageBox.Show(str); 8.  } 9.  private void cmdAnnounce_Click(object sender, 10. System.EventArgs e) 11. { 12.   PlayByPlayHandler microphone1=new 13.     PlayByPlayHandler(ReceivePlayByPlay); 14.   PlayByPlayHandler microphone2=new 15.     PlayByPlayHandler(ReceivePlayByPlay2); 16.   LakerAnnouncer chickHearn=new LakerAnnouncer(); 17.   chickHearn.Microphone+=microphone1; 18.   chickHearn.Microphone+=microphone2; 19.   chickHearn.Announce(5); 20. } 

On lines 1 through 4, I declare a method called ReceivePlayByPlay, which has a prototype that matches the PlayByPlayHandler delegate. On lines 5 through 8, I declare a second method called ReceivePlayByPlay2 that also matches the PlayByPlayHandler delegate. On lines 12 and 13, I create a PlayByPlayHandler delegate called microphone1 that points to ReceivePlayByPlay, and, on lines 14 and 15, I create a PlayByPlayHandler delegate called microphone2 that points to ReceivePlayByPlay2. On line 16, I create a new LakerAnnouncer instance, and, on line 17, I add the microphone1 delegate to the delegate member in the LakerAnnouncer instance using the "+=" operator. On line 18, I use the "+=" operator again to add the microphone2 delegate to the Microphone delegate in the LakerAnnouncer class. Now, both the ReceivePlayByPlay and ReceivePlayByPlay2 methods will be called when the LakerAnnouncer class calls back on the Microphone delegate.

Understanding Events

Of course, you can probably see that there are a lot of problems with .NET delegates as I have presented them so far. The first problem is that there is no standardized way for the client to pass its delegate to the server. In the previous case, I simply exposed a public delegate field, and the client accessed this field directly. If you find exposing a public field from a class deplorable (and you should), you could have declared the delegate field to be private and exposed a public property instead. You could even have declared the field to be private and added an AddConsumer method for a client to pass its delegate to the server and a RemoveConsumer method for halting further notifications from the server.

Another big problem with delegates is that it is very easy to make a critical mistake when using multicasting. Using the "=" operator on a delegate will assign the delegate on the right side of the equals sign to the one on the left side of the "=" sign. Any previous assignments will be lost. The "+=" operator will add (combine is the official lingo) the delegates on the left and right side of the equals and then assign the result to the delegate on the left. Therefore, if you plan to use multicasting, but accidentally use the "=" operator instead of the "+=" operator, you will wind up with a delegate that contains only the last delegate that you added. As an example, the following code will not display the result that you would expect:

 private void cmdAnnounce_Click(object sender, System.EventArgs e) {       PlayByPlayHandler microphone1=new       PlayByPlayHandler(ReceivePlayByPlay);       PlayByPlayHandler microphone2=new         PlayByPlayHandler(ReceivePlayByPlay2);       LakerAnnouncer chickHearn=new LakerAnnouncer();  chickHearn.Microphone=microphone1;   chickHearn.Microphone=microphone2;  chickHearn.Announce(5); } 

If you ran this client code, you will notice that only the second delegate microphone2 will be called. The problem is that I assigned the microphone2 instance to the delegate in the LakerAnnouncer instance using the "=" operator instead of using the "+=" operator. Therefore, the second assignment to the Microphone delegate will overwrite the first. If I changed the code to look as shown here, both the microphone1 and microphone2 delegates will be called.

 private void cmdAnnounce_Click(object sender, System.EventArgs e) {       PlayByPlayHandler microphone1=new       PlayByPlayHandler(ReceivePlayByPlay);       PlayByPlayHandler microphone2=new         PlayByPlayHandler(ReceivePlayByPlay2);       LakerAnnouncer chickHearn=new LakerAnnouncer();  chickHearn.Microphone+=microphone1;   chickHearn.Microphone+=microphone2;  chickHearn.Announce(5); } 

To prevent someone from making this mistake, I could make the Microphone delegate field in the LakerAnnouncer class private and use a pair of methods (like AddConsumer and RemoveConsumer) that would always use the correct operator internally. Of course, this is extra code that I would have to write every time I wanted to use delegates. Fortunately, there is a better way to do this using .NET events.

To use events in the LakerAnnouncer class, you would still begin by declaring a delegate that defines the prototype for the callback function that the client must create.

 public delegate void PlayByPlayHandler(string strLines); 

The definition of the delegate is the same as the one I used earlier. However, instead of defining a public field to hold the delegate, I simply declare an event that uses the PlayByPlayHandler delegate as follows:

 public event PlayByPlayHandler PlayByPlay; 

The complete definition of the LakerAnnouncer class will now look as follows:

 using System; using System.Collections.Specialized; namespace EventServer {       public delegate void PlayByPlayHandler(string strLines);       public class LakerAnnouncer       {         public LakerAnnouncer()         {         //... same as before         }         public void Announce(int numLines)         {             int r;             int strLine;             for (int i=0;i<numLines;i++)             {                 r=randomNumberGenerator.Next();                 strLine=mTrademarkLines[r % numEntries];                 PlayByPlay(strLine + "\r\n");             }         }         public event PlayByPlayHandler PlayByPlay;         private StringCollection mTrademarkLines;         private Random randomNumberGenerator;         private int numEntries;       } } 

The only change in the source code is the line where I declare the PlayByPlayHandler event. You can understand better how making this change alters the code by looking at the MSIL code that the C# compiler generated for the altered LakerAnnouncer class using ILDASM.

Figure 7-2. Events displayed in ILDASM.

graphics/07fig02.jpg

You'll notice that the event declaration caused the C# compiler to add three new members to the class:

  • A private member variable called PlayByPlay that holds an instance of the PlayByPlayHandler delegate

  • An add_PlayByPlay method that a client can use to register a delegate to receive callbacks

  • A remove_PlayByPlay method that a client can call when it no longer wants to receive callbacks

A cleaned-up version of the MSIL code showing the members that the compiler adds is as follows:

 1.  public class LakerAnnouncer 2.  { 3.    public LakerAnnouncer() 4.    { 5.    //... same as before 6.    } 7.    public void Announce(int numLines) 8.    { 9.    //... same as before 10.   }  11.   [hidebysig specialname]   12.   void add_PlayByPlay(PlayByPlayHandler aDelegate)   13.   {   14.   // implementation omitted   15.   }   16.   [hidebysig specialname]   17.   void remove_PlayByPlay(PlayByPlayHandler aDelegate)   18.   {   19.   // implementation omitted   20.   }   21.   private PlayByPlayHandler PlayByPlay;  22.   private StringCollection mTrademarkLines; 23.   private Random randomNumberGenerator; 24.   private int numEntries;  25.   .event PlayByPlayHandler PlayByPlay   26.   {   27.      .addon instance void add_PlayByPlay(   28.     class PlayByPlayHandler)   29.      .removeon instance void remove_PlayByPlay(   30.     class PlayByPlayHandler)   31.   }  32. } 

The bolded lines are actually snippets of MSIL code that I got from ILDASM. The compiler did what I proposed to do earlier to prevent people from accidentally using the wrong operator when using delegates, that is, the compiler added a private PlayByPlayHandler delegate instance and then added public methods that a client can use to add and remove its delegates to this internal instance. The add_PlayByPlay and remove_PlayByPlay methods have special attributes, hidebysig and specialname, that cause them to be hidden (see lines 11 and 16). Lines 25 through 31 have an .event declaration for the PlayByPlayHandler event, which specifies addon and removeon methods. These are assigned to add_PlayByPlay and remove_PlayByPlay, respectively. All of this code is hidden from you. The CLR will automatically call the addon method when you use the "+=" operator on the event, and it will call the removeon method when you call the "-=" operator on the event. Another key advantage of .NET events over using plain delegates is that the Visual Studio .NET IDE recognizes events. Therefore, it presents a class' events to you in an easy-to-use way and allows you to add handlers for those events with point-and-click ease.

The following code shows client-side code that uses these events:

 void ReceivePlayByPlay(string str) {       txtLines.AppendText(str + "\r\n"); } void ReceivePlayByPlay2(string str) {       MessageBox.Show(str); } private void cmdAnnounce_Click(object sender,       System.EventArgs e) {       PlayByPlayHandler microphone1=new         PlayByPlayHandler(ReceivePlayByPlay);       PlayByPlayHandler microphone2=new         PlayByPlayHandler(ReceivePlayByPlay2);       LakerAnnouncer chickHearn=new LakerAnnouncer();       chickHearn.PlayByPlay+=microphone1;       chickHearn.PlayByPlay+=microphone2;       chickHearn.Announce(5); } 

Notice that this code looks very similar to the code that I wrote before that did not use events. The main difference is that, if you attempt to assign a delegate to the PlayByPlay event by (incorrectly) using the "=" operator, you will receive the following error from the compiler:

 The event 'LakerAnnouncer.PlayByPlay' can only appear on the left hand side of += or -= 

Events are the mechanism that UI controls use to fire events back to their container, for example, to fire a click event when you click a button. UI controls always use the following prototype for their delegate:

 Public delegate void MyDelegate(object sender, EventArgs e) 

The first parameter to the delegate is the object that is sending the event. The second parameter is an instance of the EventArgs class. EventsArgs is a class that resides in the System namespace. When you define a delegate that will be used with an event, you should also create a class that derives from Event-Args and add the member variables that your event needs to pass to the code that receives the event. For instance, the MouseDown event on the System.Windows.Forms has the following delegate:

 Public delegate void MouseEventHandler(object sender,     MouseEventArgs e) 

The MouseEventArgs class inherits from EventArgs and adds X, Y, Button, Clicks, and Delta properties, which respectively store the x coordinate, y coordinate, button state, number of button clicks, and the amount of wheel rotation for the mouse.

I would be remiss if I left the subject of events and did not discuss Microsoft's recommended design pattern for events. Microsoft's recommendation is that you create all events in a manner similar to UI events, that is, the delegate for the event should return void and have two parameters, one of which should contain the object that generated the event and the other should be an EventArgs object that contains the data that the event will pass to its client. You should try to follow this pattern as much as possible in the new .NET code that you write. When you are using COM Interop, however, the delegate that the type library importer generates will match the signature of the equivalent COM event.

Note

I discuss the .NET guidelines for defining events in much more detail in Chapter 8.


Let's quickly review COM connection points before I discuss how they map to .NET events and delegates. A COM object indicates that it supports events by publishing one or more source interfaces as shown here:

 [ uuid(4506CE8F-2A84-11D3-998A-E0EC08C10000) ] dispinterface _IStockMonitorEvents { methods:     [id(0x00000001)]     HRESULT PriceChange([in] BSTR ticker,          [in] single newPrice, [in] single oldPrice);     [id(0x00000002)]     HRESULT MonitorInitiated([in] BSTR ticker,          [in] single currentPrice); }; [uuid(4506CE8E-2A84-11D3-998A-E0EC08C10000)] coclass StockMonitor {     [default] interface IStockMonitor;  [default, source] dispinterface _IStockMonitorEvents;  }; 

In this case, the _IStockMonitorEvents interface is a source interface. If a COM object includes a source interface in its type library, that means that the interface is not implemented by the object. Instead, the COM object is advertising that a client can receive callbacks from the object if the client implements the source interface and then passes its implementation of the source interfacethrough the Advise method in the IConnectionPoint interfaceto the COM object. In the language of COM connection points, the object is indicating that it is a source for events, that is, it produces connection point events. An object that sources events is sometimes called a connectable object. If a client implements the source interface and then passes its implementation of the interface to the connectable object, it becomes a sink, that is, it consumes the events.

A COM object that supports connection points must implement two interfaces: IConnectionPoint and IConnectionPointContainer. These interfaces provide the means for a client to pass its sink object to the server. The client uses the FindConnectionPoint or EnumConnectionPoints methods on the IConnectionPointContainer interface to find the IConnectionPoint interfaces supported by the connectable object. There should be one IConnectionPoint implementation for each source interface. The client then passes the IUnknown pointer of the Sink object to the Advise method on IConnectionPoint. The COM specification requires that the connectable object immediately do a QueryInterface on the Sink object to verify that the sink object implements the interfaces that the IConnectionPoint implementation is associated with. Assuming that the sink object does implement the source interface, the connectable object will cache the interface pointer on the sink object and use it to call back on the source interface when an event of interest occurs on the client.

Now that I have beaten the subject of .NET delegates and events and COM connection points to death, let's bring the discussion back to the topic at hand: how COM connection point events are mapped to managed code. Before I dive into this, let me warn you that this mapping is quite complicated and very hard to understand. You will most likely have to read this section a few times before it makes sense to you. It is absolutely critical that you understand how .NET events and delegates and COM connection points work independent of COM Interop before you dive in here. The good news is that, even though understanding the mapping of COM connection point events to .NET managed events is difficult, using the resulting Interop assembly is really simple, assuming you understand managed events. All you have to remember is that each method in the source (event) interfaces in your COM component will map to a managed event. In order to receive those events from a managed client, you only need to add a method that has the same prototype as the delegate associated with the managed event and then use the "+=" operator to add your method as a callback for the event. Because of this, I will start by doing an example, and then I'll peek behind the scenes and explore how it works.

Because you are probably getting sick of calculating monthly payments and loan amounts, let's do something a little different this time. For this example, I am going to use a stock monitoring application that I built in Chapter 8 of my first book: The COM and COM+ Programming Primer (Prentice Hall). The code for this example is available on the Web site for the book that you are now holding. This example is an out-of-process server that monitors stock prices and sends an event when a stock's price changes. I won't go into any details on how I built this COM component; there are step-by-step instructions for how to build this component in my first book.

Note

The stock monitor component obviously does not really monitor stock prices. The component loops continuously, and, each time through its loop, it will use a random number generator to pick a stock whose price it will change. It then uses a factor, which I call the propensity , to determine whether the price will go up or down. The stock monitor component will change the price of the stock and then sleep for 3 seconds before looping again.


The IDL for the stock monitor component is the following:

 [ uuid(4506CE81-2A84-11D3-998A-E0EC08C10000)] library STOCKSERVERLib {     [ uuid(4506CE8F-2A84-11D3-998A-E0EC08C10000) ]     dispinterface _IStockMonitorEvents {         properties:         methods:             [id(0x00000001)]             HRESULT PriceChange([in] BSTR ticker,                [in] single newPrice,                [in] single oldPrice);             [id(0x00000002)]             HRESULT MonitorInitiated([in] BSTR ticker,                [in] single currentPrice);     };     [uuid(4506CE8E-2A84-11D3-998A-E0EC08C10000)]     coclass StockMonitor {         [default] interface IStockMonitor;         [default, source] dispinterface                  _IStockMonitorEvents;     };     [ uuid(4506CE8D-2A84-11D3-998A-E0EC08C10000),dual]     interface IStockMonitor : IDispatch {         [id(0x00000001)]         HRESULT GetPrice([in] BSTR ticker,                [out, retval] single* price);         [id(0x00000002)]         HRESULT AddNewStock([in] BSTR ticker,                [in] single price,                [in] short propensityToRise);     }; }; 

Notice that it has a source interface called IStockMonitorEvents, which has two callback methods: (1) MonitorInitiated, which the object will call whenever you register a new stock for monitoring, and (2) PriceChange, which is called whenever the price of a stock changes. Let's run the Type Library Importer on this COM component to generate an Interop Assembly. The Stock Server is an executable, so I ran the following command:

 tlbimp stockserver.exe /out:stockserver.dll 

This command will save the Interop Assembly to a file called stockserver.dll .

When you run tlbimp on a COM type library, it will generate a managed code equivalent of each regular COM interface in the type library. It will also generate a coclass interface and an RCW class (see Chapter 6 for a reminder of what these are) for each coclass in the type library. If COM classes in the type library support source interfaces (COM events), the Type Library Importer will also generate a delegate for each event, that is, there will be one Delegate for each method in the original source interface. The Type Library Importer will also generate a managed code equivalent of the source interface, and it will generate the following three types for each source interface (there's usually only one source interface):

  • A managed code interface that has the following name : [ SourceInterfaceName]_Event that is very similar to the source interface, but has add_[MethodName] and remove_[MethodName] methods for each method in the source interface.

  • A class called [SourceInterfaceName]_EventProvider that implements the interface described in the previous bullet. The CLR will call the add_ and remove_ methods on this class when you add or remove a delegate from the events exposed by this class. This class is hidden and is only used internally within the Interop assembly.

  • A class called [SourceInterfaceName_SinkHelper] that the class described in the previous bullet uses to implement the add_ and remove_ methods. This class is a sink object for the original COM event. The provider will pass an instance of this class to the Advise method of IConnectionPoint, and, when the COM object calls back on this sink object, it will forward the method call to the managed code delegate. This class is hidden and is only used internally within the Interop assembly.

If you examine the Interop assembly that ILDASM generated for the StockServer, you will see the types shown in Figure 7-3.

Figure 7-3. The Interop assembly for the StockServer.

graphics/07fig03.jpg

There is a total of nine types in this assembly. IStockMonitor, StockMontior, and StockMonitorClass are the default interface, coclass interface, and RCW class respectively. The StockServer COM component has two events: PriceChange and MonitorInitiated. Therefore, the interop assembly also contains two delegates: PriceChangeEventHandler and MonitorInitiatedEventHandler. _IStockMonitorEvents is the managed code equivalent of the source interface in the Stock Server, and _IStockMonitorEvents_Event, _IStockMonitorEvents_EventProvider, and _IStockMonitorEvents_SinkHelper are the three types described previously that are generated for the default source interface in the COM component. Table 7-1 summarizes these types.

Table 7-1. Managed types in the Interop assembly for a COM component that has connection point events

Type Name

Description

IStockMonitor

The default interface from the Stock Server COM class.

StockMonitor

The coclass interface that implements IStockMonitor and _IStockMonitorEvents_Event.

StockMonitorClass

The RCW class that implements IStockMonitor, StockMonitor, and _IStockMonitorEvents_Event. Also contains every method implemented by the class, including hidden add_ and remove_ methods for each interface.

PriceChangeEventHandler

A delegate that maps to the PriceChange event.

MonitorInitiatedEventHandler

A delegate that maps to the MonitorInitiated event.

_IStockMonitorEvents

The outgoing (event) interface from the Stock Server COM class.

_IStockMonitorEvents_Event

An interface that is the managed code equivalent of the _IStockMonitorEvents interface but also includes add_ and remove_ methods for each method in the event interface.

_IStockMonitorEvents_EventProvider

A class that implements _IStockMonitorEvents. The CLR will call the add_ and remove_ methods on this class when you add or remove a delegate from the events exposed by this class. This class is hidden and is only used internally within the Interop assembly.

_IStockMonitorEvents_SinkHelper

A helper class that implements the _StockMonitorEvents interface and stores public PriceChange and MonitorInitiated delegate instances and does null checking, that is, if the delegates are not set, it simply returns without doing anything. If the delegates are set, it passes the call through to the underlying delegate. This class is hidden and is only used internally within the Interop assembly.

Only seven of these types are visible outside of the Interop Assembly; _IStockMonitorEvents_EventProvider and _IStockMonitorEvents_SinkHelper are hidden classes. The first three classes, IStockMonitor, StockMonitor, and StockMonitorClass, would have been generated even if the COM server did not have any source interfaces. The other six classes are event related . This is the complicated part that I warned you about earlier. I will explain how these six classes work shortly, but, because using the Interop assembly is easy, let me show you that first. Let's build a managed code client using Windows Forms.

Create a Windows Forms Client

To create a Windows Forms client, you perform the following steps:

  1. Create a Visual C# Windows application project.

  2. Draw the UI.

  3. Reference the StockServer assembly.

  4. Implement the UI.

  5. Compile the application.

  6. Test.

CREATE A VISUAL C# WINDOWS APPLICATION PROJECT

Start Visual Studio .NET and then execute the following steps:

  1. Select File New Project. The New Project dialog appears.

  2. Select Visual C# Projects under Project Types and then choose Windows Application under Templates.

  3. Change the name of the project to StockClient.

  4. Select the location where you would like to save the project.

  5. Click OK.

Visual Studio .NET will create a project that initially contains a single form that will be the main window of your application.

DRAW THE UI

Using the toolbox in Visual Studio .NET, draw the UI shown in Figure 7-4 on your form.

Figure 7-4. The UI for the Windows Forms client.

graphics/07fig04.jpg

Name the controls on this form according to Table 7-2.

Table 7-2. Control names for the stock server client

Control Description

Control Name

Events list box in the upper half of the window.

lstEvents

Ticker edit box

txtTicker

Price edit box

txtPrice

Add button

cmdAdd

Propensity spinner

updPropensity

REFERENCE THE STOCKSERVER ASSEMBLY

With Visual Studio .NET, it's easy to reference and use .NET assemblies. To do this, perform the following steps:

  1. Select Project Add Reference. The Add Reference dialog appears.

  2. Click the Browse button. The Select Component dialog will appear. Navigate to where you saved the stockserver Interop assembly. This assembly was generated when you ran tlbimp on the stockserver COM component.

  3. Click the Open button.

  4. Click OK on the Add Reference dialog.

In this case, I am using the Interop assembly that I manually generated at the start of this example. You could also generate the Interop assembly in Visual Studio .NET by performing the following steps:

  1. Select Project Add Reference.

  2. Select the COM tab.

  3. Highlight the COM component with the name "stock server 1.0 Type Library" and click the Select button. If you do not see this item in the list, you need to register the stock server by running the following command at a command prompt: stockserver /regserver.

  4. Click OK.

If you use the Add Reference command in Visual Studio .NET, it will create an Interop assembly with the name Interop.STOCKSERVERLib.dll in the build directory for your current configuration. If you are currently working on the debug configuration, you will find the Interop assembly in the bin\debug directory beneath your project directory. Visual Studio .NET will use the type library name as the namespace, so all of the types from the Stock Server assembly will reside in a namespace called StockServerLib. You will probably want to add a using statement like the following somewhere in the client code:

 using STOCKSERVERLib; 

If you manually generated the Interop assembly, that is, you used the Type Library Importer (tlbimp.exe) directly instead of using the Add References dialog in Visual Studio .NET, the assembly will have the name, StockServer, and you would have a using statement similar to the following:

 using stockserver; 
IMPLEMENT THE UI

In the form designer, double-click the form, which should take you to the handler for the Load event of the form, which gets called automatically when the form first loads. The Load handler and all the other code in the file is part of a class called Form1 that represents the main Windows of this application. Add the code shown in bold to this class:

 1.  // Add the using statement to the top of the file  2.  using stockerserver // or using STOCKSERVERLib  3.  public class Form1 : System.Windows.Forms.Form 4.  { 5.  //... code is omitted here for clarity 6.  private StockMonitor mStockServer;  7.  8.    private void MonitorInitiated(string ticker,   9.    float currentPrice)   10.   {   11.     string strMsg;   12.     strMsg="Monitor initiated for " + ticker +   13.       ", Current price $" +   14.       currentPrice.ToString();   15.     MessageBox.Show(strMsg,"Monitor Started");   16.   }   17.   public void PriceChange(string ticker,   18.     float newPrice, float oldPrice)   19.   {   20.     string strMsg;   21.     strMsg = "Stock: " + ticker +   22.     "  New Price: $" + newPrice.ToString() +   23.     "  Old Price: $" +   24.     oldPrice.ToString();   25.     lstEvents.Items.Add(strMessage);   26.   }  27.   private void Form1_Load(object sender, 28.   System.EventArgs e) 29.   {  30.     mStockServer = new StockMonitor();   31.     mStockServer.PriceChange+=new   32. _IStockMonitorEvents_PriceChangeEventHandler(PriceChange);   33.     mStockServer.MonitorInitiated+=new _IStockMonitorEvents_MonitorInitiatedEventHandler(MonitorInitiated);  34.   } 35. } 

On line 2, I add a using statement for the Interop assembly. If you generated your Interop Assembly yourself on the command line (and used the name stockserver.dll for the Interop assembly name), you will use stockserver for the namespace name, or you'll use STOCKSERVERLib if you generated the Interop assembly using Visual Studio .NET.

On line 6, I will declare a StockMonitor instance. On lines 8 through 26, I declare two methods, MonitorInitiated and PriceChange, that I will use to receive the callbacks. Notice that the MonitorInitiated method returns a void and takes a string, which contains the name of the new stock, and a float, which contains the price of the stock. This matches the prototype of the MonitorInitiated event from the COM object. The PriceChange method returns void and takes a string that contains the name of the stock and two floats, which contain the old and new price. This matches the prototype of the PriceChange event from the COM object.

The Form Load method is defined on lines 27 through 34. On line 30, I create a new instance of the StockMonitor. On line 31, I create a new instance of the PriceChangeEventHandler delegate and add this delegate to the PriceChange event on the StockMonitor instance. On line 33, I create a new instance of the MonitorInitiatedEventHandler delegate, and I add this delegate to the MonitorInitiated event on the StockMonitor instance.

Back in the form designer, double-click the Add button to add a Click event handler and add the following code:

 private void cmdAdd_Click(object sender, System.EventArgs e) {       mStockServer.AddNewStock(txtTicker.Text,           float.Parse(txtPrice.Text),           (short)updPropensity.Value); } 

This method adds a new stock for monitoring. If you run the application, you will almost immediately begin seeing price change events showing up in the event list. The Stock Server has an initial list of stock monitors that includes the following stocks: Microsoft (MSFT), Northrop Grumman (NOC), IBM (IBM), Coca Cola (COKE), Emulex (EMLX). You can also add additional stock monitors by filling in the name, price, and propensity to rise for a new stock and then click the Add button. Now that you've seen how easy it is to use COM events from a managed client, let's explore how the mapping of COM events to managed delegates is accomplished.

As you learned earlier, when a .NET client wants to receive events from a managed code object, the client will instantiate a delegate using a callback method that has the same prototype as the delegate associated with the event. The client will then use the "+=" operator to add the delegate to the event. To link connection point events and .NET events, I will need a sink object that implements the source interface that the COM object exposes. When the connectable object calls this sink object, it should forward the method call to the delegate, which will then call the callback method that the .NET client specified (see Figure 7-5).

Figure 7-5. Linking COM connection points and .NET events.

graphics/07fig05.gif

The problem is, how do you pass this sink object from the client to the server? What you essentially need is a custom implementation of the "+=" and "-=" operators for each event. When you add a callback for an event, this implementation should get the IConnectionPoint interface associated with the event from the COM object and pass the special sink object to the Advise method of IConnectionPoint. The implementation of the "+=" operator should cache the cookie that the Advise method returns. When you no longer want to receive events, the "-=" operator should use the cached cookie to call the Unadvise method in the IConnectionPoint.

The _IStockMonitorEvents_EventProvider class (see Table 7-1) contains the "+=" and "-=" methods that I described earlier. Remember from my discussion of events that declaring an event adds an add_ [EventName] and a remove_[EventName] method to your class. The implementation of these methods delegates to the methods with similar names in the [SourceInterfaceName]_EventProvider class. You can actually see the implementation of these methods (in MSIL code) if you run ILDASM on the Interop assembly. To view the implementation of the add_PriceChange method in the StockServer Interop assembly, perform the following steps:

  1. Bring up the MSIL Disassembler by entering the following command at a Visual Studio .NET command prompt: ildasm StockServer.dll (or Interop.StockServerLib.dll if you used the Add Reference command in Visual Studio .NET to create your interop assembly).

  2. Click the "+" symbol next to the class called _IStockMonitorEvents_ EventProvider .

  3. Double-click the purple box (which is the symbol for a method) labeled add_PriceChange.

  4. The MSIL code for the add_PriceChange method will appear in a Window.

Rather than showing you the IDL for this method and the related methods in the _IStockMonitorEvents_EventProvider class, I translated the code for the add_PriceChange, remove_PriceChange, and Init methods into the C# code that follows.

Note

The ease with which I was able to do this should serve as a reminder to be careful about the unobfuscated assemblies that you ship. See my note in Chapter 3 about available obfuscation technologies.


 1.  private sealed class _IStockMonitorEvents_EventProvider   2.  { 3.  private ArrayList m_aEventSinkHelpers; 4.  private UCOMIConnectionPoint m_connPt; 5.  private UCOMIConnectionPointContainer m_connPtCtr; 6.  void add_PriceChange(7.  _IStockMonitorEvents_PriceChangeEventHandler aDeleg) 8.  { 9.      _IStockMonitorEvents_SinkHelper objSink; 10.     int aCookie; 11.     lock(this) 12.     { 13.       if (m_connPt==null) 14.         Init(); 15.       objSink =new 16.         _IStockMonitorEvents_SinkHelper(); 17.       aCookie=0; 18.       m_connPt.Advise(objSink,&aCookie); 19.       objSink.m_dwCookie=aCookie; 20.       objSink. m_PriceChangeDelegate=aDeleg; 21.       m_aEventSinkHelpers.Add(objSink); 22.     } 23. } 24. private void init() 25. { 26.     System.Guid aGuid; 27.     byte[] guidValue= new byte[16]; 28. //...initialize array with the GUID for the 29. // StockServer's default, source interface. 30.     AGuid=new System.Guid(guidValue); 31.     m_ConnPtCtr.FindConnectionPoint(aGuid,&mconnPt); 32.     m_aEventSinkHelpers=new ArrayList(); 33. } 34. void  remove_PriceChange(35.    _IStockMonitorEvents_PriceChangeEventHandler aDeleg) 36. { 37.     int numSinks, i; 38.     _IStockMonitorEvents_SinkHelper objSink; 39.     lock(this) 40.     { 41.       i=0; 42.     numSinks=m_aEventSinkHelpers.get_Count(); 43.     if (numSinks > 0) 44.     { 45.         do 46.         { 47.       objSink = m_aEventSinkHelpers.get_Item(i); 48.       if (objSink.m_PriceChangeDelegate != null) 49.       { if (aDeleg.Equals(objSink.m_PriceChangeDelegate) 52.           { 53.         aEventSinkHelpers.RemoveAt(i); 54.            m_connPt.UnAdvise(objSink.m_dwCookie); 55.         if (numSinks <= 1) 56.         { 57.             m_connPoint=null; 58.             _aEventSinkHelpers=null; 59.             break; 60.         } 61.         break; 62.           } 63.       } 64.       i++; 65.         } while (i < numSinks); 66.      } 67.     } 68.  } 69. } 

On lines 3 through 5, I show the declarations of three private variables in the _IStockMonitorEvents_EventProvider:

  • m_aEventSinkHelpers Contains a list of the sink objects that are currently attached to the underlying COM object.

  • m_connPt Contains an IConnectionPoint interface pointer for the default source interface. This variable is actually called m_ConnectionPoint in the MSIL code. I shortened the name to improve the formatting and readability of the source code.

  • m_connPtrCtr Contains an IconnectionPointContainer interface pointer for the underlying COM object. This variable is actually called m_ConnectionPointContainer in the MSIL code.

Lines 7 through 23 contain the definition of the add_PriceChange method. This method takes a single parameter, which is a delegate that will receive the events from the COM object. On lines 9 and 10 are the declarations of a sink object and a variable to hold the cookie for the connection to the COM object. The lock(this) statement on line 11 ensures that this method is thread safe. On lines 13 and 14, the code checks to see if the IConnectionPoint interface pointer (m_connPt) has been initialized . If it has not, the code calls the Init method, which calls the FindConnectionPoint method on the IConnectionPointContainer interface pointer (m_connPtCtr) to initialize the connection point. The Init method also initializes the m_aEventSinkHelpers ArrayList. On lines 15 and 16, the code creates a new sink object, and, on line 18, the code calls the Advise method on the interface pointer, passing in the sink object. The cookie for this connection is returned in the second parameter to the Advise method. On line 19, I assign the cookie that was returned from the Advise method to the m_dwCookie field of the sink object, and, on line 20, I assign the delegate that was passed into the add_PriceChange method to the m_PriceChangeDelegate field of the sink object. Finally, on line 21, I add the sink object to the list of sinks that you currently have on the COM object. Lines 24 through 33 contain the implementation of the Init method that I mentioned earlier. The implementation is fairly simple. It calls FindConnectionPoint on the IConnectionPointContainer interface that is exposed by the COM object and requests the default source interface. On line 32, this method also creates the ArrayList that will hold the list of sink objects. The remove_ method for the PriceChange event is shown on lines 34 through 68. On line 39, I see the lock statement to make the method thread safe. On line 42, the code gets the number "sink" objects in the list of sink objects (m_aEventSinkHelpers). If the number of sink objects is greater than zero, the code loops through the items in the list until it finds the one that that is equal to the delegate that was passed to the method as a parameter (see line 50). The code then removes this sink object from the list calls the Unadvise method on the connection point, passing in the cookie from the sink object that it found. The code on lines 55 through 60 will release the connection point, and the list that holds the sink objects if the sink object that the remove_PriceChange just removed was the only one on the list. Again, it's not critical that you understand all of this logic. I included it in case you were interested in how the mapping between COM events and managed events works. Even though the implementation of the mapping is quite complicated, as you saw earlier, using the mapping is very simple.


Team-Fly    
Top
 


. Net and COM Interoperability Handbook
The .NET and COM Interoperability Handbook (Integrated .Net)
ISBN: 013046130X
EAN: 2147483647
Year: 2002
Pages: 119
Authors: Alan Gordon

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net