Working With Events Over Remoting
By Scott Stewart
Published: 4/24/2003
Reader Level: Intermediate
Rated: 4.50 by 6 member(s).
Tell a Friend
Rate this Article
Printable Version
Discuss in the Forums

Edited By: Russ Nemhauser
Written By: Scott P. Stewart, location unknown!

Introduction

Subscribing to events of objects instantiated via remoting can be a tricky business. However, it is possible to build solid event publish/subscribe applications while using remoting simply by applying a few extra strategies then might not seem immediately obvious. Throughout the document, I will refer to the process that exposes the object for remoting as server and the process that instantiates an instance (local proxy) of a remote object as client. However, all of these processes may in deed take place on the same machine.

There are some general assumptions made in this document in order to support a particular design requirement. The requirement is that we are building an object that will be exposed over remoting for many different clients to utilize. The objects purpose is to asynchronously take requests to do work, and then raise an event when the work is done. Internally, the object will be maintaining a pool of threads to efficiently do a bunch of concurrent work. Clients may subscribe to the events of this server object, and when an event is raised we desire all of the subscribed clients to receive the event. Therefore, it is assumed that the remote object will be a server activated remote object that is registered for the well known service type of Singleton, and that the lifetime services are overridden to persist the object forever. The object can be configured for remoting by either explicitly executing the RegisterWellKnownServiceType method of the RemotingConfiguration namespace or implicitly via a call to the RemotingConfiguration.Configure method along with the respective configuration attributes.

The reason for the Singleton assumption is so we can persist our list of event subscribers. In other words, when an event is raised we desire all of the subscribed clients to receive the event. With a Singleton object, there will only be one instance of the server object created. Therefore, there will be one complete collection of clients subscribed to our event in this one object. We can also override the lifetime services of our Singleton to assure that it stays around forever and doesnt get garbage collected (if the server object gets garbage collected, we will loose our list of clients that have subscribed to the event)

If we were to instead create the server object as a well known service type of SingleCall, the design requirement described above would be problematic. When a SingleCall object is used, we may have multiple instances of the object created at any one time depending on the number of remote method calls taking place to our object. Each of these server side instances would need to know about all of the currently subscribed clients in order to effectively raise an event. There are ways around this by persisting a master copy of the client event subscriptions in a database or separate collection. However, we would need to write some additional code to provide for the persistence of this subscription list. The information in this document would still apply, but would need to be modified slightly to support the various implementation possibilities.

Item 1) The Client Must Provide A Communication Channel For The Event To Be Raised Over.

When working with remoting, we are familiar with establishing a channel and formatter for our remote objects to be exposed over (for example, TCP and binary, or HTTP and SOAP.) We generally then connect to these objects from our client process with a call to RemotingServices.Connect or Activator.GetObject, knowing what URL and/or TCP port the object is configured on. We generally dont think about exposing a channel from within our client, only connecting to a remote object via its exposed channel. However, when our remote server object attempts to raise an event (or invoke a callback) it needs to channel that event back to our clients proxy instance of the object. In a sense, the roles of client and server become switched; instead of the client calling upon the server, here we have the server calling upon the client. In order for this to occur, out client must expose a communication channel (IChannel). We are not actually configuring our client for remoting as a WellKnownServiceType, but are only establishing a communication channel for it to expose. This can be accomplished with a call to RemotingConfiguration.Configure from within our client code prior to instantiating and subscribing to our remote object events. The configuration information necessary for this is minimal. Here is a sample app.config that will allow a client process to register a channel for this callback:

<system.runtime.remoting>
    <application name="myAppName">
        <channels>
            <channel ref="tcp" port="0">
            </channel>
        </channels>
    </application>
</system.runtime.remoting>

If you dont register a channel in the client process, youll receive an exception when the server side remote object attempts to raise the event or invoke a callback. Some possible server side exceptions (depending on how the remote object creates and raises events) are:

  • System.Reflection.TargetInvocationException
  • System.Runtime.Remoting.RemotingException with additional information of: This remoting proxy has no channel sink which means either the server has no registered server channels that are listening, or this application has no suitable client channel to talk to the server.

Item 2) The Remote Object Process Requires A Reference To Each Class That Subscribe To The Event.

In order for a remote server object to wire up an event (or register a callback) with a client, the server object needs to know a little bit about the client. This requires the server object to have a reference to each client that will subscribe to its events. This seems like a huge limitation for remote events. Imagine creating a server object to be used over remoting from many different types of client objects, and being required to add a reference to each of these client objects in the server object. Additionally, we may want to instantiate remote objects from within a client executable, and have the executable handle the remote event. Unfortunately, we cant add an executable as a reference to our server object. So, having a client executable subscribe to remote objects events might seem impossible. However, there is a good way to get around this limitation with the use of a small event helper class, or event shim. The idea is to create a small class that has a basic purpose of subscribing to remote objects events and then raising its own event. At first, this might seem like we are only postponing our event problem by propagating it up one level, but were really not. Remember, the real problem is our server object needs a reference to the client that will be subscribing to its event. By placing a reference to this small event helper class in both the client and server assemblies, we circumvent the problem. The server can see the object that will be subscribing to its event (the event helper) so it will know how to wire up the delegate callback for the event, and because our client has a reference to this small helper object it is able to subscribe to events on it. Then by passing a reference of our remote object proxy on the client to the event helper class it all comes together. Here is an example of a remote object event helper class:

