Handling Managed Events from a COM Client

Team-Fly    

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

Handling Managed Events from a COM Client

In Chapter 7, you saw how easy it is to handle COM connection point events from a managed client. Therefore, you are probably thinking that it is just as easy to handle events from a managed object in a COM client, right? Well, unfortunately , you will be disappointed. By default, events in a .NET object are not exposed to unmanaged clients at all. You must perform a few extra steps in order to expose your managed events to a COM client. These steps are quite easy, but, even after you perform these steps, you still may encounter some problems. This section shows you how to enable COM clients to handle events fired from your managed components and how to work around some of the problems this may cause.

Exposing Managed Events to Unmanaged Clients

A COM object that supports Connection Point events must publish a source (outgoing) interface in its type library. Although this interface appears in the COM object's type library, it is actually the object's client's responsibility to implement it. The client passes its implementation of the source interface to the object using the connection point interfaces (IConnectionPointContainer and IConnectionPoint), and then the COM object calls back on the source interface when it wants to fire the event.

Managed code objects that support .NET events do not fit into the connection point mechanism becausefor a startthey do not expose a source interface. In order to support COM-style events, a managed code class needs to do three things:

  • Expose a source interface that contains a method for each event that the object supports.

  • Provide a mechanism to advertise that the class supports event notifications through the source interface.

  • Implement the IConnectionPointContainer and IConnectionPoint interfaces to allow a client to pass its implementation of the source interface to instances of the class.

Fortunately, the System.Runtime.InteropServices namespace provides a pair of attributes that allow you to do all of this. If you want to expose .NET events to COM clients, you need to perform the following steps:

  1. Define an interface, with members that correspond to each of the events that the managed class exports.

  2. Mark the interface as a Dispatch interface using the Interface System.Runtime.InteropServices.InterfaceType attribute, specifying the following: ComInterfaceType.InterfaceIsIDispatch.

  3. Mark the managed class that uses the source interface to fire events with the System.Runtime.InteropServices.ComSourceInterfaces attribute.

An example should make this clear. In Chapter 7, I demonstrated a COM StockMonitor object that fired PriceChange events to a managed client. These events showed the changing price on a number of stocks. In this chapter, I demonstrate the opposite scenario: a managed stock server object firing events to a COM client. The code for the managed stock server follows :

 1.  using System; 2.  using System.Collections; 3.  using System.Timers; 4.  namespace DotNetStockServer 5.  { 6.      public class PriceChangeEventArgs : EventArgs 7.      { 8.        private string mTicker; 9.        private float mOldPrice; 10.       private float mNewPrice; 11.       public PriceChangeEventArgs(string ticker, 12.         float newPrice,float oldPrice) 13.       { 14.         mTicker=ticker; 15.         mOldPrice=oldPrice; 16.         mNewPrice=newPrice; 17.       } 18.       public string Ticker 19.       { 20.         get 21.         { 22.           return mTicker; 23.         } 24.       } 25.       public float OldPrice 26.       { 27.         get 28.         { 29.           return mOldPrice; 30.         } 31.       } 32.       public float NewPrice 33.       { 34.         get 35.         { 36.           return mNewPrice; 37.         } 38.       } 39.     } 40.     public class MonitorInitiatedEventArgs : 41.       System.EventArgs 42.     { 43. 44.       private string mTicker; 45.       private float mPrice; 46.       public MonitorInitiatedEventArgs(47.         string ticker,float price) 48.       { 49.         mTicker=ticker; 50.         mPrice=price; 51.       } 52.       public float Price 53.       { 54.         get 55.         { 56.           return mPrice; 57.         } 58.       } 59.     } 60.     public delegate void PriceChangeEventHandler(61.       object sender,PriceChangeEventArgs e); 62.     public delegate void MonitorInitiatedEventHandler 63.         (object sender,MonitorInitiatedEventArgs e); 64.     public class StockMonitor 65.     { 66.     private Hashtable mStockList; 67.     private ArrayList mTickerList; 68.     private Timer mTimer; 69.     public event PriceChangeEventHandler 70.       PriceChange; 71.     public event MonitorInitiatedEventHandler 72.       MonitorInitiated; 73.     public StockMonitor() 74.     { 75.       mStockList=new Hashtable(25); 76.       mTickerList=new ArrayList(25); 77.       AddNewStock("EMLX",(float)90.0); 78.       AddNewStock("IBM",(float)30.0); 79.       AddNewStock("MSFT",(float)49.0); 80.       mTimer=new Timer(5000); 81.       mTimer.Elapsed+=new 82.         ElapsedEventHandler(83.         timer_Elapsed); 84.       mTimer.Enabled=true; 85.     } 86.     public void AddNewStock(87.       string ticker,float price) 88.     { 89.       mTickerList.Add(ticker); 90.       mStockList.Add(ticker,price); 91.       OnMonitorInitiated(new 92.       MonitorInitiatedEventArgs(93.         ticker,price)); 94.     } 95.     public float GetPrice(string ticker) 96.     { 97.       return (float) mStockList[ticker]; 98.     } 99.     private void timer_Elapsed(object sender, 100.        System.Timers.ElapsedEventArgs e) 101.      { 102. 103.          if (mStockList.Count > 0) 104.          { 105.          Random randomizer=new Random(); 106.          int index; 107.          float oldPrice, newPrice; 108.          String ticker; 109.          index=randomizer.Next(110.          mStockList.Count); 111.          ticker=(string)mTickerList[index]; 112.          oldPrice=(float)mStockList[ticker]; 113.          newPrice=oldPrice+(float)0.1; 114.          mStockList[ticker]=newPrice; 115.          OnPriceChange(116.          new PriceChangeEventArgs(117.          ticker,newPrice,oldPrice)); 118.          } 119.      } 120.      protected void OnPriceChange(121.        PriceChangeEventArgs e) 122.      { 123.        PriceChange(this,e); 124.      } 125.      protected void OnMonitorInitiated(126.        MonitorInitiatedEventArgs e) 127.      { 128.        MonitorInitiated(this,e); 129.      } 130.    } 131. } 

