To learn how to use the MSNP component, it helps to create a simple Messenger client that emulates some of the standard features found in the Windows Messenger application. As a prerequisite, you should understand how a basic Messenger interaction works, as described here:
You sign in to Messenger with a valid user name and get authenticated.
If desired, you retrieve your list of contacts and their statuses.
You start a session with one of your Messenger contacts. A session can be thought of as a separate chat window in the Messenger application. Before you can send messages to any user, either you (or the recipient) must start a session by opening a chat window. You can also create multiple sessions at once (although our simple example won't use this feature). Whenever a session is established, the contact list is updated.
You send and receive messages through the server switchboard.
At some later point, you end the session and sign out.
Figure 12-1 shows the client we'll create to demonstrate the MSNP component. It allows a user to log in, see other contacts, start a session, and send messages.
To create this client, start by creating a new Windows project. Add the reference to the msnp.dll and import the MSNP namespace if desired.
In order to send and receive messages with the MSNP component, you must create a class that implements the ISessionHandler interface. As part of this interface, you'll need to implement methods such as MessageReceived() and ErrorReceived(). These methods will be triggered by the MSNP component in response to messages received from the Messenger network. (A more typical way to implement this type of design is to use events. However, this approach is equivalent.)
The ISessionHandler interface allows you to receive messages. To send messages, you must create an instance of the MSNPHelper class. The MSNPHelper class allows you to retrieve contacts, sign in and sign out, and create sessions. Every session is handled by a separate instance of the Session class. You use the Session class to send messages. Figure 12-2 diagrams this interaction.
Public Class MessengerForm Inherits System.Windows.Forms.Form Implements MSNP.ISessionHandler
' The helper used to sign in and out and retrieve contacts. Private Helper As MSNP.MSNPHelper ' These variables track the current session as well as the related user. Private CurrentSessionUser As String Private CurrentSession As MSNP.Session
When the form loads, it signs in to a new Messenger session. The user e-mail address and password are hard-coded to facilitate testing, but you could easily add a login window. The IP address is retrieved for the dispatch server using the System.Net.Dns class.
Private Sub MessengerForm_Load(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles MyBase.Load ' Retrieve the IP address for the messenger server. Dim IP As String IP = System.Net.Dns.GetHostByName( _ "messenger.hotmail.com").AddressList(0).ToString() ' For simplicity's sake, a test user is hard-coded. ' Note that that communication is always performed on port 1863. Helper = New MSNP.MSNPHelper(IP, 1863, "firstname.lastname@example.org", _ "letmein", Me) ' SignIn with the supplied information. ' This method blocks until the sign operation is complete. ' An invalid user or password may simply stall the application without ' generating an error, so you may want to execute this method asynchronously. Helper.Signin() Me.RefreshContactList() End Sub
Although the MSNPHelper requires that you supply the password in clear text, this password is never transmitted over the network. Instead, the password is hashed using the MD5 hashing algorithm and a value supplied by the server. For more information, refer to the detailed description of the underlying protocol at http://www.hypothetic.org/docs/msn/connecting.php.
When you create the MSNPHelper you supply the login information, the IP address and port to use, and an ISessionHandler object. In this example, the current form implements the ISessionHandler, so we pass that as a reference.
The next step is to call the form-level RefreshContactList() subroutine, which retrieves contact information and uses it to fill a ListView control:
Private Sub RefreshContactList() ' Fill the contact list. Dim Item As ListViewItem Dim Peer As MSNP.Contact For Each Peer In Me.Helper.FLContacts Item = lstContacts.Items.Add(Peer.FriendlyName) Item.SubItems.Add(Peer.State.ToString()) Item.SubItems.Add(Peer.Substate.ToString()) Item.SubItems.Add(Peer.UserName) Next End Sub
This method is also called by the ISessionHandler UserJoined() and UserDeparted() methods. However, in this case the method won't execute on the main application thread, so the call must be marshaled using the Control.Invoke() method.
Public Sub UserDeparted(ByVal session As MSNP.Session, _ ByVal userHandle As String) Implements MSNP.ISessionHandler.UserDeparted ' Refresh the contact list. Dim Invoker As New MethodInvoker(AddressOf Me.RefreshContactList) Me.Invoke(Invoker) End Sub Public Sub UserJoined(ByVal session As MSNP.Session, _ ByVal userHandle As String, ByVal userFriendlyName As String) _ Implements MSNP.ISessionHandler.UserJoined ' Refresh the contact list. Dim Invoker As New MethodInvoker(AddressOf Me.RefreshContactList) Me.Invoke(Invoker) End Sub
Note that if the user's friendly name is different from his or her e-mail address, multiple entries may appear for the user in the contact list (you may have also noticed this phenomenon if you use the Microsoft Outlook Express Hotmail integration). You can use additional code to ignore entries with duplicate UserName values.
Nothing else happens until a user starts a session, or a session is started when another user sends a message. The user can start a session by selecting a user in the contact list and clicking the Create Session button. The button event handler uses the MSNPHelper.RequestSession() method, which returns immediately. The MSNP component will continue trying to establish the session for a maximum of about 30 seconds.
Private Sub cmdStartSession_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles cmdStartSession.Click If Not Me.CurrentSession Is Nothing Then MessageBox.Show("There is already a current session.") Return Else If lstContacts.SelectedIndices.Count = 0 Then MessageBox.Show("No user is selected.") Return Else Dim Contact As String Contact = lstContacts.Items( _ lstContacts.SelectedIndices(0)).SubItems(3).Text Helper.RequestSession(Contact, Guid.NewGuid()) End If End If End Sub
Note that every session requires an identifier that's generated by the client and is unique within the application. Our custom client simply creates a new GUID.
If the session is successfully established, the ISessionHandler.Session Started() method will be triggered. In our example, the method handler simply updates the form with the retrieved session ID and stores the session object in a member variable for use when sending messages later on. In addition, the ISessionHandler.SessionEnded() method removes these details.
Public Sub SessionStarted(ByVal session As MSNP.Session) _ Implements MSNP.ISessionHandler.SessionStarted Dim Updater As New UpdateControlText(lblSession) Updater.ReplaceText(session.SessionIdentifier.ToString()) Me.CurrentSession = session End Sub Public Sub SessionEnded(ByVal session As MSNP.Session) _ Implements MSNP.ISessionHandler.SessionEnded ' Don't try to update the form if it's in the process of closing. If Not IsClosing Then Dim Updater As New UpdateControlText(lblSession) Updater.ReplaceText("") End If Me.CurrentSession = Nothing End Sub
This code uses the UpdateControlText class, which can update the Text property of any control on the correct thread. This useful class is shown here:
Public Class UpdateControlText Private NewText As String Private ControlToUpdate As Control Public Sub New(ByVal controlToUpdate As Control) Me.ControlToUpdate = controlToUpdate End Sub Public Sub AddText(ByVal newText As String) SyncLock Me Me.NewText = newText Dim Invoker As New MethodInvoker(AddressOf AddText) Me.ControlToUpdate.Invoke(Invoker) End SyncLock End Sub ' This method executes on the user-interface thread. Private Sub AddText() Me.ControlToUpdate.Text &= NewText End Sub Public Sub ReplaceText(ByVal newText As String) SyncLock Me Me.NewText = newText Dim Invoker As New MethodInvoker(AddressOf ReplaceText) Me.ControlToUpdate.Invoke(Invoker) End SyncLock End Sub ' This method executes on the user-interface thread. Private Sub ReplaceText() Me.ControlToUpdate.Text = NewText End Sub End Class
Now that a session is established, the client can send messages by clicking the Send button. The button event handler checks that there's a current session and uses the Session.SendMessage() method.
Private Sub cmdSend_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles cmdSend.Click If Me.CurrentSession Is Nothing Then MessageBox.Show("There is no current session.") Return Else Me.CurrentSession.SendMessage(txtSend.Text) Dim NewText As String NewText = "SENT: " & txtSend.Text NewText &= Environment.NewLine & Environment.NewLine txtMessages.Text &= NewText End If End Sub
Messages are received through the ISessionHandler.MessageReceived() method. Blank messages are ignored, because they're used to indicate that the user has started typing, thereby allowing you to display the "User is typing a message" status message in your application.
Public Sub MessageReceived(ByVal session As MSNP.Session, _ ByVal message As MSNP.MimeMessage) _ Implements MSNP.ISessionHandler.MessageReceived ' Add text. If message.Body <> "" Then Dim Updater As New UpdateControlText(txtMessages) Dim NewText As String NewText = "FROM: " & message.SenderFriendlyName NewText &= Environment.NewLine NewText &= "RECEIVED: " & message.Body NewText &= Environment.NewLine & Environment.NewLine Updater.AddText(NewText) End If End Sub
Finally, when the form closes, it signs the user out of Windows Messenger.
Private Sub MessengerForm_Closed(ByVal sender As Object, _ ByVal e As System.EventArgs) Handles MyBase.Closed If Not Me.CurrentSession Is Nothing Then Me.CurrentSession.EndSession() End If Helper.Signout() End Sub
Figure 12-3 shows the interaction of two Windows Messenger peers, one of which uses the custom client.