Let's now look at the codebehind for MainPage in Listing 3.
Listing 3. Codebehind for the MainPage in MainPage.xaml.cs
using System.Windows;
using System.Windows.Controls;
namespace Recipe7_5.ChatClient
{
public partial class MainPage : UserControl
{
public ClientConnectionManager ConnManager { get; set; }
public MainPage()
{
InitializeComponent();
//initialize the ClientConnectionManager
ConnManager = new ClientConnectionManager { ParentPage = this };
//set the data context to the ClientConnetionManager
LayoutRoot.DataContext = ConnManager;
}
private void btnJoin_Click(object sender, RoutedEventArgs e)
{
ConnManager.Join();
}
private void btnLogoff_Click(object sender, RoutedEventArgs e)
{
ConnManager.Disconnect();
}
private void btnTalk_Click(object sender, RoutedEventArgs e)
{
//get the participant name from the Button.Tag
//which was bound to the name at data binding
ConnManager.TalkingTo = (sender as Button).Tag as string;
ShowChatView();
}
private void btnSend_Click(object sender, RoutedEventArgs e)
{
ConnManager.SendTextMessage();
}
private void btnEndChat_Click(object sender, RoutedEventArgs e)
{
ConnManager.SendChatEnd();
}
internal void ShowParticipantsView()
{
viewParticipants.Visibility = Visibility.Visible;
viewLogin.Visibility = Visibility.Collapsed;
viewChat.Visibility = Visibility.Collapsed;
}
internal void ShowChatView()
{
viewParticipants.Visibility = Visibility.Collapsed;
viewLogin.Visibility = Visibility.Collapsed;
viewChat.Visibility = Visibility.Visible;
}
internal void ShowLoginView()
{
viewParticipants.Visibility = Visibility.Collapsed;
viewLogin.Visibility = Visibility.Visible;
viewChat.Visibility = Visibility.Collapsed;
}
}
}
|
The MainPage constructor creates a new instance of the ClientConnectionManager named ConnManager, initializing its ParentPage property with this Page instance. This is done so that in the ClientConnectionManager implementation, you have access to MainPage and its UI elements to effect various state changes. You also set the DataContext of the topmost Grid named LayoutRoot to ConnManager so that all the bindings to various properties of ClientConnectionManager that you saw in the XAML can be put into effect.
The various Button click handlers are self-explanatory; corresponding functions in the ClientConnectionManager are invoked from them. The ShowLoginView(), ShowParticipantsView(), and ShowChatView() methods toggle between views and are used from within the ClientConnectionManager, as you see next.
We have encapsulated all the client-side sockets-based communication and message processing in ClientConnectionManager. Listing 4 shows the ClientConnectionManager class.
Listing 4. ClientConnectionManager in ConnectionManager.cs
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.Net;
using System.Net.Sockets;
namespace Recipe7_5.ChatClient
{
public class ClientConnectionManager : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
//create a new socket
Socket ClientSocket = new Socket(AddressFamily.InterNetwork,
SocketType.Stream, ProtocolType.Tcp);
//reference to the parent page
public Page ParentPage { get; set; }
//participants collection
private ObservableCollection<string> _Participants;
public ObservableCollection<string> Participants
{
get { return _Participants; }
set
{
_Participants = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("Participants"));
}
}
//collection of all messages exchanged in a particular conversation
private ObservableCollection<TextMessage> _Conversation;
public ObservableCollection<TextMessage> Conversation
{
get { return _Conversation; }
set
{
_Conversation = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("Conversation"));
}
}
//IP Address of the server connected to
private string _IP;
public string IP
{
get { return _IP; }
set
{
_IP = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("IP"));
}
}
//Port connected to
private string _Port;
public string Port
{
get { return _Port; }
set
{
_Port = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("Port"));
}
}
//name of the person logged in
private string _Me;
public string Me
{
get { return _Me; }
set
{
_Me = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("Me"));
}
}
//the other person in a conversation
private string _TalkingTo;
public string TalkingTo
{
get { return _TalkingTo; }
set
{
_TalkingTo = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("TalkingTo"));
}
}
//the body of a conversation message
private string _MessageBody;
public string MessageBody
{
get { return _MessageBody; }
set
{
_MessageBody = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("MessageBody"));
}
}
//buffer used to receive messages
private const int RECEIVEBUFFERSIZE = 10 * 1024;
private byte[] ReceiveBuffer = new Byte[RECEIVEBUFFERSIZE];
//constructor
public ClientConnectionManager()
{
//initialize the collections
Participants = new ObservableCollection<string>();
Conversation = new ObservableCollection<TextMessage>();
}
//called when the login button is clicked
public void Join()
{
//create a new SocketEventArgs, specify the remote endpoint details
SocketAsyncEventArgs sockEvtArgs =
new SocketAsyncEventArgs
{
RemoteEndPoint = new IPEndPoint(IPAddress.Parse(IP),
Convert.ToInt32(Port)),
UserToken = Me
};
//connect a completion handler
sockEvtArgs.Completed +=
new EventHandler<SocketAsyncEventArgs>(Connection_Completed);
//connect asynchronously
ClientSocket.ConnectAsync(sockEvtArgs);
}
//connection completion handler
void Connection_Completed(object sender, SocketAsyncEventArgs e)
{
//connected successfully, send a
//ConnectionDisconnectionRequest with Connect=true
if (e.SocketError == SocketError.Success)
{
SocketAsyncEventArgs sockEvtArgs =
new SocketAsyncEventArgs { UserToken = e.UserToken };
//serialize a new ConnectionDisconnectionMessage into a MemoryStream
MemoryStream SerializedStream =
MessageWrapper.SerializeMessage(
new MessageWrapper
{
Message = new ConnectionDisconnectionRequest
{
From = e.UserToken as string,
Connect = true
}
});
//set buffer to the contents of the memorystream
sockEvtArgs.SetBuffer(SerializedStream.GetBuffer(),
0, (int)SerializedStream.Length);
sockEvtArgs.Completed +=
new EventHandler<SocketAsyncEventArgs>(ConnectionRequestSend_Completed);
//send
ClientSocket.SendAsync(sockEvtArgs);
}
}
//ConnectionDisconnectionRequest send completion handler
void ConnectionRequestSend_Completed(object sender, SocketAsyncEventArgs e)
{
//sent successfully
if (e.SocketError == SocketError.Success)
{
//start receiving messages
ReceiveMessage();
//switch context
ParentPage.Dispatcher.BeginInvoke(new Action(delegate
{
//switch view to participants
ParentPage.ShowParticipantsView();
}));
}
}
//receive a message
private void ReceiveMessage()
{
SocketAsyncEventArgs sockEvtArgsReceive = new SocketAsyncEventArgs();
sockEvtArgsReceive.SetBuffer(ReceiveBuffer, 0, RECEIVEBUFFERSIZE);
sockEvtArgsReceive.Completed +=
new EventHandler<SocketAsyncEventArgs>(Receive_Completed);
ClientSocket.ReceiveAsync(sockEvtArgsReceive);
}
//receive completion handler
void Receive_Completed(object sender, SocketAsyncEventArgs e)
{
if (e.SocketError == SocketError.Success)
{
ParentPage.Dispatcher.BeginInvoke(new Action(delegate
{
//copy the message to a temporary buffer - this is
//because we reuse the same buffer for all SocketAsyncEventArgs,
//and message lengths may vary
byte[] Message = new byte[e.BytesTransferred];
Array.Copy(e.Buffer, 0, Message, 0, e.BytesTransferred);
//process the message
ProcessMessage(Message);
//keep receiving
ReceiveMessage();
}));
}
}
//process a message
internal void ProcessMessage(byte[] Message)
{
//deserialize the message into the wrapper
MessageWrapper mw = MessageWrapper.DeserializeMessage(Message);
//check type of the contained message
//correct type resolution is ensured through the
//usage of KnownTypeAttribute on the MessageWrapper
//data contract declaration
if (mw.Message is TextMessage)
{
//receiving a text message from someone -
//switch to chat view if not there already
ParentPage.ShowChatView();
//remember the other party in the conversation
if (this.TalkingTo == null)
this.TalkingTo = (mw.Message as TextMessage).From;
//data bind the text of the message
Conversation.Add(mw.Message as TextMessage);
}
//someone has ended an ongoing chat
else if (mw.Message is ChatEndNotification)
{
//reset
this.TalkingTo = null;
//reset
Conversation.Clear();
//go back to participants list
ParentPage.ShowParticipantsView();
}
//server has sent a reply to your connection request
else if (mw.Message is ConnectionReply)
{
//reset
Participants.Clear();
//get the list of the other participants
List<string> ReplyList = (mw.Message as ConnectionReply).Participants;
//data bind
foreach (string s in ReplyList)
Participants.Add(s);
}
//someone has connected or disconnected
else if (mw.Message is ConnectionDisconnectionNotification)
{
ConnectionDisconnectionNotification notif =
mw.Message as ConnectionDisconnectionNotification;
//if it is a connection
if (notif.Connect)
//add to participants list
Participants.Add(notif.Participant);
else
{
//remove from participants list
Participants.Remove(notif.Participant);
//if you were in a conversation with this person,
//go back to the participants view
if (notif.Participant == TalkingTo)
{
ParentPage.ShowParticipantsView();
}
}
}
}
//send a text message
internal void SendTextMessage()
{
//package the From, To and Text of the message
//into a TextMessage, and then into a wrapper
MessageWrapper mwSend =
new MessageWrapper
{
Message = new TextMessage {
From = Me, To = TalkingTo, Body = MessageBody }
};
//serialize
MemoryStream SerializedStream = MessageWrapper.SerializeMessage(mwSend);
SocketAsyncEventArgs sockEvtArgsSend =
new SocketAsyncEventArgs { UserToken = mwSend.Message };
//grab the byte[] and set the buffer
sockEvtArgsSend.SetBuffer(
SerializedStream.GetBuffer(), 0, (int)SerializedStream.Length);
//attach handler
sockEvtArgsSend.Completed +=
new EventHandler<SocketAsyncEventArgs>(SendTextMessage_Completed);
//send
ClientSocket.SendAsync(sockEvtArgsSend);
}
//send completed
void SendTextMessage_Completed(object sender, SocketAsyncEventArgs e)
{
//success
if (e.SocketError == SocketError.Success)
{
//switch context
ParentPage.Dispatcher.BeginInvoke(new Action(delegate
{
//send was successful, add message to ongoing conversation
Conversation.Add(e.UserToken as TextMessage);
//reset edit box
MessageBody = "";
}));
}
}
//disconnect
internal void Disconnect()
{
SocketAsyncEventArgs sockEvtArgs = new SocketAsyncEventArgs();
//package a ConnectionDisconnectionRequest with Connect=false
MemoryStream SerializedStream =
MessageWrapper.SerializeMessage(
new MessageWrapper
{
Message = new ConnectionDisconnectionRequest
{
From = Me,
Connect = false
}
});
sockEvtArgs.SetBuffer(
SerializedStream.GetBuffer(), 0, (int)SerializedStream.Length);
sockEvtArgs.Completed +=
new EventHandler<SocketAsyncEventArgs>(DisconnectRequest_Completed);
ClientSocket.SendAsync(sockEvtArgs);
}
//disconnect completed
void DisconnectRequest_Completed(object sender, SocketAsyncEventArgs e)
{
//success
if (e.SocketError == SocketError.Success)
{
//reset my identity
this.Me = null;
//clear all participants
Participants.Clear();
//show login screen
ParentPage.ShowLoginView();
}
}
//end a chat
internal void SendChatEnd()
{
MessageWrapper mwSend =
new MessageWrapper
{
Message = new ChatEndNotification { From = Me, To = TalkingTo }
};
MemoryStream SerializedStream =
MessageWrapper.SerializeMessage(mwSend);
SocketAsyncEventArgs sockEvtArgsSend =
new SocketAsyncEventArgs { UserToken = mwSend.Message };
sockEvtArgsSend.SetBuffer(
SerializedStream.GetBuffer(), 0, (int)SerializedStream.Length);
sockEvtArgsSend.Completed +=
new EventHandler<SocketAsyncEventArgs>(SendChatEnd_Completed);
ClientSocket.SendAsync(sockEvtArgsSend);
}
//chat ended
void SendChatEnd_Completed(object sender, SocketAsyncEventArgs e)
{
//success
if (e.SocketError == SocketError.Success)
{
//switch context
ParentPage.Dispatcher.BeginInvoke(new Action(delegate
{
//reset identity of the other participant
this.TalkingTo = null;
//clear the conversation
Conversation.Clear();
//switch back to the participants view
ParentPage.ShowParticipantsView();
}));
}
}
}
}
|
As discussed before, ClientConnectionManager is used as the datasource for most of the data bound to the XAML for the client UI, and therefore implements INotifyPropertyChanged; the appropriate property setters raise PropertyChanged events.
After the user specifies the IP
address, a port, and a participant name in the initial login screen, you
establish a connection to the server. To do this, call the Join() method, which uses the Socket.ConnectAsync() method to establish the server connection. You specify the details of the remote endpoint (IP address and port) in the SocketeventArgs parameter. You also specify Connection_Completed() as the completion handler for ConnectAsync().
When a successful socket connection is established, in Connection_Completed() you send the first application-specific message of type ConnectionDisconnectionRequest to the server, with the ConnectionDisconnectionRequest.Connect property set to True, to indicate a request for connection. You wrap the message in an instance of the MessageWrapper type, serialize it to a MemoryStream, and use the MemoryStream contents to fill the send buffer. Attach ConnectionRequestSend_Completed() as the completion handler, and then call SendAsync() to send the request.
When the send request returns, ConnectionRequestSend_Completed() is invoked; you check for a successful send by checking the SocketAsyncEventArgs.SocketError property. In the event of a successful operation, this property is set to SocketError.Success;
a plethora of other values indicate different error conditions. On a
successful send, you prepare the client for receiving a message back
from the server by calling ReceiveMessage(). You also switch the client UI to display the view with the list of participants by calling ShowParticipantsView() on the Page.
In ReceiveMessage(), you use a preallocated buffer to receive all your messages. You call Socket.ReceiveAsync() to start receiving messages, after attaching the Receive_Completed()
completion handler. When a message is successfully retrieved, you copy
it out of the message buffer into a temporary one before you process the
message and take appropriate action. Note that you call ReceiveMessage()
again as soon as you complete processing the previous message in order
to keep the socket in a constant receive mode and not miss any incoming
messages—albeit on a background thread because of the asynchronous
nature of ReceiveAsync().
The ProcessMessage() method is central to the client-side message processing. Incoming messages are deserialized from byte[] to MessageWrapper instances by calling MessageWrapper.DeserializeMessage(). The type of the contained message in MessageWrapper.Body is used to determine the action taken.
The first message a client receives is the server's acknowledgment of the connection, in the form of a ConnectionReply message. The ConnectionReply.Participants collection contains the names of all the other participants logged in; you bind that collection to the participants ListBox on the UI and switch the view by calling ShowParticipantsView() on the page.
For incoming TextMessage instances, if the client is not already in chat mode, you switch the UI appropriately by calling ShowChatView() and then display the message by adding it to the Conversations collection bound to the ListBox used to display a conversation. You also set the ClientConnectionManager.TalkingTo property to the name of the participant from whom you are receiving the message, as indicated by the TextMessage.From property.
Clients can also receive a couple of other types of messages. When you receive a ChatEndNotification, you reset the TalkingTo property, clear the conversation ListBox, and switch to the participants view. For a ConnectionDisconnectionNotification, if the Connect property is True (indicating that a new participant is connecting), you add the participant to the bound Participants
property; otherwise, you remove them, and switch views if you were
currently in conversation with the disconnecting participant.
The ClientConnectionManager
class also implements various methods for sending different types of
messages from the client. All of these methods follow the same pattern
demonstrated when you sent the first ConnectionDisconnectionRequest earlier: you create and initialize a new message instance of the appropriate message type, serialize it using MessageWrapper.SerializeMessage(), and then send it using Socket.SendAsync().