Home > Articles > Programming > Windows Programming

  • Print
  • + Share This
This chapter is from the book

This chapter is from the book

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.

08fig03.gifFigure 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.

08fig04.gifFigure 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.

  • + Share This
  • 🔖 Save To Your Account