*Edit:
A much more enhanced version with screen casting on my github using WPF Dynamic Modules.
Another (better performance) version using standard Windows Forms with added features.
Introduction
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.
Try the demo online
.NET 3.5 Framework must be installed.
- Download the demo (or alternate location)
- Run it, go to my blog to get an always-updated service IP
- Choose a name and an avatar, and hit Connect
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:
- Create a service and configure it to use TCP binding.
- Host a service and manually control it.
- Define service contracts, duplex, and data contracts.
- Implement a reliable session between the service and the client, and keep it alive for a long time.
- Increase max connections, let's say to 100.
- Handle the communication state in the client application.
- Asynchronously call service operations.
- Enable the service to be accessed online.
The application features include:
- Connect to the service and disconnect, offline from inside a network or online from the internet.
- Choose nickname and avatar.
- Participate in public chat, or whisper in private chat.
- Know if someone is writing a message.
- Know if the service stopped or disconnected.
- Reconnect after faults.
- File transfer (updated).
Updates
File transfer enabled
I really recommend these two books in case you like WCF:
Technique
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:
- Contracts: Contracts in Windows Communication Foundation provide the interoperability they need to communicate with the client. It is through contracts that clients and services agree as to the types of operations and structures they will use during the period that they are communicating back and forth. Without contracts, no work would be accomplished because no agreement would happen (Wrox Professional WCF Programming). In our case, we define the service operations that we want the service to implement, in an interface, and define the operations that the service wants the client to implement in another interface called the callback interface, this is where they both agree through contracts.
- EndPoints: The service must define at least one EndPoint and apply binding to it.
- Bindings: WCF has some predefined bindings that are really useful and can fit many cases. In our case, we are going to use
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). - Addresses: Services have to define an address for every EndPoint (we will go through base addresses later), so it can be called from, and here we have three addresses, one on TCP protocol to call the service, one on TCP protocol to publish metadata, and one on HTTP protocol to enable HTTP GET for metadata. And you (the developer) is the one who defines the service address (it doesn't relate to any other names, or paths, you just define a new address like http://localhost/blablabla/myService.svc and it will work).
Step by step, Building a WCF duplex service
Steps in points:
- Create service assembly:
- Data contracts
- Service contract
- Callback contract
- Concurrency handling
- Service implementation
- Create Host on WPF:
ServiceHost
class - Binding configuration in config file
- Binding configuration programmatically
- BaseAddresses
- Enable metadata configuration in config file
- Enable metadata configuration programmatically
- Reliable Sessions
- Max Connections
- Create Client on WPF:
- Generate proxy
- Add avatars as embedded resources
- Proxy duplex channel state handling
- Asynchronously calling service operations
- Implement callback interface
- Other stuff:
- Enable online access (port forward and firewalls)
- Automatically locating service IP
Create service assembly
using System.Linq;
using System.Text;
using System;
using System.Collections.Generic;
using System.ServiceModel;
namespace ServiceAssembly
{
public class ChatService
{
}
}
[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; }
}
}
[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.
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;
}
}
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
}
}
- Create a new folder in the C: directory and name it WCFWPFRoot.
- Fire up Visual Studio, File > New > Project, choose C# Class Library.
- Set the solution name to WCFWPFApp and the project name to ServiceAssembly; the location is the WCFWPFRoot folder.
- Rename Class1.cs to ChatService.cs.
- Remove:
- Right click the project name, choose Add Reference.. , and add a reference for
System.ServiceModel
. - Add this line to your code:
using System.ServiceModel;
. - ServiceChat.cs should look like this:
- Now, we will start to add some interfaces and other classes beside this class (
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. - To add a Data Contract, add a reference to
System.Runtime.Serialization
, and so add this line: using System.Runtime.Serialization;
. - Our data will be of two types, the first one represents a client and consists of name, avatar ID, and time; so we design the client to be like this:
- The second Data Contract is for the message. We want clients to send messages to each other; so
Message
will have information about the sender, content, and time: - The next Data Contract is the
MessageFile
to enable sending files between clients. - After we design our Data Contracts that will be exchanged through the service and clients, we have to design the service operations in an interface:
- Designing a callback interface is very easy; just want an interface to define some operations to be called on the client side.
- Time for the service implementation (of
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. - Implement the
IChat
interface: - Finally, this is the complete service assembly:
- Set debug mode to Release and build the project. ServiceAssembly.dll will be added to the Release folder under bin folder.
Create Host on WPF
< 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.
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;
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);
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;
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);
}
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");
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 />
- File > Add > New Project.., choose WPF Application, and set its name to WPFHost.
- Add a reference to
System.ServiceModel
. - Add reference and browse for the service DLL file. C: > WCFWPFRoot > WCFWPFApp > ServiceAssembly > bin > Release > ServiceAssembly.dll, in our case.
- Replace the XAML code in window1.xaml with this:
- Programmatically: Add a reference to
System.Runtime.Serialization
. - Replace the code in window1.xaml.cs with this code:
- Using the configuration file: Add New Item and choose the configuration file.
- Add this code:
Create client on WPF
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
{
public partial class Window1 : Window, SVC.IChatCallback
{
SVC.ChatClient proxy = null;
SVC.Client receiver = null;
SVC.Client localClient = null;
string rcvFilesPath = @"C:/WCF_Received_Files/";
private delegate void FaultedInvoker();
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);
}
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
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;
}
}
}
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);
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
{
SVC.Message msg = new WPFClient.SVC.Message();
msg.Sender = this.localClient.Name;
msg.Content = chatTxtBoxType.Text.ToString();
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();
}
proxy.IsWritingAsync(null);
}
}
}
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;
}
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;
}
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;
}
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;
}
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;
}
}
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)
{
DirectoryInfo dir = new DirectoryInfo(rcvFilesPath);
dir.Create();
Dictionary< int, Image> images = GetImages();
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)
{
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);
}
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)
{
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
}
}
- File > Add > New Project.., choose WPF Application, and set its name to WPFClient.
- Add a reference to
System.ServiceModel
. - Set the WPFHost project to be the startup project, start it, hit the Start button to run the service.
- Set the WPFClient project to be the startup project, and add a service reference, as in this picture:
- Now, hit OK and you will notice that a new app.config file has been added. Open it and modify the binding to enable file transfers of 64 MB, and increase max connections to 100.
- Switch to window1.xaml.cs and replace its code with this.
Other stuff
Enable online access over the Internet
- If your server (the machine that will run the service application) is inside a network, you have to forward the port that the service listens on (in our case, it is 7997). To do this, login in to your router configuration, and port forward 7997 (Help).
- You may also want to open this port manually, or create a rule for it in your firewall. I use Kaspersky Internet Security
- .
Automatically locate service IP
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();
Feedback
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.