Microsoft included an example of using events in .NET Remoting in the help documentation at
ms-help://MS.VSCC/MS.MSDNVS/
cpguide/html/cpconremotingexampledelegatesevents.htm.
(You can open this help topic by browsing to the referenced link in Internet Explorer or the URL control of the Web toolbar
in the VS .NET IDE.) The example is a simple console-based, chat example that permits clients to communicate through a Singleton object. Rather than repeat that code here (and because I thought the example was fun), I include a WinForms-based version
that is slightly more advanced and a lot of fun to play with. Here is the basic idea.
Recall that we talked about Singleton remoted objects. When we create a Singleton MarshalByRefObject, every client will get a transparent proxy—think “super pointer”—to the exact same object on the server. By exposing an event each client can add an event handler to the event. Now mix in delegates. Delegates are multicast in .NET. This means that the event is sent to all handlers. Thus, if one client raises an event, every client that has a handler in that object's invocation list will receive an event message. Combined with a Singleton Remote object reference, each client will be adding a handler to one object's multicast delegate invocation list. Voilà! A simplified chat application.
Understanding Remote Event Behavior
We have thought of remoting so far as clients having transparent proxy reference to an object on the server. However, when we need to raise an event on the server, the server is actually calling back to the client; the client handler becomes a server, and the server becomes the client. Consequently the client has a reference to the server, and when the roles are reversed, the server needs a reference to the client. We can solve this predicament by sharing code between client and server.
Invoking Remote Events
The example is comprised of three projects all contained in the \Chapter 8\Events\Chat\Chat.sln file. The solution includes the Chat.vbproj client, ChatServer.vbproj, and the shared class library General. The ChatServer.exe is a console application that has a reference to General.dll and configures ChatServer.ChatMessage as a well-known Singleton object using an application configuration file. The Chat.exe server is a Windows Forms application (see Figure 8.3) that has a reference to General.dll. Each instance of Chat.exe requests a reference to the ChatMessage object created on the remote server. The server returns the same instance to every client that requests a ChatMessage object on the same channel from the same server. After the client gets the ChatMessage wrapper back, it assigns one of its event handlers to an event defined by the wrapper class. When any particular client sends a message to the server, a ChatMessage object raises an event and all clients get the event. As a result we can selectively echo the original message (or not) to the sender and notify each client of a message.
Figure 8.3. The simple instant messaging example.
The server class simply uses a configuration file to register a Singleton instance of a ChatMessage wrapper object. You can see the code for the server in \Chapter 8\Events\Server\Server.vb. The shared General.dll assembly (which contains the wrapper) and the client that sends and handles events provide the most interesting functionality. We will go over most of that code next.
Implementing the Shared Event Wrapper
The code containing the shared event wrapper class is defined in \Chapter 8\Events\General\Class1.vb. Class1.vb defines three classes and a delegate. Listing 8.12 contains all the code for Class1.vb; a synopsis of the code follows the listing.
Listing 8.12 The Shared Classes That Manage Events between Client and Server
1: Option Strict On 2: Option Explicit On 3: 4: Imports System 5: Imports System.Runtime.Remoting 6: Imports System.Runtime.Remoting.Channels 7: Imports System.Runtime.Remoting.Channels.Http 8: Imports System.Runtime.Remoting.Messaging 9: 10: Imports System.Collections 11: 12: <Serializable()>_ 13: Public Class ChatEventArgs 14: Inherits System.EventArgs 15: 16: Private FSender As String 17: Private FMessage As String 18: 19: Public Sub New() 20: MyBase.New() 21: End Sub 22: 23: Public Sub New(ByVal sender As String, _ 24: ByVal message As String) 25: MyClass.New() 26: FSender = sender 27: FMessage = message 28: End Sub 29: 30: Public ReadOnly Property Sender() As String 31: Get 32: Return FSender 33: End Get 34: End Property 35: 36: Public ReadOnly Property Message() As String 37: Get 38: Return FMessage 39: End Get 40: End Property 41: End Class 42: 43: Public Delegate Sub MessageEventHandler(ByVal Sender As Object, _ 44: ByVal e As ChatEventArgs) 45: 46: Public Class ChatMessage 47: Inherits MarshalByRefObject 48: 49: Public Event MessageEvent As MessageEventHandler 50: 51: Public Overrides Function InitializeLifetimeService() As Object 52: Return Nothing 53: End Function 54: 55: <OneWay()> _ 56: Public Sub Send(ByVal sender As String, _ 57: ByVal message As String) 58: 59: Console.WriteLine(New String("-"c, 80)) 60: Console.WriteLine("{0} said: {1}", sender, message) 61: Console.WriteLine(New String("-"c, 80)) 62: 63: RaiseEvent MessageEvent(Me, _ 64: New ChatEventArgs(sender, message)) 65: End Sub 66: 67: End Class 68: 69: 70: Public Class Client 71: Inherits MarshalByRefObject 72: 73: Private FChat As ChatMessage = Nothing 74: 75: Public Overrides Function InitializeLifetimeService() As Object 76: Return Nothing 77: End Function 78: 79: Public Sub New() 80: RemotingConfiguration.Configure("Chat.exe.config") 81: 82: FChat = New ChatMessage() 83: 84: AddHandler FChat.MessageEvent, _ 85: AddressOf Handler 86: End Sub 87: 88: Public Event MessageEvent As MessageEventHandler 89: 90: Public Sub Handler(ByVal sender As Object, _ 91: ByVal e As ChatEventArgs) 92: RaiseEvent MessageEvent(sender, e) 93: End Sub 94: 95: Public Sub Send(ByVal Sender As String, _ 96: ByVal Message As String) 97: FChat.Send(Sender, Message) 98: End Sub 99: 100: Public ReadOnly Property Chat() As ChatMessage 101: Get 102: Return FChat 103: End Get 104: End Property 105: 106: End Class
Lines 12 through 41 define a new type of event argument, ChatEventArgs. ChatEventArgs inherits from System.EventArgs and introduces two new members: Message and Sender. Message is the content of the message sent by a client, and Sender is a user name. ChatEventArgs is an example of an object that the client needs for information purposes only; hence it was designated as a by-value object.
Lines 43 and 44 define a new delegate named MessageEventHandler. Its signature accepts the new event argument ChatEventArgs.
Lines 46 through 67 define the by-reference object ChatMessage that is the Singleton object shared by all clients. Every client on the same channel and originating from the same server will be referring to the same instance of this class. The class itself is easy enough, but it demonstrates some old concepts and introduces some new ones. Line 47 indicates that ChatMessage is a by-reference type. Line 49 exposes a public event; this is how all clients attach their event handlers to the ChatMessage Singleton. Lines 51 through 53 override the MarshalByRefObject.InitializeLifetimeService method. InitializeLifetimeService can be overridden to change the lifetime of a Remote object. Return Nothing sets the lifetime to infinity. (Refer to the Managing a Remoted Object's Lifetime subsection later in this chapter for more information.) Lines 55 through 65 define the Send message. Clients use Send to broadcast messages. All Send does is raise MessageEvent. Note that Send is adorned with the OneWayAttribute, which causes the server to treat Send as a “fire and forget” method. Send doesn't care whether the recipients receive the message or not. This handles the case of a client dropping off without disconnecting its handler. (Send also displays trace information on the server application; see Figure 8.4.) That's all the ChatMessage class is: a class shared between client and server that wraps the message invocation.
Figure 8.4. Trace information being written to the server console.
Finally, we come to the Client class in lines 70 through 106. The Client class plays the role of the executable code that is remotable and shared between client and server. If you examine it closely you will see that it mirrors the ChatMessage class except that Client is responsible for allowing the server to call back into the client application. The Client class in General.dll plays the role of client-application-on-the-server when the roles between client and server are reversed. If we didn't have a remotable class shared between client and server, we would need to copy the client application into the directory of the server application. Remember that for clients to run code defined on a server, we need an interface or shared code in order to have something to assign the shared object to. When the roles between client and server are reversed—client becomes server during the callback—the server would need an interface or shared code to the client to talk back to it. Thus for the same reason that we share code between client and server, we also share code between server and client.
TIP
For a comprehensive discussion of event sinks and .NET Remoting, Ingo Rammer [2002] has written a whole book, Advanced .NET Remoting.
Listing 8.13 contains the Chat.exe.config file that describes the configuration information to the well-known object registered on the server and the back channel to the client used when the client calls the server back.
Listing 8.13 The Configuration File for the Client Application
1: <?xml version="1.0" encoding="utf-8" ?> 2: <configuration> 3: <system.runtime.remoting> 4: <application> 5: <channels> 6: <channel 7: ref="http" 8: port="0" 9: /> 10: </channels> 11: <client> 12: <wellknown 13: type="ChatServer.ChatMessage, General" 14: url="http://localhost:6007/ChatMessage.soap" 15: /> 16: </client> 17: </application> 18: </system.runtime.remoting> 19: 20: <appSettings> 21: <add key="user" value="Your Name Here!" /> 22: <add key="echo" value="true" /> 23: </appSettings> 24: </configuration>
The <channels> element describes the back channel used by server to client. By initializing the port attribute with 0 we allow the port to be dynamically selected. The <client> element registers the reference to the well-known ChatMessage class on the client. This allows us to create an instance of the ChatMessage class on the client using the New operator, getting a transparent proxy instance rather than the literal ChatMessage class also defined in the client. Without the <client> element we would need to use the Activator or we'd end up with a local instance of ChatMessage rather than the remote instance.
Finally, the <appSettings> element is used by the ConfigurationSettings.AppSettings shared property to externalize general, nonremoting configuration information.
Implementing the Client Application
The client application creates an instance of the Client class. Client represents the assembly shared by both client and server, allowing server to talk back to client. The client application (shown in Figure 8.3) actually registers its events with the Client class. Listing 8.14 provides the relevant code for the client application that responds to events raised by the remote ChatMessage object. (The Client.vb source contains about 400 lines of Windows Forms code not specifically related to remoting. Listing 8.14 contains only that code related to interaction with the remote object. For the complete listing, download \Chapter 8\Events\Client\Client.vb.)
Listing 8.14 An Excerpt from the Client Application Related to Remoting
1: Option Strict On 2: Option Explicit On 3: 4: Imports System 5: Imports System.Runtime.Remoting 6: Imports System.Runtime.Remoting.Channels 7: Imports System.Runtime.Remoting.Channels.Http 8: Imports Microsoft.VisualBasic 9: Imports System.Configuration 10: 11: Public Class Form1 12: Inherits System.Windows.Forms.Form 13: . . . 284: 285: Public Sub Handler(ByVal sender As Object, _ 286: ByVal e As ChatEventArgs) 287: 288: If (e.Sender <> User) Then 289: Received = GetSenderMessage(e.Sender, e.Message) + Received 290: ElseIf (Echo) Then 291: Received = GetSendeeMessage(e.Message) + Received 292: End If 293: 294: End Sub 295: 296: Private ChatClient As Client 297: 298: Private Sub Form1_Load(ByVal sender As Object, _ 299: ByVal e As System.EventArgs) Handles MyBase.Load 300: 301: Init() 302: 303: ChatClient = New Client() 304: 305: AddHandler ChatClient.MessageEvent, _ 306: AddressOf Handler 307: End Sub 308: 309: Private Sub Send() 310: If (ChatClient Is Nothing = False) Then 311: ChatClient.Send(User, Sent) 312: Sent = "" 313: End If 314: End Sub 315: 316: Private Sub Button1_Click(ByVal sender As System.Object, _ 317: ByVal e As System.EventArgs) Handles ButtonSend.Click, _ 318: MenuItemSend.Click 319: 320: Send() 321: 322: End Sub 323: 324: Private Sub Form1_Closed(ByVal sender As Object, _ 325: ByVal e As System.EventArgs) Handles MyBase.Closed 326: 327: If (ChatClient Is Nothing) Then Return 328: RemoveHandler ChatClient.MessageEvent, _ 329: AddressOf Handler 330: End Sub 331: . . . 344: 345: Private Sub Init() 346: User = ConfigurationSettings.AppSettings("user") 347: Echo = (ConfigurationSettings.AppSettings("echo") = "true") 348: End Sub 349: . . . 396: End Class
Listing 8.14 contains snippets from Client.vb. Parts that are basic to Windows Forms or programming in general were removed to shorten the listing. Lines 285 through 294 define an event handler named Handler. As you can see from the listing, this handler looks like any other event handler. Note that there are no special considerations made for remoting (although there should be; more on this in a moment).
Line 296 declares the shared instance of the Client object. Client is the remotable object that the server treats like a server when it needs to communicate back with us.
Lines 298 through 307 define the form's Load event handler. Load initializes the application settings (line 301), creates a new instance of the Client class, and associates the form's event handler with the Client class's event handler. Client is the actual object called back by ChatMessage.
Button1_Click in lines 316 through 322 calls a local Send method that invokes Client.Send. Form1_Closed (lines 324 through 330) removes the event handler. If for some reason this code isn't called, the server will try to call this instance of the client application for as long as the server is running. If we hadn't used the OneWayAttribute, removing the client application without removing the event would cause exceptions. Using the OneWayAttribute avoids the exceptions but could potentially send out tons of calls to dead clients. (An alternative is to skip using the OneWayAttribute on the server and remove delegates that cause an exception on the server.) The Init method (lines 345 through 348) demonstrates how to read configuration settings from an application .config file.
Remoting and Threads
.NET Remoting is easier than DCOM and other technologies, but writing distributed applications is still not a trivial exercise. Recall that reference to something missing from the event handler in Listing 8.14 in lines 285 through 294? What's missing is a discussion of threads.
When the event handler is called, it actually comes back on a different thread than the one that Windows Forms controls are in. Recall that in Chapter 6, Multithreading, I said that Windows Forms is not thread-safe. This means that it is not safe to interact with Windows Forms controls across threads. To resolve this predicament we need to perform a synchronous invocation to marshal the call to the event handler—a thread used by remoting—to the same thread the Windows Forms controls are on. In short, we need to add a delegate and call Form.Invoke to move the data out of the event handler onto the same thread that the form and its controls are on.