The main class here is called StockMonitor , and it has two methods : (1) GetPrice, which takes a stock ticker as a parameter and returns the current price of the stock, and (2) AddNewStock, which allows you to specify the ticker and current price of a stock, which should be monitored. The StockMonitor class supports two events: (1) a MonitorInitiated event, which is fired when a client calls AddNewStock, and (2) PriceChanged, which is fired whenever the price of a monitored stock changes. In reality, the price changes are actually implemented by a timer (using the Systems.Timers.Timer class), which fires an event every 5 seconds and randomly picks a stock, raises its price by 10 cents , and then fires the PriceChange event. I won't walk through every line of code of this example, and I won't discuss in depth how to create a Visual Studio .NET project to build this assembly. The code is simple, and you've built projects similar to this many times already in this book.

Note

I added a strong name to the DotNetStockServer assembly because you will install it into the GAC later.


I will quickly run through the event- related code in this class. Before I do that, however, I would like to quickly discuss the recommended design pattern for events as published in the .NET Framework SDK. That way you will understand why I structured this managed event as I did.

THE RECOMMENDED DESIGN PATTERN FOR EVENTS

If you want to expose an event called EventName, the .NET design guidelines for events recommends that you do the following:

"Declare an 'argument' class for the event, which is just a class called EventNameEventArgs. This class should derive from the System.Event-Args class and it should contain a property for each argument of the event.

Declare a delegate for the event called EventName EventHandler. This delegate should return void and have 2 parameters. The first parameter should be a System.Object that contains the object that fired the event; this argument should be called "sender". The second parameter is an instance of the "argument" class defined above; this parameter should be called "e". The sender should initialize this instance with the argument values that it is passing to the consumer of the event.

Declare the event as a public member of the "event firing" class and give it the name: EventName. Use the EventNameEventHandler delegate declared above for the event.

Add a protected method to the "event firing" class called OnEventName and fire the event from there. The reason for this is so derived classes can override the event behavior of the "event firing" class. By default, derived classes cannot directly raise events in their base classes."

Now, let's return the discussion to the StockMonitor class. In this example, the StockMonitor class is the event-firing class. Let's look at all the elements that make up the PriceChange event. If you refer back to the previous code listing, notice that you declared a PriceChangeEventArgs class on lines 6 through 39. This class has three read-only properties: Ticker, which contains the ticker name of the stock; old price, which contains the old price of the stock prior to the price change; and new price, which contains the price of the stock after the price change. I made these properties read-only because the event-firing class will use the parameterized constructor to initialize the underlying data for the class and the consumer of the event only needs to read the properties. Lines 60 and 61 declare the PriceChangeEventHandler delegate. Notice that the second parameter to this delegate is an instance of the PriceChangeEventArgs class. Lines 120 through 124 declare the protected OnPriceChange method, which raises the event to the client. The OnPriceChange method is called from the timer_Elapsed method on lines 115 through 117. The timer_Elapsed method is called every 5 seconds by the StockMonitor class' internal timer.

