![]() |
General Programming »
Internet / Network »
Client/Server Development
Intermediate
License: The Code Project Open License (CPOL)
A WCF-WPF Chat ApplicationBy Islam ElDemeryAn Internet chat application with file transfer. |
C# (C# 1.0, C# 2.0, C# 3.0), .NET (.NET 2.0, .NET 3.0, .NET 3.5), Visual Studio (VS2008), WCF, XAML, WPF
|
|
Advanced Search |
|
|
|
||||||||||||||||
This application is built on the WCF .NET Framework 3.0. It uses duplex communication and TCP binding (for some reasons that are discussed later in this article). It concentrates on handling and controlling a WCF service that has to make reliable sessions with many clients and keep those connections alive as long as possible.
The application UI is built on WPF .NET Framework 3.0, and here I have to say that I am not good in WPF, and I haven't used animations; I just wanted it to look better, so I chose WPF.
.NET 3.5 Framework must be installed.
Before we start, I have to mention two great applications that I have found:
As Sacha explains here, it is a great application (and Sacha is really smart -I love this man-).
The WCF Chat mechanism developed by Nikola and Sacha is a great technique, but it is complex somehow; I wasn't able to understand Sacha's article the first time I read it, that's why I decided to make this simple and step by step.
Note that this article uses client-server pattern and not peer to peer.
In this article, you are going to learn how to:
The application features include:
I really recommend these two books in case you like WCF:
This service is a singleton service; every client starting a session does not instantiate a new instance of the service. This is to enable a single service to deal with many clients. This means that the first client calling the service instantiates a new instance of the service, and each subsequent call is just calling the service operations. Service operations are methods or functions that the service implements; a service represents its operations in an interface, and represents the callback operations in another interface, asking a client to implement those callback operations, to be able to call the client again whenever it may want.
So, you have got to know:
netTcpBinding which allows duplex communications (the red EndPoint in the image above), and mexTcpBinding to support publishing service metadata (the blue EndPoint in the image above).Steps in points:
ServiceHost classusing System.Linq;
using System.Text;
System.ServiceModel.using System.ServiceModel;.using System;
using System.Collections.Generic;
using System.ServiceModel;
namespace ServiceAssembly
{
public class ChatService
{
}
}
ChatService) in the ServiceAssembly namespace. The first class we will add is a Data Contract; a data contract means an agreement to transfer this type of data.System.Runtime.Serialization, and so add this line: using System.Runtime.Serialization;. [DataContract]
public class Client
{
private string _name;
private int _avatarID;
private DateTime _time;
[DataMember]
public string Name
{
get { return _name; }
set { _name = value; }
}
[DataMember]
public int AvatarID
{
get { return _avatarID; }
set { _avatarID = value; }
}
[DataMember]
public DateTime Time
{
get { return _time; }
set { _time = value; }
}
}
Message will have information about the sender, content, and time: [DataContract]
public class Message
{
private string _sender;
private string _content;
private DateTime _time;
[DataMember]
public string Sender
{
get { return _sender; }
set { _sender = value; }
}
[DataMember]
public string Content
{
get { return _content; }
set { _content = value; }
}
[DataMember]
public DateTime Time
{
get { return _time; }
set { _time = value; }
}
}
MessageFile to enable sending files between clients. [DataContract]
public class FileMessage
{
private string sender;
private string fileName;
private byte[] data;
private DateTime time;
[DataMember]
public string Sender
{
get { return sender; }
set { sender = value; }
}
[DataMember]
public string FileName
{
get { return fileName; }
set { fileName = value; }
}
[DataMember]
public byte[] Data
{
get { return data; }
set { data = value; }
}
[DataMember]
public DateTime Time
{
get { return time; }
set { time = value; }
}
}
[ServiceContract(CallbackContract = typeof(IChatCallback),
SessionMode = SessionMode.Required)]
public interface IChat
{
[OperationContract(IsInitiating = true)]
bool Connect(Client client);
[OperationContract(IsOneWay = true)]
void Say(Message msg);
[OperationContract(IsOneWay = true)]
void Whisper(Message msg, Client receiver);
[OperationContract(IsOneWay = true)]
void IsWriting(Client client);
[OperationContract(IsOneWay = false)]
bool SendFile(FileMessage fileMsg, Client receiver);
[OperationContract(IsOneWay = true, IsTerminating = true)]
void Disconnect(Client client);
}
For designing a service contract, we need to know how the client is going to interact with the service. Here, we specify that a client has to start a session with the service, and terminate this session by calling some operations that can really start and terminate a session (this is why we set SessionMode = SessionMode.Required). Well, how can a service start a session or terminate it? The answer is, just by setting two properties (IsInitiating or IsTerminating) in the OperationContract attribute, and the WCF runtime will understand this. Operations can be one-way (void); this has an advantage that a client can call the operation and proceed its process without waiting for a reply from the service. In our contract, all operations are one-way except for the Connect operation; it returns a boolean to know if the client has been successfully joined, or the client name has been found and already exists. The last thing is to reference the Callback interface, which is the interface the client will implement: CallbackContract = typeof(IChatCallback).
public interface IChatCallback
{
[OperationContract(IsOneWay = true)]
void RefreshClients(List< Client> clients);
[OperationContract(IsOneWay = true)]
void Receive(Message msg);
[OperationContract(IsOneWay = true)]
void ReceiveWhisper(Message msg, Client receiver);
[OperationContract(IsOneWay = true)]
void IsWritingCallback(Client client);
[OperationContract(IsOneWay = true)]
void ReceiverFile(FileMessage fileMsg, Client receiver);
[OperationContract(IsOneWay = true)]
void UserJoin(Client client);
[OperationContract(IsOneWay = true)]
void UserLeave(Client client);
}
The service is responsible for defining these parameters and calling those operations.
IChat interface). Our service contains a generic dictionary of key types of clients and value types of IChatCallback; so this generic collection will hold the online clients as keys and callback objects for each client as values. The service also contains a generic list to hold the online clients (to quickly pass it to users), a public property that represents the current callback object, a private method used by the service to search for a client in the clients list, and an object to synchronize our work - this object is useful to lock the current thread from receiving calls from clients and wait for the current operation to be completed. We will need this because in the service context, you might be sending some information for each callback object using a foreach loop, and suddenly one of these callback objects' client might get disconnected, and the operation won't complete because the collection has been modified and a client has been removed from it.Well, this brings us to talk about handling concurrency on the service. A WCF service can handle concurrency in three different ways. The Single and Reentrant options use a synchronized pattern to handle incoming calls from clients, but this can make a deadlock if a client tries to call a service and waits for a reply, the service processes the client request and needs to call the client back again, but the client is still waiting for a reply from the service, and thus causing a deadlock. The Reentrant option will make the WCF release the lock, but we will use the Multiple option which allows to make calls on another thread (this is good, but needs us to synchronize our code as we said before).
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single,
ConcurrencyMode = ConcurrencyMode.Multiple,
UseSynchronizationContext = false)]
public class ChatService
{
Dictionary< Client, IChatCallback> clients =
new Dictionary< Client, IChatCallback>();
List< Client> clientList = new List< Client>();
public INewServiceCallback CurrentCallback
{
get
{
return OperationContext.Current.
GetCallbackChannel< IChatCallback>();
}
}
object syncObj = new object();
private bool SearchClientsByName(string name)
{
foreach (Client c in clients.Keys)
{
if (c.Name == name)
{
return true;
}
}
return false;
}
}
IChat interface: public class ChatService : IChat
{
...
#region IChat Members
public bool Connect(Client client)
{
if (!clients.ContainsValue(CurrentCallback) &&
!SearchClientsByName(client.Name))
{
lock (syncObj)
{
clients.Add(client, CurrentCallback);
clientList.Add(client);
foreach (Client key in clients.Keys)
{
IChatCallback callback = clients[key];
try
{
callback.RefreshClients(clientList);
callback.UserJoin(client);
}
catch
{
clients.Remove(key);
return false;
}
}
}
return true;
}
return false;
}
public void Say(Message msg)
{
lock (syncObj)
{
foreach (IChatCallback callback in clients.Values)
{
callback.Receive(msg);
}
}
}
public void Whisper(Message msg, Client receiver)
{
foreach (Client rec in clients.Keys)
{
if (rec.Name == receiver.Name)
{
IChatCallback callback = clients[rec];
callback.ReceiveWhisper(msg, rec);
foreach (Client sender in clients.Keys)
{
if (sender.Name == msg.Sender)
{
IChatCallback senderCallback = clients[sender];
senderCallback.ReceiveWhisper(msg, rec);
return;
}
}
}
}
}
public void IsWriting(Client client)
{
lock (syncObj)
{
foreach (IChatCallback callback in clients.Values)
{
callback.IsWritingCallback(client);
}
}
}
public bool SendFile(FileMessage fileMsg, Client receiver)
{
foreach (Client rcvr in clients.Keys)
{
if (rcvr.Name == receiver.Name)
{
Message msg = new Message();
msg.Sender = fileMsg.Sender;
msg.Content = "I'M SENDING FILE.. " + fileMsg.FileName;
IChatCallback rcvrCallback = clients[rcvr];
rcvrCallback.ReceiveWhisper(msg, receiver);
rcvrCallback.ReceiverFile(fileMsg, receiver);
foreach (Client sender in clients.Keys)
{
if (sender.Name == fileMsg.Sender)
{
IChatCallback sndrCallback = clients[sender];
sndrCallback.ReceiveWhisper(msg, receiver);
return true;
}
}
}
}
return false;
}
public void Disconnect(Client client)
{
foreach (Client c in clients.Keys)
{
if (client.Name == c.Name)
{
lock (syncObj)
{
this.clients.Remove(c);
this.clientList.Remove(c);
foreach (IChatCallback callback in clients.Values)
{
callback.RefreshClients(this.clientList);
callback.UserLeave(client);
}
}
return;
}
}
}
#endregion
}
using System;
using System.Collections.Generic;
using System.ServiceModel;
using System.Runtime.Serialization;
namespace ServiceAssembly
{
[DataContract]
public class Client
{
private string _name;
private int _avatarID;
private DateTime _time;
[DataMember]
public string Name
{
get { return _name; }
set { _name = value; }
}
[DataMember]
public int AvatarID
{
get { return _avatarID; }
set { _avatarID = value; }
}
[DataMember]
public DateTime Time
{
get { return _time; }
set { _time = value; }
}
}
[DataContract]
public class Message
{
private string _sender;
private string _content;
private DateTime _time;
[DataMember]
public string Sender
{
get { return _sender; }
set { _sender = value; }
}
[DataMember]
public string Content
{
get { return _content; }
set { _content = value; }
}
[DataMember]
public DateTime Time
{
get { return _time; }
set { _time = value; }
}
}
[ServiceContract(CallbackContract = typeof(IChatCallback),
SessionMode = SessionMode.Required)]
public interface IChat
{
[OperationContract(IsInitiating = true)]
bool Connect(Client client);
[OperationContract(IsOneWay = true)]
void Say(Message msg);
[OperationContract(IsOneWay = true)]
void Whisper(Message msg, Client receiver);
[OperationContract(IsOneWay = true)]
void IsWriting(Client client);
[OperationContract(IsOneWay = true,
IsTerminating = true)]
void Disconnect(Client client);
}
public interface IChatCallback
{
[OperationContract(IsOneWay = true)]
void RefreshClients(List< Client> clients);
[OperationContract(IsOneWay = true)]
void Receive(Message msg);
[OperationContract(IsOneWay = true)]
void ReceiveWhisper(Message msg, Client receiver);
[OperationContract(IsOneWay = true)]
void IsWritingCallback(Client client);
[OperationContract(IsOneWay = true)]
void UserJoin(Client client);
[OperationContract(IsOneWay = true)]
void UserLeave(Client client);
}
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single,
ConcurrencyMode = ConcurrencyMode.Multiple,
UseSynchronizationContext = false)]
public class ChatService : IChat
{
Dictionary< Client, IChatCallback> clients =
new Dictionary< List< Client>
clientList = new List< Client>();
public IChatCallback CurrentCallback
{
get
{
return OperationContext.Current.
GetCallbackChannel< IChatCallback>();
}
}
object syncObj = new object();
private bool SearchClientsByName(string name)
{
foreach (Client c in clients.Keys)
{
if (c.Name == name)
{
return true;
}
}
return false;
}
#region IChat Members
public bool Connect(Client client)
{
if (!clients.ContainsValue(CurrentCallback) &&
!SearchClientsByName(client.Name))
{
lock (syncObj)
{
clients.Add(client, CurrentCallback);
clientList.Add(client);
foreach (Client key in clients.Keys)
{
IChatCallback callback = clients[key];
try
{
callback.RefreshClients(clientList);
callback.UserJoin(client);
}
catch
{
clients.Remove(key);
return false;
}
}
}
return true;
}
return false;
}
public void Say(Message msg)
{
lock (syncObj)
{
foreach (IChatCallback callback in clients.Values)
{
callback.Receive(msg);
}
}
}
public void Whisper(Message msg, Client receiver)
{
foreach (Client rec in clients.Keys)
{
if (rec.Name == receiver.Name)
{
IChatCallback callback = clients[rec];
callback.ReceiveWhisper(msg, rec);
foreach (Client sender in clients.Keys)
{
if (sender.Name == msg.Sender)
{
IChatCallback senderCallback = clients[sender];
senderCallback.ReceiveWhisper(msg, rec);
return;
}
}
}
}
}
public void IsWriting(Client client)
{
lock (syncObj)
{
foreach (IChatCallback callback in clients.Values)
{
callback.IsWritingCallback(client);
}
}
}
public bool SendFile(FileMessage fileMsg, Client receiver)
{
foreach (Client rcvr in clients.Keys)
{
if (rcvr.Name == receiver.Name)
{
Message msg = new Message();
msg.Sender = fileMsg.Sender;
msg.Content = "I'M SENDING FILE.. " + fileMsg.FileName;
IChatCallback rcvrCallback = clients[rcvr];
rcvrCallback.ReceiveWhisper(msg, receiver);
rcvrCallback.ReceiverFile(fileMsg, receiver);
foreach (Client sender in clients.Keys)
{
if (sender.Name == fileMsg.Sender)
{
IChatCallback sndrCallback = clients[sender];
sndrCallback.ReceiveWhisper(msg, receiver);
return true;
}
}
}
}
return false;
}
public void Disconnect(Client client)
{
foreach (Client c in clients.Keys)
{
if (client.Name == c.Name)
{
lock (syncObj)
{
this.clients.Remove(c);
this.clientList.Remove(c);
foreach (IChatCallback callback in clients.Values)
{
callback.RefreshClients(this.clientList);
callback.UserLeave(client);
}
}
return;
}
}
}
#endregion
}
}
System.ServiceModel.< window title="Chat Service Host"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
x:class="WPFHost.Window1" height="300" width="300" />
< grid />
< grid.background />
< lineargradientbrush />
< gradientstop color="LightSlateGray" offset="0" />
< gradientstop color="White" offset="0.5" />
< gradientstop color="LightSlateGray" offset="0.9" />
< /lineargradientbrush />
< /grid.background />
< label name="label1" height="28" width="67"
margin="10,93,0,0" verticalalignment="Top"
horizontalalignment="Left">Local IP:</label />
< label name="label2" height="28" width="67"
margin="10,0,0,85" verticalalignment="Bottom"
horizontalalignment="Left">Listen Port:</label />
< textbox height="23" margin="76,98,108,0"
verticalalignment="Top" x:name="textBoxIP" text="localhost" />
< textbox height="23" margin="76,0,108,88"
verticalalignment="Bottom" x:name="textBoxPort"
text="7997" />
< button height="23" width="82"
margin="0,0,15,88" verticalalignment="Bottom"
horizontalalignment="Right" x:name="buttonStop"
click="buttonStop_Click">Stop</button />
< button height="23" width="82"
margin="0,96,15,0" verticalalignment="Top"
horizontalalignment="Right" x:name="buttonStart"
click="buttonStart_Click">Start</button />
< label height="28" margin="10,0,15,45"
verticalalignment="Bottom" x:name="labelStatus">Status</label />
< label height="37" margin="10,18,15,0"
verticalalignment="Top" x:name="labelTitle"
fontfamily="Jokerman" fontsize="20"
foreground="White">Chat Service</label />
< /grid />
< /window />
This is our host application that should instantiate a ServiceHost object. A ServiceHost object is what will actually host your service, enable you to apply bindings, add EndPoints, start the service or stop it. So, we will start defining a ServiceHost programmatically, and then use the configuration file.
System.Runtime.Serialization.using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.ServiceModel;
using ServiceAssembly;
using System.ServiceModel.Description;
using System.Xml;
namespace WPFHost
{
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
}
ServiceHost host;
private void buttonStart_Click(object sender,
RoutedEventArgs e)
{
buttonStart.IsEnabled = false;
//Define base addresses so all
//endPoints can go under it
Uri tcpAdrs = new Uri("net.tcp://" +
textBoxIP.Text.ToString() + ":" +
textBoxPort.Text.ToString() + "/WPFHost/");
Uri httpAdrs = new Uri("http://" +
textBoxIP.Text.ToString() + ":" +
(int.Parse(textBoxPort.Text.ToString()) + 1).ToString() +
"/WPFHost/");
Uri[] baseAdresses = { tcpAdrs, httpAdrs };
host = new ServiceHost(
typeof(ServiceAssembly.ChatService), baseAdresses);
NetTcpBinding tcpBinding =
new NetTcpBinding(SecurityMode.None, true);
//Updated: to enable file transefer of 64 MB
tcpBinding.MaxBufferPoolSize = (int)67108864;
tcpBinding.MaxBufferSize = 67108864;
tcpBinding.MaxReceivedMessageSize = (int)67108864;
tcpBinding.TransferMode = TransferMode.Buffered;
tcpBinding.ReaderQuotas.MaxArrayLength = 67108864;
tcpBinding.ReaderQuotas.MaxBytesPerRead = 67108864;
tcpBinding.ReaderQuotas.MaxStringContentLength = 67108864;
tcpBinding.MaxConnections = 100;
//To maxmize MaxConnections you have
//to assign another port for mex endpoint
//and configure ServiceThrottling as well
ServiceThrottlingBehavior throttle;
throttle =
host.Description.Behaviors.Find< ServiceThrottlingBehavior>();
if (throttle == null)
{
throttle = new ServiceThrottlingBehavior();
throttle.MaxConcurrentCalls = 100;
throttle.MaxConcurrentSessions = 100;
host.Description.Behaviors.Add(throttle);
}
//Enable reliable session and keep
//the connection alive for 20 hours.
tcpBinding.ReceiveTimeout = new TimeSpan(20, 0, 0);
tcpBinding.ReliableSession.Enabled = true;
tcpBinding.ReliableSession.InactivityTimeout =
new TimeSpan(20, 0, 10);
host.AddServiceEndpoint(typeof(ServiceAssembly.IChat),
tcpBinding, "tcp");
//Define Metadata endPoint, So we can
//publish information about the service
ServiceMetadataBehavior mBehave =
new ServiceMetadataBehavior();
host.Description.Behaviors.Add(mBehave);
host.AddServiceEndpoint(typeof(IMetadataExchange),
MetadataExchangeBindings.CreateMexTcpBinding(),
"net.tcp://" + textBoxIP.Text.ToString() + ":" +
(int.Parse(textBoxPort.Text.ToString()) - 1).ToString() +
"/WPFHost/mex");
try
{
host.Open();
}
catch (Exception ex)
{
labelStatus.Content = ex.Message.ToString();
}
finally
{
if (host.State == CommunicationState.Opened)
{
labelStatus.Content = "Opened";
buttonStop.IsEnabled = true;
}
}
}
private void buttonStop_Click(object sender, RoutedEventArgs e)
{
if (host != null)
{
try
{
host.Close();
}
catch (Exception ex)
{
labelStatus.Content = ex.Message.ToString();
}
finally
{
if (host.State == CommunicationState.Closed)
{
labelStatus.Content = "Closed";
buttonStart.IsEnabled = true;
buttonStop.IsEnabled = false;
}
}
}
}
}
}
< configuration />
< system.servicemodel />
< services />
< service name="WCFService.Service"
behaviorconfiguration="behaviorConfig" />
< host />
< baseaddresses />
< add baseaddress="net.tcp://localhost:7997/WPFHost/" />
< add baseaddress="http://localhost:7998/WPFHost/" />
< /baseaddresses />
< /host />
< endpoint contract="ServiceAssembly.IChat" binding="netTcpBinding"
address="tcp" bindingconfiguration="tcpBinding" />
< endpoint contract="IMetadataExchange" binding="mexTcpBinding"
address="net.tcp://localhost:7996/WcfWinFormsHost/mex" />
< /service />
< /services />
< behaviors />
< servicebehaviors />
< behavior name="behaviorConfig" />
< servicemetadata httpgetenabled="true" />
< servicedebug includeexceptiondetailinfaults="true" />
< servicethrottling maxconcurrentcalls="100"
maxconcurrentsessions="100" />
< /behavior />
< /servicebehaviors />
< /behaviors />
< bindings />
< nettcpbinding />
< binding name="tcpBinding" maxbuffersize="67108864"
maxreceivedmessagesize="67108864" maxbufferpoolsize="67108864"
transfermode="Buffered" closetimeout="00:00:10"
opentimeout="00:00:10" receivetimeout="00:20:00"
sendtimeout="00:01:00" maxconnections="100" />
< security mode="None" />
< /security />
< readerquotas maxarraylength="67108864"
maxbytesperread="67108864"
maxstringcontentlength="67108864" />
< reliablesession enabled="true"
inactivitytimeout="00:20:00" />
< /binding />
< /nettcpbinding />
< /bindings />
< /system.servicemodel />
< /configuration />
System.ServiceModel.As you see, we give it the address of the mex (our metadata) EndPoint, and it will know and configure a new config file with the address of the TCP EndPoint. Click Advanced to enable asynchronous operations and generic lists.
maxBufferPoolSize="67108864"
maxBufferSize="67108864" maxConnections="100"
maxReceivedMessageSize="67108864">
Client code commented, read from the top to bottom..
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.IO;
using System.Reflection;
using System.ServiceModel;
using WPFClient.SVC;
using System.Collections;
using System.Windows.Threading;
using Microsoft.Win32;
namespace WPFClient
{
/// < summary>
/// Interaction logic for Window1.xaml
/// < /summary>
public partial class Window1 : Window, SVC.IChatCallback
{
//SVC holds references to the proxy and cotracts..
SVC.ChatClient proxy = null;
SVC.Client receiver = null;
SVC.Client localClient = null;
//Client will create this folder when loading
string rcvFilesPath = @"C:/WCF_Received_Files/";
//When the communication object
//turns to fault state it will
//require another thread to invoke a fault event
private delegate void FaultedInvoker();
//This will hold each online client with
//a listBoxItem to quickly handle adding
//and removing clients when they join or leave
Dictionary< ListBoxItem, SVC.Client> OnlineClients =
new Dictionary< ListBoxItem, Client>();
public Window1()
{
InitializeComponent();
this.Loaded += new RoutedEventHandler(Window1_Loaded);
chatListBoxNames.SelectionChanged += new
SelectionChangedEventHandler(
chatListBoxNames_SelectionChanged);
chatTxtBoxType.KeyDown +=
new KeyEventHandler(chatTxtBoxType_KeyDown);
chatTxtBoxType.KeyUp +=
new KeyEventHandler(chatTxtBoxType_KeyUp);
}
//Service might be disconnected or stopped for any reason,
//so we have to handle the state of the communication object,
//the communication object will fire
//an event for each transitioning
//from a state to another, notice that when a connection state goes
//from opening to opened or from opened to closing state.. it can't go
//back so, if it is closed or faulted you have to set the proxy = null;
//to be able to create a proxy again and open a connection
//..
//I have made a method called HandleProxy() to handle the state
//of the connection, so in each event like opened, closed or faulted
//we will call this method, and it will switch on the connection state
//and apply a suitable reaction.
//..
//Because this events will need to be invoked on another thread
//you can do like so in WPF applications (I've got this idea from
//Sacha Barber's greate article on WCF WPF Application)
void InnerDuplexChannel_Closed(object sender, EventArgs e)
{
if (!this.Dispatcher.CheckAccess())
{
this.Dispatcher.BeginInvoke(DispatcherPriority.Normal,
new FaultedInvoker(HandleProxy));
return;
}
HandleProxy();
}
void InnerDuplexChannel_Opened(object sender, EventArgs e)
{
if (!this.Dispatcher.CheckAccess())
{
this.Dispatcher.BeginInvoke(DispatcherPriority.Normal,
new FaultedInvoker(HandleProxy));
return;
}
HandleProxy();
}
void InnerDuplexChannel_Faulted(object sender, EventArgs e)
{
if (!this.Dispatcher.CheckAccess())
{
this.Dispatcher.BeginInvoke(DispatcherPriority.Normal,
new FaultedInvoker(HandleProxy));
return;
}
HandleProxy();
}
#region Private Methods
/// < summary>
/// This is the most method I like, it helps us alot
/// We may can't know when a connection is lost in
/// of network failure or service stopped.
/// And also to maintain performance client doesnt know
/// that the connection will be lost when hitting the
/// disconnect button, but when a session is terminated
/// this method will be called, and it will handle everything.
/// < /summary>
private void HandleProxy()
{
if (proxy != null)
{
switch (this.proxy.State)
{
case CommunicationState.Closed:
proxy = null;
chatListBoxMsgs.Items.Clear();
chatListBoxNames.Items.Clear();
loginLabelStatus.Content = "Disconnected";
ShowChat(false);
ShowLogin(true);
loginButtonConnect.IsEnabled = true;
break;
case CommunicationState.Closing:
break;
case CommunicationState.Created:
break;
case CommunicationState.Faulted:
proxy.Abort();
proxy = null;
chatListBoxMsgs.Items.Clear();
chatListBoxNames.Items.Clear();
ShowChat(false);
ShowLogin(true);
loginLabelStatus.Content = "Disconnected";
loginButtonConnect.IsEnabled = true;
break;
case CommunicationState.Opened:
ShowLogin(false);
ShowChat(true);
chatLabelCurrentStatus.Content = "online";
chatLabelCurrentUName.Content = this.localClient.Name;
Dictionary< int, Image> images = GetImages();
Image img = images[loginComboBoxImgs.SelectedIndex];
chatCurrentImage.Source = img.Source;
break;
case CommunicationState.Opening:
break;
default:
break;
}
}
}
/// < summary>
/// This is the second important method, which creates
/// the proxy, subscribe to connection state events
/// and open a connection with the service
/// < /summary>
private void Connect()
{
if (proxy == null)
{
try
{
this.localClient = new SVC.Client();
this.localClient.Name = loginTxtBoxUName.Text.ToString();
this.localClient.AvatarID = loginComboBoxImgs.SelectedIndex;
InstanceContext context = new InstanceContext(this);
proxy = new SVC.ChatClient(context);
//As the address in the configuration file is set to localhost
//we want to change it so we can call a service in internal
//network, or over internet
string servicePath = proxy.Endpoint.ListenUri.AbsolutePath;
string serviceListenPort =
proxy.Endpoint.Address.Uri.Port.ToString();
proxy.Endpoint.Address = new EndpointAddress("net.tcp://"
+ loginTxtBoxIP.Text.ToString() + ":" +
serviceListenPort + servicePath);
proxy.Open();
proxy.InnerDuplexChannel.Faulted +=
new EventHandler(InnerDuplexChannel_Faulted);
proxy.InnerDuplexChannel.Opened +=
new EventHandler(InnerDuplexChannel_Opened);
proxy.InnerDuplexChannel.Closed +=
new EventHandler(InnerDuplexChannel_Closed);
proxy.ConnectAsync(this.localClient);
proxy.ConnectCompleted += new EventHandler<
ConnectCompletedEventArgs>(proxy_ConnectCompleted);
}
catch (Exception ex)
{
loginTxtBoxUName.Text = ex.Message.ToString();
loginLabelStatus.Content = "Offline";
loginButtonConnect.IsEnabled = true;
}
}
else
{
HandleProxy();
}
}
private void Send()
{
if (proxy != null && chatTxtBoxType.Text != "")
{
if (proxy.State == CommunicationState.Faulted)
{
HandleProxy();
}
else
{
//Create message, assign its properties
SVC.Message msg = new WPFClient.SVC.Message();
msg.Sender = this.localClient.Name;
msg.Content = chatTxtBoxType.Text.ToString();
//If whisper mode is checked and an item is
//selected in the list box of clients, it will
//arrange a client object called receiver
//to whisper
if ((bool)chatCheckBoxWhisper.IsChecked)
{
if (this.receiver != null)
{
proxy.WhisperAsync(msg, this.receiver);
chatTxtBoxType.Text = "";
chatTxtBoxType.Focus();
}
}
else
{
proxy.SayAsync(msg);
chatTxtBoxType.Text = "";
chatTxtBoxType.Focus();
}
//Tell the service to tell back
//all clients that this client
//has just finished typing..
proxy.IsWritingAsync(null);
}
}
}
/// < summary>
/// This method to enable us scrolling the list box of messages
/// when a new message comes from the service..
/// < /summary>
private ScrollViewer FindVisualChild(DependencyObject obj)
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
{
DependencyObject child = VisualTreeHelper.GetChild(obj, i);
if (child != null && child is ScrollViewer)
{
return (ScrollViewer)child;
}
else
{
ScrollViewer childOfChild = FindVisualChild(child);
if (childOfChild != null)
{
return childOfChild;
}
}
}
return null;
}
/// < summary>
/// This is an important method which is called whenever
/// a message comes from the service, a client joins or
/// leaves, to return a ready item to be added in the
/// list box (either the one for messages or the one for
/// clients).
/// < /summary>
private ListBoxItem MakeItem(int imgID, string text)
{
ListBoxItem item = new ListBoxItem();
Dictionary< int, Image> images = GetImages();
Image img = images[imgID];
img.Height = 70;
img.Width = 60;
item.Content = img;
TextBlock txtblock = new TextBlock();
txtblock.Text = text;
txtblock.VerticalAlignment = VerticalAlignment.Center;
StackPanel panel = new StackPanel();
panel.Orientation = Orientation.Horizontal;
panel.Children.Add(item);
panel.Children.Add(txtblock);
ListBoxItem bigItem = new ListBoxItem();
bigItem.Content = panel;
return bigItem;
}
/// < summary>
/// This method is not used, I just put it here to help
/// you in case you want to make a rich text box and enable
/// emoticons for example.
/// Just add a richTextBox control and set
/// richTextBox.Document = MakeDocument(imgid, text);
/// < /summary>
private FlowDocument MakeDocument(int imgID, string text)
{
Dictionary< int, Image> images = GetImages();
Image img = images[imgID];
img.Height = 70;
img.Width = 60;
Block imgBlock = new BlockUIContainer(img);
Block txtBlock = new Paragraph(new Run(text));
FlowDocument doc = new FlowDocument();
doc.Blocks.Add(imgBlock);
doc.Blocks.Add(txtBlock);
doc.FlowDirection = FlowDirection.LeftToRight;
return doc;
}
/// < summary>
/// A method to retreive avatars as stream objects
/// and get an objects of type Image from the stream,
/// to return a dictionary of images and an ID for each
/// image.
/// < /summary>
private Dictionary< int, Image> GetImages()
{
List< Stream> picsStrm = new List< Stream>();
Assembly asmb = Assembly.GetExecutingAssembly();
string[] picNames = asmb.GetManifestResourceNames();
foreach (string s in picNames)
{
if (s.EndsWith(".png"))
{
Stream strm = asmb.GetManifestResourceStream(s);
if (strm != null)
{
picsStrm.Add(strm);
}
}
}
Dictionary< int, Image> images = new Dictionary< int, Image>();
int i = 0;
foreach (Stream strm in picsStrm)
{
PngBitmapDecoder decoder = new PngBitmapDecoder(strm,
BitmapCreateOptions.PreservePixelFormat,
BitmapCacheOption.Default);
BitmapSource bitmap = decoder.Frames[0] as BitmapSource;
Image img = new Image();
img.Source = bitmap;
img.Stretch = Stretch.UniformToFill;
images.Add(i, img);
i++;
strm.Close();
}
return images;
}
/// < summary>
/// Show or hide login controls depends on the parameter
/// < /summary>
/// < param name="show">< /param>
private void ShowLogin(bool show)
{
if (show)
{
loginButtonConnect.Visibility = Visibility.Visible;
loginComboBoxImgs.Visibility = Visibility.Visible;
loginLabelIP.Visibility = Visibility.Visible;
loginLabelStatus.Visibility = Visibility.Visible;
loginLabelTitle.Visibility = Visibility.Visible;
loginLabelUName.Visibility = Visibility.Visible;
loginPolyLine.Visibility = Visibility.Visible;
loginTxtBoxIP.Visibility = Visibility.Visible;
loginTxtBoxUName.Visibility = Visibility.Visible;
}
else
{
loginButtonConnect.Visibility = Visibility.Collapsed;
loginComboBoxImgs.Visibility = Visibility.Collapsed;
loginLabelIP.Visibility = Visibility.Collapsed;
loginLabelStatus.Visibility = Visibility.Collapsed;
loginLabelTitle.Visibility = Visibility.Collapsed;
loginLabelUName.Visibility = Visibility.Collapsed;
loginPolyLine.Visibility = Visibility.Collapsed;
loginTxtBoxIP.Visibility = Visibility.Collapsed;
loginTxtBoxUName.Visibility = Visibility.Collapsed;
}
}
/// < summary>
/// Show or hide chat controls depends on the parameter
/// < /summary>
/// < param name="show">< /param>
private void ShowChat(bool show)
{
if (show)
{
chatButtonDisconnect.Visibility = Visibility.Visible;
chatButtonSend.Visibility = Visibility.Visible;
chatCheckBoxWhisper.Visibility = Visibility.Visible;
chatCurrentImage.Visibility = Visibility.Visible;
chatLabelCurrentStatus.Visibility = Visibility.Visible;
chatLabelCurrentUName.Visibility = Visibility.Visible;
chatListBoxMsgs.Visibility = Visibility.Visible;
chatListBoxNames.Visibility = Visibility.Visible;
chatTxtBoxType.Visibility = Visibility.Visible;
chatLabelWritingMsg.Visibility = Visibility.Visible;
chatLabelSendFileStatus.Visibility = Visibility.Visible;
chatButtonOpenReceived.Visibility = Visibility.Visible;
chatButtonSendFile.Visibility = Visibility.Visible;
}
else
{
chatButtonDisconnect.Visibility = Visibility.Collapsed;
chatButtonSend.Visibility = Visibility.Collapsed;
chatCheckBoxWhisper.Visibility = Visibility.Collapsed;
chatCurrentImage.Visibility = Visibility.Collapsed;
chatLabelCurrentStatus.Visibility = Visibility.Collapsed;
chatLabelCurrentUName.Visibility = Visibility.Collapsed;
chatListBoxMsgs.Visibility = Visibility.Collapsed;
chatListBoxNames.Visibility = Visibility.Collapsed;
chatTxtBoxType.Visibility = Visibility.Collapsed;
chatLabelWritingMsg.Visibility = Visibility.Collapsed;
chatLabelSendFileStatus.Visibility = Visibility.Collapsed;
chatButtonOpenReceived.Visibility = Visibility.Collapsed;
chatButtonSendFile.Visibility = Visibility.Collapsed;
}
}
#endregion
#region UI_Events
void Window1_Loaded(object sender, RoutedEventArgs e)
{
//Create a folder named WCF_Received_Files in C directory
DirectoryInfo dir = new DirectoryInfo(rcvFilesPath);
dir.Create();
Dictionary< int, Image> images = GetImages();
//Populate images in the login comboBoc control
foreach (Image img in images.Values)
{
ListBoxItem item = new ListBoxItem();
item.Width = 90;
item.Height = 90;
item.Content = img;
loginComboBoxImgs.Items.Add(item);
}
loginComboBoxImgs.SelectedIndex = 0;
ShowChat(false);
ShowLogin(true);
}
private void chatButtonOpenReceived_Click(object sender,
RoutedEventArgs e)
{
//Open WCF_Received_Files folder in windows explorer
System.Diagnostics.Process.Start(rcvFilesPath);
}
private void chatButtonSendFile_Click(object sender,
RoutedEventArgs e)
{
if (this.receiver != null)
{
Stream strm = null;
try
{
OpenFileDialog fileDialog = new OpenFileDialog();
fileDialog.Multiselect = false;
if (fileDialog.ShowDialog() == DialogResult.HasValue)
{
return;
}
strm = fileDialog.OpenFile();
if (strm != null)
{
byte[] buffer = new byte[(int)strm.Length];
int i = strm.Read(buffer, 0, buffer.Length);
if (i > 0)
{
SVC.FileMessage fMsg = new FileMessage();
fMsg.FileName = fileDialog.SafeFileName;
fMsg.Sender = this.localClient.Name;
fMsg.Data = buffer;
proxy.SendFileAsync(fMsg, this.receiver);
proxy.SendFileCompleted += new
EventHandler< SendFileCompletedEventArgs>
(proxy_SendFileCompleted);
chatLabelSendFileStatus.Content = "Sending...";
}
}
}
catch (Exception ex)
{
chatTxtBoxType.Text = ex.Message.ToString();
}
finally
{
if (strm != null)
{
strm.Close();
}
}
}
}
void proxy_SendFileCompleted(object sender,
SendFileCompletedEventArgs e)
{
chatLabelSendFileStatus.Content = "File Sent";
}
protected override void OnClosing(
System.ComponentModel.CancelEventArgs e)
{
if (proxy != null)
{
if (proxy.State == CommunicationState.Opened)
{
proxy.Disconnect(this.localClient);
//dont set proxy.Close(); because
//isTerminating = true on Disconnect()
//and this by default will call
//HandleProxy() to take care of this.
}
else
{
HandleProxy();
}
}
}
private void buttonConnect_Click(object sender,
RoutedEventArgs e)
{
loginButtonConnect.IsEnabled = false;
loginLabelStatus.Content = "Connecting..";
proxy = null;
Connect();
}
void proxy_ConnectCompleted(object sender,
ConnectCompletedEventArgs e)
{
if (e.Error != null)
{
loginLabelStatus.Foreground =
new SolidColorBrush(Colors.Red);
loginTxtBoxUName.Text = e.Error.Message.ToString();
loginButtonConnect.IsEnabled = true;
}
else if (e.Result)
{
HandleProxy();
}
else if (!e.Result)
{
loginLabelStatus.Content = "Name found";
loginButtonConnect.IsEnabled = true;
}
}
private void chatButtonSend_Click(object sender,
RoutedEventArgs e)
{
Send();
}
private void chatButtonDisconnect_Click(object sender,
RoutedEventArgs e)
{
if (proxy != null)
{
if (proxy.State == CommunicationState.Faulted)
{
HandleProxy();
}
else
{
proxy.DisconnectAsync(this.localClient);
}
}
}
void chatTxtBoxType_KeyUp(object sender, KeyEventArgs e)
{
if (proxy != null)
{
if (proxy.State == CommunicationState.Faulted)
{
HandleProxy();
}
else
{
if (chatTxtBoxType.Text.Length < 1)
{
proxy.IsWritingAsync(null);
}
}
}
}
void chatTxtBoxType_KeyDown(object sender, KeyEventArgs e)
{
if (proxy != null)
{
if (proxy.State == CommunicationState.Faulted)
{
HandleProxy();
}
else
{
if (e.Key == Key.Enter)
{
Send();
}
else if (chatTxtBoxType.Text.Length < 1)
{
proxy.IsWritingAsync(this.localClient);
}
}
}
}
void chatListBoxNames_SelectionChanged(object sender,
SelectionChangedEventArgs e)
{
//If user select an online client, make a client object
//to be the receiver if the user wants to whisper him.
ListBoxItem item =
chatListBoxNames.SelectedItem as ListBoxItem;
if (item != null)
{
this.receiver = this.OnlineClients[item];
}
}
#endregion
#region IChatCallback Members
public void RefreshClients(List< WPFClient.SVC.Client> clients)
{
chatListBoxNames.Items.Clear();
OnlineClients.Clear();
foreach (SVC.Client c in clients)
{
ListBoxItem item = MakeItem(c.AvatarID, c.Name);
chatListBoxNames.Items.Add(item);
OnlineClients.Add(item, c);
}
}
public void Receive(WPFClient.SVC.Message msg)
{
foreach (SVC.Client c in this.OnlineClients.Values)
{
if (c.Name == msg.Sender)
{
ListBoxItem item = MakeItem(c.AvatarID,
msg.Sender + " : " + msg.Content);
chatListBoxMsgs.Items.Add(item);
}
}
ScrollViewer sv = FindVisualChild(chatListBoxMsgs);
sv.LineDown();
}
public void ReceiveWhisper(WPFClient.SVC.Message msg,
WPFClient.SVC.Client receiver)
{
foreach (SVC.Client c in this.OnlineClients.Values)
{
if (c.Name == msg.Sender)
{
ListBoxItem item = MakeItem(c.AvatarID,
msg.Sender + " whispers " +
receiver.Name + " : " + msg.Content);
chatListBoxMsgs.Items.Add(item);
}
}
ScrollViewer sv = FindVisualChild(chatListBoxMsgs);
sv.LineDown();
}
public void IsWritingCallback(WPFClient.SVC.Client client)
{
if (client == null)
{
chatLabelWritingMsg.Content = "";
}
else
{
chatLabelWritingMsg.Content += client.Name +
" is writing a message.., ";
}
}
public void ReceiverFile(WPFClient.SVC.FileMessage fileMsg,
WPFClient.SVC.Client receiver)
{
try
{
FileStream fileStrm = new FileStream(rcvFilesPath +
fileMsg.FileName, FileMode.Create,
FileAccess.ReadWrite);
fileStrm.Write(fileMsg.Data, 0, fileMsg.Data.Length);
chatLabelSendFileStatus.Content =
"Received file, " + fileMsg.FileName;
}
catch (Exception ex)
{
chatLabelSendFileStatus.Content = ex.Message.ToString();
}
}
public void UserJoin(WPFClient.SVC.Client client)
{
ListBoxItem item = MakeItem(client.AvatarID,
"------------ " + client.Name + " joined chat ------------");
chatListBoxMsgs.Items.Add(item);
ScrollViewer sv = FindVisualChild(chatListBoxMsgs);
sv.LineDown();
}
public void UserLeave(WPFClient.SVC.Client client)
{
ListBoxItem item = MakeItem(client.AvatarID,
"------------ " + client.Name + " left chat ------------");
chatListBoxMsgs.Items.Add(item);
ScrollViewer sv = FindVisualChild(chatListBoxMsgs);
sv.LineDown();
}
#endregion
}
}
You can automatically locate your service by saving an always-updated IP in a text file, uploading it online, and letting your client applications read the IP from the uploaded text file..
WebRequest request = WebRequest.Create("www.yourserver.com/textfile.txt");
WebResponse response = request.GetResponse();
Stream strm = response.GetResponseStream();
StreamReader reader = new StreamReader(strm);
string serviceIP = reader.ReadToEnd();
These distributed applications are built on a learning experience. I wanted to share the code to learn more and let others learn too, so if you got problems, errors, ideas, please let me know. If you liked it, please vote. Thanks.
General
News
Question
Answer
Joke
Rant
Admin
|
PermaLink |
Privacy |
Terms of Use
Last Updated: 15 Apr 2008 Editor: Smitha Vijayan |
Copyright 2008 by Islam ElDemery Everything else Copyright © CodeProject, 1999-2009 Web12 | Advertise on the Code Project |