The final step is to modify the Talk .NET peer application to use the discovery service instead of the well-known Remoting server. Thanks to the well-encapsulated design of the Talk .NET client, you won't need to modify the main form code. Instead, almost all of the changes are confined to the remotable ClientProcess class.
The ClientProcess class is used to send and receive messages with .NET Remoting. In the revised version, it will also have the additional responsibility of interacting with the discovery web service. To support this design, we need to add two member variables, as shown here:
Public Class ClientProcess Inherits MarshalByRefObject Implements ITalkClient ' Holds a reference to the web-server proxy. Private DiscoveryService As New localhost.DiscoveryService() ' Tracks the GUID for the current session. Private SessionID As Guid ' (Other code omitted.) End Class
The ClientProcess constructor accepts a Boolean parameter that indicates whether a new record needs to be created for this user. If the user hasn't registered before, the ClientProcess class calls the RegisterNewUser() web method.
Public Sub New(ByVal userEmailAddress As String, ByVal createUser As Boolean) Me.[Alias] = userEmailAddress If createUser Then DiscoveryService.RegisterNewUser(userEmailAddress) End If End Sub
The Login() method registers ClientProcess to receive messages from other peers. It also retrieves the ObjRef for the current instance using the Remoting Services.Marshal() method, and submits it to the sever.
Public Sub Login() ' Configure the client channel for sending messages and receiving ' the server callback. RemotingConfiguration.Configure("TalkClient.exe.config") ' Retrieve the ObjRef for this class. Dim Obj As ObjRef = RemotingServices.Marshal(Me) ' Serialize the ObjRef to a memory stream. Dim ObjStream As New MemoryStream() Dim f As New BinaryFormatter() f.Serialize(ObjStream, Obj) ' Start a new session and record the session GUID. Me.SessionID = DiscoveryService.StartSession(ObjStream.ToArray()) End Sub
The GetUsers() method now calls the discovery web service to retrieve the list of peer e-mail addresses:
Public Function GetUsers() As ICollection Return DiscoveryService.GetPeers(Me.SessionID) End Function
The SendMessage() method calls the discovery service to retrieve the appropriate ObjRef, deserializes it, converts it to a proxy, and then invokes the ITalkClient.ReceiveMessage() method.
Public Sub SendMessage(ByVal emailAddress As String, ByVal messageBody As String) ' Retrieve the peer information. Dim PeerInfo As localhost.PeerInfo PeerInfo = DiscoveryService.GetPeerInfo(emailAddress) ' Deserialize the proxy. Dim ObjStream As New MemoryStream(PeerInfo.ObjRef) Dim f As New BinaryFormatter() Dim Obj As Object = f.Deserialize(ObjStream) Dim Peer As ITalkClient = CType(Obj, ITalkClient) ' Send the message to this peer. Try Peer.ReceiveMessage(messageBody, Me.Alias) Catch ' Ignore connectivity errors. ' Alternatively, you could raise an event or throw an error that the main ' form could respond to and use to update the form display. End Try End Sub
The LogOut() method ends the session:
Public Sub LogOut() DiscoveryService.EndSession(Me.SessionID) End Sub
Finally, the Login window is modified to include a check box that the user can select to create the account for the first time, as shown in Figure 10-2.
The startup code can retrieve the user's check box selection from the readonly CreateNew property:
Public ReadOnly Property CreateNew() As Boolean Get Return chkCreateNew.Checked End Get End Property
Dim Client As New ClientProcess(frmLogin.UserName, frmLogin.CreateNew)
The new Talk .NET client is now fully functional. The next two sections describe some enhancements you can implement.
Currently, the Talk .NET client contacts the discovery service every time it sends a message. You could improve upon this situation by increasing the amount of information the client keeps locally. For example, the client might keep a cache with peer-connectivity information in it. That way, if one user sends several messages to another, it will only need to contact the server once, when the first message is sent.
To add caching, you must first add a Hashtable collection to the ClientProcess class. This collection will store all the PeerInfo objects for recently contacted clients, indexed by the e-mail address.
' Contains all recently contacted clients. Private RecentClients As New Hashtable()
Whenever a message is sent, the code will check the RecentClients collection. If it finds the corresponding user, it will use the stored ObjRef. Otherwise, it will retrieve the ObjRef from the server and add it to the hashtable.
Public Sub SendMessage(ByVal emailAddress As String, ByVal messageBody As String) Dim PeerInfo As localhost.PeerInfo ' Check if the peer-connectivity information is cached. If RecentClients.Contains(emailAddress) Then PeerInfo = CType(RecentClients(emailAddress), localhost.PeerInfo) Else PeerInfo = DiscoveryService.GetPeerInfo(emailAddress, Me.SessionID) RecentClients.Add(PeerInfo.EmailAddress, PeerInfo) End If ' Deserialize the proxy. Dim ObjStream As New MemoryStream(PeerInfo.ObjRef) Dim f As New BinaryFormatter() Dim Obj As Object = f.Deserialize(ObjStream) Dim Peer As ITalkClient = CType(Obj, ITalkClient) ' Send the message to this peer. Try Peer.ReceiveMessage(messageBody, Me.Alias) Catch RecentClients.Remove(PeerInfo) ' Optionally, you might want to try retrieving new peer information ' and resending the message, if you used the connectivity information ' in the local cache. End Try End Sub
As implemented, this will retain ObjRef for the life of the application, or until a transmission error occurs. If you anticipate that connectivity information will change frequently, or that the Talk .NET client application will run for an extremely long period of time (for example, several days), you might want to take a few additional measures to help ensure that this information is valid. For example, you could use code in the GetUsers() method to check the currently logged-on users and remove an ObjRef as soon as a peer disappears from the network:
Public Function GetUsers() As ICollection Dim Peers() As String Peers = DiscoveryService.GetPeers() ' Identify any peers in the local cache that aren't online. Dim PeerSearch As New ArrayList() PeerSearch.AddRange(Peers) Dim PeersToDelete As New ArrayList() Dim Item As DictionaryItem Dim Peer As localhost.PeerInfo For Each Item In Me.RecentClients Peer = CType(Item.Value, localhost.PeerInfo) ' Check if this e-mail address is in the server list. If Not PeerSearch.Contains(Peer.EmailAddress) Then ' The e-mail address wasn't found. Mark this peer for deletion. PeersToDelete.Add(Peer) End If Next ' Remove the peers that weren't found. For Each Peer In PeersToDelete Me.RecentClients.Remove(Peer.EmailAddress) Next Return Peers End Function
This code works in two steps because items cannot be removed from a collection while you're iterating through it, without causing an error.
Currently, no validation is performed when a user registers with the server. This is simply intended as a convenience for testing purposes. Ideally, you would not create a new user account until you could confirm that the e-mail address is correct.
To validate an e-mail address, you can borrow a technique from the world of e-commerce. It works like this:
When the user makes a request, save the submitted information into a different table (for example, a NewUserRequests table). Create a new GUID to identify the request.
Next, send an e-mail to the user-supplied e-mail address (you can use the System.Web.Mail.SmtpServer class for this task). Here's the trick: This e-mail can include an HTTP GET link to a web-service method (or ASP.NET web page) that confirms the new user account. This link will submit the request GUID through the query string. For example, the link might take this form: http://www.mysite.com/RegisterUser.asmx?requestGuid=382c74c3-721d-4f34-80e5-57657b6cbc27 (assuming "requestGuid" is the name of the web-method parameter).
When the user receives the message and clicks on the link, the confirmation method will run with the identifying GUID.
The confirmation method will first check that the response has been received within a reasonable amount of time (for example, three days). If so, it can then find the request record with the matching GUID, remove it from the database, and add the user information to the Users table.