As it stands right now, a COM client cannot receive events from the StockMonitor class. Let's take a look at the changes you must make to allow a COM client to receive these events. First, you need to define a source interface that contains matching methods for each event. Let's add an interface called IStockMonitorEvents as follows:

 [InterfaceType(ComInterfaceType.InterfaceIsIDispatch)] public interface IStockMonitorEvents {     [DispId(1)] void PriceChange(object sender,PriceChangeEventArgs e);     [DispId(2)] void MonitorInitiated(object sender,MonitorInitiatedEventArgs e); } 

The names and signatures of the methods in this interface must match up exactly with both the event names and the method signature of the managed classes' event delegates. Aside from that, this interface has nothing unusual or event specific. I declared it to be an IDispatch-only interface because that will make it accessible to the largest number of connection-point event consumers. I also explicitly declared DispIDs for the methods because that saves the CLR from having to call GetIDsOfNames to fetch the DispID for the method. I'll add the IStockMonitorEvents interface outside the declaration of the StockMonitor class, but inside the DotNetStockServer namespace. I'll also add a using Statement for System.Runtime.InteropServices as follows because the InterfaceType and DispId attributes reside in this namespace:

 using System.Runtime.InteropServices; 

Next, add the following attribute to the StockMonitor class:

 [ComSourceInterfaces(typeof(IStockMonitorEvents))] public class StockMonitor { // Implementation omitted... } 

This attribute, when applied to the StockMonitor class, indicates that the class uses the IStockMonitorEvents interface to fire connection point events to unmanaged clients. Finally, execute the following command to register the .NET StockMonitor for use by a COM client:

 regasm DotNetStockServer.dll /tlb 

Let's also execute the following commend to insert the stock server assembly into the GAC:

 gacutil i DotNetStockServer.dll 

This last step isn't strictly necessary, but, if you don't do it, you will need to remember to deploy the assembly in the same directory as its unmanaged client.

Next, I built a client for the DotNetStockServer using Visual Basic 6. I first referenced the DotNetServer using the Project References as shown in Figure 8-13.

Figure 8-13. Referencing the DotNetStockServer from a Visual Basic 6 client.

graphics/08fig13.jpg

Notice, that, if I declare the StockMonitor object using the WithEvents keyword as shown here

 Private WithEvents mStockServer As StockMonitor 

I can now view the two events for the StockMonitor class in the events dropdown list in Visual Basic as shown in Figure 8-14.

Figure 8-14. The MonitorInitiated and PriceChange events viewed from Visual Basic 6.

graphics/08fig14.jpg

The UI for my Visual Basic client looks as shown in Figure 8-15.

Figure 8-15. The UI for the Visual Basic 6 stock monitor client.

graphics/08fig15.jpg

The code for this client is as follows:

 Option Explicit Private WithEvents mStockServer As StockMonitor Private Sub cmdAdd_Click()     On Error GoTo errHandler     Call mStockServer.AddNewStock(txtTicker.Text, txtPrice.Text)     Exit Sub errHandler:     MsgBox (Err.Description) End Sub Private Sub Form_Load()     On Error GoTo errHandler     Set mStockServer = New StockMonitor     Exit Sub errHandler:     MsgBox (Err.Description) End Sub Private Sub Form_Unload(Cancel As Integer)     Set mStockServer = Nothing End Sub Private Sub mStockServer_MonitorInitiated(ByVal sender As Variant, ByVal e As DotNetStockServer.MonitorInitiatedEventArgs)     Dim strMessage As String     strMessage = "Monitor initiated for " & _         e.ticker & vbCrLf & _           "Current price $" & CStr(e.Price)     Call MsgBox(strMessage, vbInformation,         "Monitor Initiated") End Sub Private Sub mStockServer_PriceChange(ByVal sender As Variant, ByVal e As DotNetStockServer.PriceChangeEventArgs)     Dim strMessage As String     strMessage = "Stock: " & e.ticker & _         "  New Price: $" & CStr(e.newPrice) & _         "  Old Price: $" & CStr(e.oldPrice)     lstEvents.AddItem strMessage End Sub 

I won't go line by line through this code because there really is nothing .NETspecific about it. It looks exactly like any code that you would write to use a COM object that supports connection point events.


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