<Serializable ()> _
Public Class RemoteObjectEventHelper
    Inherits MarshalByRefObject

    Private mSource As myRemoteObjectType

    Public Event RemoteObjectEvent(ByVal source As Object, ByVal e As _
      EventArgs)

    Public Overrides Function InitializeLifetimeService() As Object
        Return Nothing
    End Function

    Public Sub New (ByRef Value As myRemoteObjectType)
        mSource = Value
        AddHandler mSource.somethingHappened, AddressOf _
          RemoteObjectHelper
    End Sub

    Public Sub RemoteObjectHelper(ByVal source As Object, ByVal e As _
      EventArgs)
        RaiseEvent RemoteObjectEvent (source, e)
    End Sub
End Class

Here we simply provide a public event and a constructor that accepts a reference to our remote object. We subscribe to an event of the remote object with a local event handler, and then all we do is raise our own public event and pass along the source and EventArgs from our remote objects event. Notice also that the event helper class is created with the <Serializable ()> attribute, it inherits from MarshalByRefObject , and it overrides InitializeLifetimeService with a return of nothing. Our helper class is going to be referenced by both our client and server code, so we need to make sure it can be serialized and that the instance we create stays in place forever when created. If we dont do this, we will have problems with the event helper class getting garbage collected. So, things might seem like they are working fine at first, but once the event helper gets garbage collected we would no longer receive the events from the remote object.

The client process can then instantiate this event helper class and register for its event in the following manor (assuming mRemoteObject is already instantiated on the client via RemotingServices.Connect or Activator.GetObject ) as :

Dim mRemoteObjectEventHelper As RemoteObjectEventHelper
mRemoteObjectEventHelper = New RemoteObjectEventHelper(mRemoteObject)
AddHandler mRemoteObjectEventHelper.RemoteObjectEvent, AddressOf _
  LocalRemoteObjectEventHandler

As you can see, we pass a reference of the locally instantiated remote object proxy (mRemoteObject) to the RemoteObjectEventHelper class in order for it to subscribe to the remote object events. We then add a local event handler to the public event of our helper class. Therefore, the helper class subscribes to the actual event of the remote object and we subscribe to the event of the helper class. When the helper class handles an event from the remote object, it grabs the event source and event arguments and uses these as the source and argument for the event that it raises. In order to really make this effective, we will want to create our own event argument class that inherits from EventArgs. This will enable us to pass along useful information from the remote object with the event.

If you choose not to create a small event helper class, you will need to add an actual reference of the client to the server side remote object. If not, youll receive a 'System.IO.FileNotFoundException on the server side when the event is raised.

Item 3) The Remote Object Does Not Explicitly Know When A Client Is No Longer Around.

This issue might not surface immediately, but will eventually when multiple clients instantiate a local proxy to a remote Singleton object. As soon as one of the clients go away, the server side remote object will begin throwing a System.Net.Sockets.SocketException every time the event is raised. In addition, any clients that subscribed to the event after the dead client will never receive the event. To get to the bottom of this, we need to understand a little about what is going on with events behind the scenes.

When we declare an event in our remote object class as:

Public Event somethingHappened(ByVal source As object, ByVal e As EventArgs)

the compiler is going to do a few things for us to treat our event as a multicast delegate type. This is because delegates are the mechanism .Net uses for callback implementation in general. This is the case even if we are only explicitly declaring an event and then calling RaiseEvent on it. When we do this, the compiler is creating a Public Sub Delegate somethingHappenedEventHandler(ByVal source as object, ByVal e as EventArgs) and an instance of this delegate called somethingHappenedEvent, all behind the scenes for us. You can see this if you examine the IL code (using Ildasm.exe) generated by your class. The compiler creates an instance of a multicast delegate called somethingHappenedEvent that will actually implement the callback to our subscribers when we call RaiseEvent. Basically, each client subscription to our event is added to this delegates invocation list. Then, when we call RaiseEvent, it is really this delegate instances invocation list that is getting invoked.

This is a pretty quick explanation of things, but is enough for us to see the root of the problem. When we call RaiseEvent, we are attempting to call Invoke on our invocation list of subscribers. Its the invoke method of our multicast delegate that fails when it attempts to invoke on a subscriber that is no longer around. When invoke is called on the now dead subscriber, an exception occurs because the channel that the event is subscribed over is no longer available. This exception prevents the rest of the invocation list from getting invoked. That is why clients that have subscribed to an event after a client that is now dead will never receive the event. According to the .Net framework SDK this behavior is by design: If an invoked method throws an exception, the method stops executing, the exception is passed back to the caller of the delegate, and remaining methods in the invocation list are not invoked. Catching the exception in the caller does not alter this behavior

