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