Now that we know what the root of the problem is, and have a little insight to what is going on behind the scenes when we declare and raise an event, we can begin to write some code to get around the issue. The basic idea is that we 1) gain access to the multicast delegate instance that contains our event subscribers in its invocation list 2) loop through this invocation list and try to manually call invoke on each item 3) catch any exceptions from dead clients 4) remove dead client subscriptions from the invocation list 5) continue manually calling invoke on all the remaining items in invocation list.

Lets assume that we want to expose an event called somethingHappened from our object to be available over remoting. Generally, we would simply write something like this:

Public Event somethingHappened(ByVal source As object, ByVal e As EventArgs)

And then at some point in our code call:

RaiseEvent somethingHappened.

Here is a way that we can effectively do the same thing, but manually step through the events invocation list. (If you use RaiseEvent from remote objects on a regular basis, it makes sense to encapsulate the following code in a module or create a separate class that encapsulates this into a Remoting Safe RaiseEvent object. Well take the non-encapsulated step-by-step approach here for clarity.)

First we declare a public delegate outside of our class called:

Public Delegate Sub somethingHappenedEventHandler(ByVal source As _
  object, ByVal e As EventArgs)

Notice that I named the delegate type after my desired event name, but added the suffix of EventHandler to it.

Then inside our class, we declare our event like this:

Public Event somethingHappened As somethingHappenedEventHandler

The compiler would have done this behind the scenes for us anyways, but now we explicitly have access to the somethingHappenedEventHandler type within our code.

When it comes time to raise the event, instead of calling RaiseEvent somethingHappened, we are going to do the following:

Try
    Dim invocationList() As [Delegate] = _
      somethingHappenedEvent.GetInvocationList()
    Dim del As [Delegate]
    For Each del In invocationList
        Try
            del.Method.Invoke(del.Target, args)
        Catch deadClientEx As Exception
            SomethingHappenedEvent = _
              CType(System.Delegate.Remove(somethingHappenedEvent,_
              del), somethingHappenedEventHandler)
        End Try
    Next del
Catch nullRefEx As System.NullReferenceException
    If no objects have subscribed to this event yet, then
    the EventDelegate will by nothing and therefore won't
    have an invocation list.
End Try

Notice that we first try to get at our invocation list by calling GetInvocationList on an instance of a delegate type called somethingHappenedEvent. This is the instance of somethingHappenedEventHandler that is created when we declared our event: Public Event somethingHappened as somethingHappenedEventHandler.

Now that we have access to this invocation list, we attempt to loop through it and manually invoke each target subscriber. We do this by calling method.invoke on each item in the list (our event subscribers in this case), and pass to it the target of this item (our subscribed client) and a parameter array of our arguments. The arguments are just the signature of our declared Event (i.e. Dim args() As Object = {mySource, myEventArgs}) If we catch an exception when invoking our event (or callback), we catch this exception and then remove that target event subscription from our multicast delegates invocation list. We could just catch the exception and do nothing with it, and then loop onto the next item in the list. However, there is a performance hit when we wait for the invoke exception to be thrown each time. So, it makes sense to remove this client subscription from the list altogether, and not attempt to invoke it on subsequent raise event attempts. If the client should come back to life, it will need to re-subscribe to the event. The final exception we catch in the outside try/catch is when our somethingHappenedEvents invocation list is empty. This will happen when we try to raise an event, but have no clients subscribed to it yet.

It should be clear how this same mechanism could be applied not only to pure events, but also to any exposed delegate types that we want to make available for callback from our class. It should also be noted that with this model, we could create multiple events that all utilize the same event handler delegate type. In other words, we could create multiple events like this:

Public somethingHappened As somethingHappenedEventHandler
Public anotherThingHappened As somethingHappenedEventHandler
Public yetAnotherThingHappened As somethingHappenedEventHandler

In each case we would have access to a delegate of type somethingHappenedEventHandler that would be named somethingHappenedEvent, anotherThingHappenedEvent, and yetAnotherThingHappenedEvent respectively. We could then get the invocation list from each of these multicast delegates when we wanted to raise an event. This model is actually a preferred way to create multiple events within our class, even if were just calling RaiseEvent (instead of explicitly invoking the items of the delegate instance) and even if we are not registering our object over remoting. In this model, the compiler will create less code then it would if we were to declare each event independently. In other words, the compiler would need to create multiple versions of our delegate type xxxEventHandler, one for each event. With a few events this probably wont be too bad, but could begin to bloat up the code when there are a lot of events in the class.

Conclusion:

Remoting can play an important role in building scaleable distributed applications, and with a little extra effort, events can be part of these applications. Events and remoting together can give us the ability to do asynchronous work on different physical tiers. Not something we may need to do with every application every day, but an architecture that can be very effective and powerful in the right situation.



Marketplace
(Sponsored Links)
What are the green links?
   



 
Copyright © 2007 CMP Tech LLC |
Privacy Policy (4/10/06) | Your California Privacy Rights (4/10/06) | Terms of Service | Advertising Info | About Us | Help