Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

WCF / WPF Chat Application

4.93/5 (478 votes)
22 Jul 2011CPOL29 min read 7   61.3K  
How to create a peer-to-peer chat application using Windows Communication Foundation

Contents

Introduction

For those that have read some of my other CodeProject.com articles, you will probably know that I am not shy about trying out new technologies. One good thing about that is that I generally share what I learn right here and this article is one of the hardest ones I've done, IMHO. This article is about how to create a peer-to-peer chat application using Windows Communication Foundation (WCF) and also how to make it look nice using Windows Presentation Foundation (WPF).

When I first started reading about WCF, the first place I looked was the MSDN WCF Samples (which I read a lot), but they weren't that great. I also found lots of chat apps based on the MSDN versions, which were no good, as they could not return the list of users within the chat application. I wanted to create a nice WPF styled app with the list of connected chatters.

So I hunted around a bit more and eventually came across a damn fine/brilliant article by Nikola Paljetak, which I have tailored for this article. I have OK'd this with Nikola and the original article content is here. To be honest, the original article was pure brilliance (it should be mentioned that Nikola is a Professor), but it took a while for me to get what was going on, as the code wasn't commented. I have now commented all code, so I think it will still make a very nice discussion/article for those who are new to WCF/WPF. I was totally new to WCF before this article, so if I can do it, so can all of you.

So that's what this article is all about. At the end of the article, I would hope you understand at least some of the key WCF areas and possibly be inspired enough to look at the WPF side of this article, also.

A Note About the Demo App

Before I start bombarding people with the inner mechanisms of the attached WPF/WCF application, shall we have a quick look at the finished product? There are 3 main areas within the attached demo application:

A login screen

Screenshot - SignIn.png

A main window, from which the user can choose who to chat with:

Screenshot - Main.png

And a window where chatters may openly chat:

Screenshot - Chat.png

The application is based on using Visual Studio 2005 with The Visual Studio Designer for WPF installed, or using Expression BLEND and Visual Studio 2005 combination, or Wordpad if you prefer to write stuff in that. Obviously, as it's a WPF/WCF application, you will also need the .NET 3.0 Framework. This application will cover the following concepts:

  • WCF
    • The new service orientated attributes
    • The use of interfaces
    • The use of callbacks
    • Asynchronous delegates
    • Creating the proxy
  • WPF
    • Styles
    • Templates
    • Animations
    • Databinding
    • Multithreading a WPF application

However, this application is not really that orientated to WPF, as that is covered in numerous other WPF articles at The Code Project. The WPF stuff is really just a wrapper around the WCF article, which is the real guts of this article. Although there is some nice WPF stuff going on, just to make the chat application look nicer than an ordinary console application. I will, however, discuss interesting bits of the WPF implementation.

Prerequisites

  1. To run the code supplied with this article, you will need to install the May 2006 LINQ CTP, which is available here. There is a new March 2007 CTP available, but it's about 4GB for the full install (as its not just LINQ but the entire next generation of Visual Studio codenamed "Orcas") and quite fiddly, and probably going to change anyway. So, the May 2006 LINQ CTP will be OK for the purpose of what this article is trying to demonstrate.
  2. The .NET 3.0 Framework, which is available for download here.

A Brief Overview of the Demo App and What We are Trying to Achieve

In the attached demo application, we are trying to carry our the following functionality:

  1. Allow chatters to pick a name for themselves and pick an image (Avatar) that they would like to be presented by
  2. Allow the chatters to join a peer-to-peer chat
  3. Allow chatters to see who else is available to chat to
  4. Allow chatters to send private message
  5. Allow chatters to send global messages
  6. Allow chatters to leave the chat environment
  7. Make it all look pretty using WPF (not strictly a requirement for a chat app, but I like WPF, so humour me)

In order to achieve all of this I have developed 3 separate assemblies, which by the end I hope you will understand.

  • ChatService.Dll: the WCF chat server, that allows chat clients to connect and is the main message router
  • Common.Dll: is a simple serializable class which is used by both the ChatService.Dll and the WPFChatter.Dll files
  • WPFChatter.Dll: the pretty WPF wrapper around the client WCF functions

Some Key Concepts Explained

There are a number of key concepts that were mentioned earlier that need to be explained in order for the full application (which covers a lot of ground) to be understood. So I'll just explain each of these a little bit at a time, so the final application should be a little easier to explain (well that's the idea anyway).

WCF: the New Service Orientated Attributes

There are a number of new attributes that may be used with WCF to adorn our NET classes/interfaces, shown below are the ones that are used as part of the attached demo application.

ServiceContractAttribute

Indicates that an interface or a class defines a service contract in a Windows Communication Foundation (WCF) application. It has the following members:

NameDescription
CallbackContractGets or sets the type of callback contract when the contract is a duplex contract.
ConfigurationNameGets or sets the name used to locate the service in an application configuration file.
HasProtectionLevelGets a value that indicates whether the member has a protection level assigned.
NameGets or sets the name for the <portType> element in Web Services Description Language (WSDL).
NamespaceGets or sets the namespace of the <portType> element in Web Services Description Language (WSDL).
ProtectionLevelSpecifies whether the binding for the contract must support the value of the ProtectionLevel property.
SessionModeGets or sets whether sessions are allowed, not allowed or required.
TypeId (Inherited from Attribute)

See the MSDN article for more details.

OperationContractAttribute

Indicates that a method defines an operation that is part of a service contract in a Windows Communication Foundation (WCF) application. It has the following members:

NameDescription
ActionGets or sets the WS-Addressing action of the request message.
AsyncPatternIndicates that an operation is implemented asynchronously using a Begin<methodName> and End<methodName> method pair in a service contract.
HasProtectionLevelGets a value that indicates whether the messages for this operation must be encrypted, signed, or both.
IsInitiatingGets or sets a value that indicates whether the method implements an operation that can initiate a session on the server (if such a session exists).
IsOneWayGets or sets a value that indicates whether an operation returns a reply message.
IsTerminatingGets or sets a value that indicates whether the service operation causes the server to close the session after the reply message, if any, is sent.
NameGets or sets the name of the operation.
ProtectionLevelGets or sets a value that specifies whether the messages of an operation must be encrypted, signed, or both.
ReplyActionGets or sets the value of the SOAP action for the reply message of the operation.
TypeId (Inherited from Attribute)

See the MSDN article for more details

ServiceBehaviorAttribute

Specifies the internal execution behavior of a service contract implementation. It has the following members:

NameDescription
AddressFilterModeGets or sets the AddressFilterMode that is used by the dispatcher to route incoming messages to the correct endpoint.
AutomaticSessionShutdownSpecifies whether to automatically close a session when a client closes an output session.
ConcurrencyModeGets or sets whether a service supports one thread, multiple threads, or reentrant calls.
ConfigurationNameGets or sets the value used to locate the service element in an application configuration file.
IgnoreExtensionDataObjectGets or sets a value that specifies whether to send unknown serialization data onto the wire.
IncludeExceptionDetailInFaultsGets or sets a value that specifies that general unhandled execution exceptions are to be converted into a System.ServiceModel.FaultException of type System.ServiceModel.ExceptionDetail and sent as a fault message. Set this to true only during development to troubleshoot a service.
InstanceContextModeGets or sets the value that indicates when new service objects are created.
MaxItemsInObjectGraphGets or sets the maximum number of items allowed in a serialized object.
NameGets or sets the value of the name attribute in the service element in Web Services Description Language (WSDL).
NamespaceGets or sets the value of the target namespace for the service in Web Services Description Language (WSDL).
ReleaseServiceInstanceOnTransactionCompleteGets or sets a value that specifies whether the service object is released when the current transaction completes.
TransactionAutoCompleteOnSessionCloseGets or sets a value that specifies whether pending transactions are completed when the current session closes without error.
TransactionIsolationLevelSpecifies the transaction isolation level for new transactions created inside the service, and incoming transactions flowed from a client.
TransactionTimeoutGets or sets the period within which a transaction must complete.
TypeId (Inherited from Attribute)
UseSynchronizationContextGets or sets a value that specifies whether to use the current synchronization context to choose the thread of execution.
ValidateMustUnderstandGets or sets a value that specifies whether the system or the application enforces SOAP MustUnderstand header processing.

See the MSDN article or more details. Here is an example of how these new WCF attributes are used within the demo application, Service project -> ChatService.cs.

C#
[ServiceContract(SessionMode = SessionMode.Required, 
    CallbackContract = typeof(IChatCallback))]
interface IChat
{
    [OperationContract(IsOneWay = true, IsInitiating = false, 
        IsTerminating = false)]
    void Say(string msg);

    [OperationContract(IsOneWay = true, IsInitiating = false, 
        IsTerminating = false)]
    void Whisper(string to, string msg);

    [OperationContract(IsOneWay = false, IsInitiating = true, 
        IsTerminating = false)]
    Person[] Join(Person name);
    
    [OperationContract(IsOneWay = true, IsInitiating = false, 
        IsTerminating = true)]
    void Leave();
}

WCF: the Use of Interfaces

"The notion of a contract is the key to building a WCF service. Those of you that have a background in classic DCOM or COM technologies might be surprised to know that WCF contracts are expressed using interface-based programming techniques (everything old is new again!). While not mandatory, the vast amount your WCF applications will begin by defining a set of .NET interface types that are used to represent the set of members a given WCF type will support. Specifically speaking, interfaces that represent a WCF contract are termed service contracts. The classes (or structures) that implement them are termed service types."

Pro C# with .NET3.0, Apress. Andrew Troelsen

So there you are -- that's what a nice new book says -- but what does this look like to us in code? Well the actual ChatService.cs class implements the IChat interface just mentioned, and so looks like this:

C#
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession, 
    ConcurrencyMode = ConcurrencyMode.Multiple)]
public class ChatService : IChat
{

}

WCF: the Use of Callbacks

Recall earlier, when I mentioned the ServiceContractAttribute I also mentioned that one of its properties was a CallBackContract that was defined as, which allows the WCF service to call the client back. This new WCF method is very nice. I remember trying to do something like this with remoting and events back to the client, and it was not fun at all. I much prefer this method. Let's have a look. Recall that the original service contract interface was declared like this:

C#
[ServiceContract(SessionMode = SessionMode.Required, 
    CallbackContract = typeof(IChatCallback))]
interface IChat
{
....
}

Well we still need to define an interface to allow the callback to work, so an example of this may be (as done in the demo app):

C#
interface IChatCallback
{
    [OperationContract(IsOneWay = true)]
    void Receive(Person sender, string message);

    [OperationContract(IsOneWay = true)]
    void ReceiveWhisper(Person sender, string message);

    [OperationContract(IsOneWay = true)]
    void UserEnter(Person person);

    [OperationContract(IsOneWay = true)]
    void UserLeave(Person person);
}

WCF: Asynchronous Delegates

"The .NET Framework allows you to call any method asynchronously. To do this you define a delegate with the same signature as the method you want to call; the common language runtime automatically defines BeginInvoke and EndInvoke methods for this delegate, with the appropriate signatures.

The BeginInvoke method initiates the asynchronous call. It has the same parameters as the method you want to execute asynchronously, plus two additional optional parameters. The first parameter is an AsyncCallback delegate that references a method to be called when the asynchronous call completes. The second parameter is a user-defined object that passes information into the callback method. BeginInvoke returns immediately and does not wait for the asynchronous call to complete. BeginInvoke returns an IAsyncResult, which can be used to monitor the progress of the asynchronous call.

The EndInvoke method retrieves the results of the asynchronous call. It can be called any time after BeginInvoke; if the asynchronous call has not completed, EndInvoke blocks the calling thread until it completes. "

Calling Synchronous Methods Asynchronously

WCF: Creating the Proxy

In order to for the client to communicate with a WCF service, we need a proxy object. This can be quite a daunting task (and a little complicated to be honest). Luckily like a lot of things in .NET 3.0/3.5 there are tools provided to make our lives easier (you still have to know about them though), and WCF is no different. It has a little tool called "svcutil" which comes to the rescue.

So how do we create a proxy for our little WCF service (ChatService.exe for the demo app) using svcutil. Well I have read one thing that said you should be able to just start the WCF service, point svcutil at the running WCF service, and be able to create the client proxy that way. But I have to say, I could NOT get that to work at all. It seems to a common gripe, if you search the internet. So the way I got it to work was as follows:

  1. Open a visual studio command prompt, and change to the directory that contains the WCF service
  2. Run the following command line: svcutil <YOUR_SERVICE.exe>
  3. This will list a few files, namely *.wsdl, and *.xsd, and a schemas.microsoft.com.2003.10.Serialization.xsd file
  4. Next you need to run the following command line: svcutil *.wsdl *.xsd /language:C# /out:MyProxy.cs /config:app.config
  5. You now have 2 new client files, MyProxy.cs and app.config, so you can copy these to your client application

To give you an idea of what svcutil.exe produces in terms of client files, let's have a look. Here is the MyProxy.cs C# file that was auto-generated by svcutil.exe.

C#
//---------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by a tool.
//     Runtime Version:2.0.50727.312
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// </auto-generated>
//---------------------------------------------------------------------------
namespace Common
{
    using System.Runtime.Serialization;
    
    
    [System.CodeDom.Compiler.GeneratedCodeAttribute(
        "System.Runtime.Serialization", "3.0.0.0")]
    [System.Runtime.Serialization.DataContractAttribute()]
    public partial class Person : object, 
        System.Runtime.Serialization.IExtensibleDataObject
    {       
        private System.Runtime.Serialization.ExtensionDataObject 
            extensionDataField;
        
        private string ImageURLField;
        
        private string NameField;
        
        public System.Runtime.Serialization.ExtensionDataObject ExtensionData
        {
            get
            {
                return this.extensionDataField;
            }
            set
            {
                this.extensionDataField = value;
            }
        }
        
        [System.Runtime.Serialization.DataMemberAttribute()]
        public string ImageURL
        {
            get
            {
                return this.ImageURLField;
            }
            set
            {
                this.ImageURLField = value;
            }
        }
        
        [System.Runtime.Serialization.DataMemberAttribute()]
        public string Name
        {
            get
            {
                return this.NameField;
            }
            set
            {
                this.NameField = value;
            }
        }
    }
}

[System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", 
    "3.0.0.0")]
[System.ServiceModel.ServiceContractAttribute(ConfigurationName="IChat", 
CallbackContract=typeof(IChatCallback), 
    SessionMode=System.ServiceModel.SessionMode.Required)]
public interface IChat
{
    
    [System.ServiceModel.OperationContractAttribute(IsOneWay=true, 
        IsInitiating=false, Action="http://tempuri.org/IChat/Say")]
    void Say(string msg);
    
    [System.ServiceModel.OperationContractAttribute(IsOneWay=true, 
        IsInitiating=false, Action="http://tempuri.org/IChat/Whisper")]
    void Whisper(string to, string msg);
    
    [System.ServiceModel.OperationContractAttribute(
        Action=http://tempuri.org/IChat/Join, 
        ReplyAction="http://tempuri.org/IChat/JoinResponse")]
    Common.Person[] Join(Common.Person name);
    
    [System.ServiceModel.OperationContractAttribute(IsOneWay=true, 
        IsTerminating=true, IsInitiating=false, 
        Action="http://tempuri.org/IChat/Leave")]
    void Leave();
}

[System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", 
    "3.0.0.0")]
public interface IChatCallback
{
    
    [System.ServiceModel.OperationContractAttribute(IsOneWay=true, 
        Action="http://tempuri.org/IChat/Receive")]
    void Receive(Common.Person sender, string message);
    
    [System.ServiceModel.OperationContractAttribute(IsOneWay=true, 
        Action="http://tempuri.org/IChat/ReceiveWhisper")]
    void ReceiveWhisper(Common.Person sender, string message);
    
    [System.ServiceModel.OperationContractAttribute(IsOneWay=true, 
        Action="http://tempuri.org/IChat/UserEnter")]
    void UserEnter(Common.Person person);
    
    [System.ServiceModel.OperationContractAttribute(IsOneWay=true, 
        Action="http://tempuri.org/IChat/UserLeave")]
    void UserLeave(Common.Person person);
}

[System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", 
    "3.0.0.0")]
public interface IChatChannel : IChat, System.ServiceModel.IClientChannel
{
}

[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", 
    "3.0.0.0")]
public partial class ChatClient : 
    System.ServiceModel.DuplexClientBase<IChat>,
    IChat
{
    
    public ChatClient(System.ServiceModel.InstanceContext callbackInstance) : 
            base(callbackInstance)
    {
    }
    
    public ChatClient(System.ServiceModel.InstanceContext callbackInstance, 
        string endpointConfigurationName) : 
            base(callbackInstance, endpointConfigurationName)
    {
    }
    
    public ChatClient(System.ServiceModel.InstanceContext callbackInstance, 
        string endpointConfigurationName, string remoteAddress) : 
            base(callbackInstance, endpointConfigurationName, remoteAddress)
    {
    }
    
    public ChatClient(System.ServiceModel.InstanceContext callbackInstance, 
            string endpointConfigurationName, 
                System.ServiceModel.EndpointAddress remoteAddress) : 
            base(callbackInstance, endpointConfigurationName, remoteAddress)
    {
    }
    
    public ChatClient(System.ServiceModel.InstanceContext callbackInstance, 
            System.ServiceModel.Channels.Binding binding, 
                System.ServiceModel.EndpointAddress remoteAddress) : 
            base(callbackInstance, binding, remoteAddress)
    {
    }
    
    public void Say(string msg)
    {
        base.Channel.Say(msg);
    }
    
    public void Whisper(string to, string msg)
    {
        base.Channel.Whisper(to, msg);
    }
    
    public Common.Person[] Join(Common.Person name)
    {
        return base.Channel.Join(name);
    }
    
    public void Leave()
    {
        base.Channel.Leave();
    }
}

And here is the client App.Config that was auto-generated by svcutil.exe.

XML
<?xml version="1.0" encoding="utf-8"?>
<configuration>
    <system.serviceModel>
        <bindings>
            <basicHttpBinding>
                <binding name="DefaultBinding_IChat" closeTimeout="00:01:00"
                    openTimeout="00:01:00" receiveTimeout="00:10:00" 
                    sendTimeout="00:01:00"
                    allowCookies="false" bypassProxyOnLocal="false" 
                    hostNameComparisonMode="StrongWildcard"
                    maxBufferSize="65536" maxBufferPoolSize="524288" 
                    maxReceivedMessageSize="65536"
                    messageEncoding="Text" textEncoding="utf-8" 
                    transferMode="Buffered"
                    useDefaultWebProxy="true">

                    <readerQuotas maxDepth="32" 
                       maxStringContentLength="8192"
                       maxArrayLength="16384"
                       maxBytesPerRead="4096" 
                       maxNameTableCharCount="16384" />
                    <security mode="None">
                        <transport clientCredentialType="None" 
                            proxyCredentialType="None" realm="" />
                        <message clientCredentialType="UserName" 
                            algorithmSuite="Default" />
                    </security>
                </binding>

            </basicHttpBinding>
        </bindings>
        <client>
            <endpoint binding="basicHttpBinding" 
                bindingConfiguration="DefaultBinding_IChat"
                contract="IChat" name="DefaultBinding_IChat_IChat" />
        </client>
    </system.serviceModel>

</configuration>

So as you can see, these files can simply be used straightaway within your own client application to communicate with the WCF service. But wait, we are still not finished with svcutil.exe. Recall that I mentioned asynchronous delegates -- so why did I do that? Well the svcutil.exe also allows us to create asynchronous proxy code, using one of the command line switches. To do this, we use the following command line (notice the /a option):

svcutil *.wsdl *.xsd /a /language:C# /out:MyProxy.cs /config:app.config

...instead of:

svcutil *.wsdl *.xsd /language:C# /out:MyProxy.cs /config:app.config

...which we used previously. This will then change the format of the C# (or VB .NET) file we get out. What we get now for each WCF service method is an asynchronous one. So, we would get the following:

C#
[System.ServiceModel.OperationContractAttribute(IsOneWay=true,
    IsInitiating=false, Action="http://tempuri.org/IChat/Say")]
void Say(string msg);

[System.ServiceModel.OperationContractAttribute(IsOneWay=true,
    IsInitiating=false, AsyncPattern=true,
    Action="http://tempuri.org/IChat/Say")]
System.IAsyncResult BeginSay(string msg, System.AsyncCallback callback,
    object asyncState);

void EndSay(System.IAsyncResult result);

...instead of:

C#
[System.ServiceModel.OperationContractAttribute(IsOneWay=true,
    IsInitiating=false, Action="http://tempuri.org/IChat/Say")]
void Say(string msg);

Hopefully you can see where this ties in with the WCF: Asynchronous Delegates section mentioned earlier. But just to be sure, here's a more detailed description of what's going on. Using the /a switch of svcutil.exe, you can generate a proxy that contains asynchronous methods in addition to the synchronous ones. For each operation in the original contract, the asynchronous proxy and contract will contain two additional methods of this form:

The OperationContractAttribute offers the AsyncPattern Boolean property. AsyncPattern only has meaning on the client-side copy of the contract. You can only set AsyncPattern to true on a method with a Begin<Operation>( )-compatible signature. The defining contract must also have a matching method with an End<Operation>( )-compatible signature. These requirements are verified at proxy load time. What AsyncPattern does is bind the underlying synchronous method with the Begin/End pair, and correlates the synchronous execution with the asynchronous one.

When the client invokes a method of the form Begin<Operation>( ) with AsyncPattern set to true, it tells WCF not to try to directly invoke a method by that name on the service. Instead, it will use a thread from the thread pool to synchronously call the underlying method (identified by the Action name). The synchronous call will block the thread from the thread pool, not the calling client. The client will only be blocked for the slightest moment it takes to dispatch the call request to the thread pool. The reply method of the synchronous invocation is correlated with the End<Operation>( ) method.

As the attached demo application makes use of asynchronous methods for the Join operation, asynchronous delegates are using within the code to achieve this.

WCF: Configuration

As with all .NET applications, WCF applications allow the application to be configured via a configuration file. This will be discussed later on, for now you just need to know that the following items may be configured in an App.Config file for a WCF application:

  • Service addresses
  • Service type
  • Behavior configuration
  • Endpoints
  • Binding types
  • Service security

WPF: Styles / Templates

WPF styles and templates allow us to change how standard components look. This is quite a well documented feature, but what I will say is that by using a little bit of styling one is able to convert a rather plain ListView into a ListView that looks like the figure shown below. Quite nice, I think. So how is this done? Well, it's all down to styling. The figure below shows a standard ListView item which is styled and has some custom data templates assigned. Each item in the ListView is actually a Common.Person object, which will be discussed later.

Screenshot - listview.png

All that has been done is that I have applied a style to a standard .NET ListView control. Here is the XAML that does this. However, I say I am not going to dwell on these WPF features, as they are well documented and not really part of the main thrust of this article. I just wanted those people reading this that hadn't come across WPF before, to know what can be done with it.

XML
<Style x:Key="ListViewContainer" TargetType="{x:Type ListViewItem}">
   <Setter Property="FontWeight" Value="Normal"/>
   <Setter Property="Foreground" Value="#FF000000"/>

   <Setter Property="FontFamily" Value="Agency FB"/>

   <Setter Property="FontSize" Value="15"/>
   <Style.Triggers>
     <Trigger Property="IsMouseOver" Value="true">
       <Setter Property="Foreground" Value="Black" />
       <Setter Property="Background">

         <Setter.Value>

           <LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
             <LinearGradientBrush.GradientStops>
               <GradientStop Color="#D88" Offset="0"/>
               <GradientStop Color="#D31" Offset="1"/>
             </LinearGradientBrush.GradientStops>

           </LinearGradientBrush>

         </Setter.Value>
       </Setter>
       <Setter Property="Cursor" Value="Hand"/>
     </Trigger>
     <MultiTrigger>

       <MultiTrigger.Conditions>

         <Condition Property="IsSelected" Value="true" />
         <Condition Property="Selector.IsSelectionActive" Value="true" />
       </MultiTrigger.Conditions>
       <Setter Property="Background">
         <Setter.Value>

           <LinearGradientBrush StartPoint="0,0" EndPoint="0,1">

             <LinearGradientBrush.GradientStops>
               <GradientStop Color="#0E4791" Offset="0"/>
               <GradientStop Color="#468DE2" Offset="1"/>
             </LinearGradientBrush.GradientStops>
           </LinearGradientBrush>

         </Setter.Value>

       </Setter>
       <Setter Property="Foreground" Value="Black" />
     </MultiTrigger>
   </Style.Triggers>
 </Style>


 <!-- Gridview Templates  -->

 <DataTemplate x:Key="noTextHeaderTemplate"/>

 <DataTemplate x:Key="textCellTemplate">
   <TextBlock Margin="10,0,0,0" Text="{Binding}"
       VerticalAlignment="Center"/>
 </DataTemplate>

 <DataTemplate x:Key="imageCellTemplate">

   <Border CornerRadius="2,2,2,2" Width="40" Height="40"
     Background="#FFFFC934" BorderBrush="#FF000000" Margin="3,3,3,3">
     <Image HorizontalAlignment="Center" VerticalAlignment="Center"
          Width="Auto" Height="Auto"
          Source="{Binding Path=ImageURL}" Stretch="Fill"
          Margin="2,2,2,2"/>
   </Border>
 </DataTemplate>

 .....
 .....

 <ListView DockPanel.Dock="Bottom" Margin="0,-10,0,0"
       VerticalAlignment="Bottom"
       x:Name="lstChatters" SelectionMode="Single"
       ItemContainerStyle="{StaticResource ListViewContainer}"
       Background="{x:Null}" BorderBrush="#FFFFFBFB" Foreground="#FFB5B5B5"
           Opacity="1" BorderThickness="2,2,2,2"
       HorizontalAlignment="Stretch" Width="Auto" Height="Auto">

 <ListView.View>
 <GridView>
     <GridView.ColumnHeaderContainerStyle>
         <Style TargetType="GridViewColumnHeader">
             <Setter Property="Visibility" Value="Hidden" />
             <Setter Property="Height" Value="0" />

         </Style>
     </GridView.ColumnHeaderContainerStyle>
     <GridViewColumn Header="Image"
           HeaderTemplate="{StaticResource noTextHeaderTemplate}"
           Width="100" CellTemplate="{StaticResource imageCellTemplate}"/>
         <GridViewColumn DisplayMemberBinding="{Binding Path=Name}"
           Header="First Name"
           HeaderTemplate="{StaticResource textCellTemplate}" Width="100"/>
     </GridView>
 </ListView.View>

 </ListView>

WPF: Animations

Animations are another element of WPF (again well documented, so I'll not go into it too much). I have not gone too overboard with animations in the demo application, but I do use animation twice (because one simply has to if they are developing WPF stuff).

Once to load the ChatControl and once to hide the ChatControl. The only thing that is special is the way that I am using animation. They are part of the Window1.xaml but the triggers are performed in code behind logic. As this may be useful to some people I'll give a small example.

In Window1.xaml I have declared an animation as follows, the one shown below loads the ChatControl by growing it from 0 X/Y scale to 100% X/Y scale over a period of 1 second, and is triggered when a user clicks a ListView item.

XML
  <!-- Show Chat Window Animation -->
<Storyboard x:Key="showChatWindow">

  <DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
        Storyboard.TargetName="ChatControl"
        Storyboard.TargetProperty="(UIElement.RenderTransform).(
        TransformGroup.Children)[0].(ScaleTransform.ScaleX)">
    <SplineDoubleKeyFrame KeyTime="00:00:00" Value="0"/>
    <SplineDoubleKeyFrame KeyTime="00:00:001" Value="1"/>
  </DoubleAnimationUsingKeyFrames>
  <DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
        Storyboard.TargetName="ChatControl"
        Storyboard.TargetProperty="(UIElement.RenderTransform).(
        TransformGroup.Children)[0].(ScaleTransform.ScaleY)">
    <SplineDoubleKeyFrame KeyTime="00:00:00" Value="0"/>

    <SplineDoubleKeyFrame KeyTime="00:00:001" Value="1"/>
  </DoubleAnimationUsingKeyFrames>
</Storyboard>

I trigger this animation directly from code-behind. So how do I do that? Well, let's have a look at the code, shall we? It's fairly easy; the code to do that is as follows:

C#
//get Storyboard animation from window resources
((Storyboard)this.Resources["showChatWindow"]).Begin(this);

WPF: Databinding

The styled ListView that I am using within Window1.xaml utilizes data binding in order to bind a List of Person objects. This is done by using Templates as already shown in the WPF : Styles/Templates section. But just in case you're not so sure about all this WPF stuff, what actually happens is that I use a DataTemplate which specifies just how the ListView will display its data. To do that, I define the following DataTemplates and these DataTemplates include the Binding values. This allows a collection of Person objects to be bound to the ListView.

XML
<DataTemplate x:Key="textCellTemplate">

 <TextBlock Margin="10,0,0,0" Text="{Binding}" VerticalAlignment="Center"/>
</DataTemplate>

<DataTemplate x:Key="imageCellTemplate">
  <Border CornerRadius="2,2,2,2" Width="40" Height="40"
      Background="#FFFFC934" BorderBrush="#FF000000" Margin="3,3,3,3">

    <Image HorizontalAlignment="Center" VerticalAlignment="Center"
       Width="Auto"
       Height="Auto" Source="{Binding Path=ImageURL}" Stretch="Fill"
       Margin="2,2,2,2"/>

  </Border>
</DataTemplate>

WPF: Multithreading a WPF Application

Threading in WPF is quite similar to .NET 2.0/Win forms, you still have the issue of threads that are not on the same owner thread as a UI component needing to be marshaled to the correct thread. The only difference is the keywords. For example, in .NET 2.0, one would probably have done:

C#
if (this.InvokeRequired)
{
    this.Invoke(new EventHandler(delegate
    {
        progressBar1.Value = e.ProgressValue;
    }));
}
else
{
    progressBar1.Value = e.ProgressValue;
}

...while in WPF we would (and I do) use the following syntax. Note : CheckAccess() is marked as [Browsable(false)] so don't be expecting to see it using Intellisense. However, it does work.

C#
/// <summary>

/// A delegate to allow a cross UI thread call to be marshaled
/// to correct UI thread
/// </summary>
private delegate void ProxySingleton_ProxyEvent_Delegate(
    object sender, ProxyEventArgs e);

/// <summary>
/// This method checks to see if the current thread needs to be
/// marshalled to the correct (UI owner) thread. If it does a new
/// delegate is created
/// which recalls this method on the correct thread
/// </summary>
/// <param name="sender"><see
///     cref="Proxy_Singleton">ProxySingleton</see></param>

/// <param name="e">The event args</param>
private void ProxySingleton_ProxyEvent(object sender,
    ProxyEventArgs e)
{
    if (!this.Dispatcher.CheckAccess())
    {
        this.Dispatcher.BeginInvoke(DispatcherPriority.Normal,
        new ProxySingleton_ProxyEvent_Delegate(
        ProxySingleton_ProxyEvent),
        sender, new object[] { e });
        return;
    }
    //now marshalled to correct thread so proceed
    foreach (Person person in e.list)
    {
        lstChatters.Items.Add(person);
    }
    this.ChatControl.AppendText("Connected at " +
        DateTime.Now.ToString() + " with user name " +
        currPerson.Name + Environment.NewLine);
}

Well, you know what, if you've got to this point without falling asleep, I think you're ready to deal with the inner workings of the attached demo application(s). It should be child's play now, as we've covered all the key elements. There's nothing new to say, apart from how the demo app uses all this stuff (though, some of it we've already discussed). So it should be just a question of explaining it all now.

How This All Works in the DEMO Application

Well how about we start with a sequence diagram (I know UML isn't that great for distributed apps, so I've annotated it with comments, but I hope you get the general idea).

Screenshot - Join_Operations.png

I apologize that the text on this diagram is so small, but that's down to the restrictions on image sizes here at The Code Project. I'll even give you some class diagrams, for those that prefer them. Remember that there are 3 separate assemblies (ChatService / Common / WPFChatter), which I talked about earlier:

ChatService

Screenshot - ChatService.png

In order for this service to work correctly, there is a special configuration file. It could also have been done in code, but App.Config is just more flexible. So, let's have a look at the ChatService.exe App.Config, shall we? Well, it looks like this:

XML
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <appSettings>

    <add key="addr" value="net.tcp://localhost:22222/chatservice" />
  </appSettings>
  <system.serviceModel>

    <services>
      <service name="Chatters.ChatService" behaviorConfiguration="MyBehavior">
        <endpoint address=""
                  binding="netTcpBinding"
                  bindingConfiguration="DuplexBinding"
                  contract="Chatters.IChat" />

      </service>
    </services>

    <behaviors>

      <serviceBehaviors>
        <behavior  name="MyBehavior">

          <serviceThrottling maxConcurrentSessions="10000" />
        </behavior>
      </serviceBehaviors>
    </behaviors>

    <bindings>
      <netTcpBinding>

        <binding name="DuplexBinding" sendTimeout="00:00:01">
          <reliableSession enabled="true" />
          <security mode="None" />
        </binding>

      </netTcpBinding>
    </bindings>

  </system.serviceModel>
</configuration>

As you can see, this App.Config file contains all the information required to enable the service to operate. WCF supports a lot of different binding options, such as:

Binding typeDescription
BasicHttpBindingA binding that is suitable for communicating with WS-Basic Profile conformant Web Services, for example, ASMX-based services. This binding uses HTTP as the transport and Text/XML as the default message encoding.
WSHttpBindingA secure and interoperable binding that is suitable for non-duplex service contracts.
WSDualHttpBindingA secure and interoperable binding that is suitable for duplex service contracts or communication through SOAP intermediaries.
WSFederationHttpBindingA secure and interoperable binding that supports the WS-Federation protocol, enabling organizations that are in a federation to efficiently authenticate and authorize users.
NetTcpBindingA secure and optimized binding suitable for cross-machine communication between WCF applications.
NetNamedPipeBinding

A secure, reliable, optimized binding that is suitable for on-machine communication between WCF applications.

NetMsmqBindingA queued binding that is suitable for cross-machine communication between WCF applications.
NetPeerTcpBinding

A binding that enables secure, multi-machine communication.

MsmqIntegrationBindingA binding that is suitable for cross-machine communication between a WCF application and existing MSMQ applications.

For the demo application, I am using NetTcpBinding. For more information on bindings in WCF applications, you can see this link.

Common

This is a very simple class that is used by the ChatService and WPFChatter assemblies. This class represents a single chatter and may be sent over the WCF channel due the special WCF annotations that have been applied to this class. The entire class is listed below, as it's not so big:

Screenshot - Common.png
C#
using System
using System.Collections.Generic;
using System.Text;
using System.ServiceModel;
using System.Runtime.Serialization;
using System.ComponentModel;

namespace Common
{
    #region Person class
    /// <summary>
    /// This class represnts a single chat user that can participate in 
    /// this chat application
    /// This class implements INotifyPropertyChanged to support one-way 
    /// and two-way WPF bindings (such that the UI element updates when 
    /// the source has been changed dynamically)
    /// [DataContract] specifies that the type defines or implements a 
    /// data contract and is serializable by a serializer, such as 
    /// the DataContractSerializer
    /// </summary>
    [DataContract]
    public class Person : INotifyPropertyChanged
    {
        #region Instance Fields
        private string imageURL;
        private string name;
        public event PropertyChangedEventHandler PropertyChanged;
        #endregion
        #region Ctors
        /// <summary>
        /// Blank constructor
        /// </summary>

        public Person()
        {
        }
        
        /// <summary>

        /// Assign constructor
        /// </summary>
        /// <param name="imageURL">Image url to allow a picture to be 
        /// created for this chatter</param>
        /// <param name="name">The name to use for this chatter</param>

        public Person(string imageURL, string name)
        {
            this.imageURL = imageURL;
            this.name = name;
        }
        #endregion
        #region Public Properties
        /// <summary>

        /// The chatters image url
        /// </summary>
        [DataMember]
        public string ImageURL
        {
            get { return imageURL; }
            set
            {
                imageURL = value;
                // Call OnPropertyChanged whenever the property is updated
                OnPropertyChanged("ImageURL");
            }
        }
        /// <summary>
        /// The chatters Name
        /// </summary>

        [DataMember]
        public string Name
        {
            get { return name; }
            set
            {
                name = value;
                // Call OnPropertyChanged whenever the property is updated
                OnPropertyChanged("Name");
            }
        }
        #endregion
        #region OnPropertyChanged (for correct well behaved databinding)
        /// <summary>
        /// Notifies the parent bindings (if any) that a property
        /// value changed and that the binding needs updating
        /// </summary>

        /// <param name="propValue">The property which changed</param>
        protected void OnPropertyChanged(string propValue)
        {
            PropertyChangedEventHandler handler = PropertyChanged;
            if (handler != null)
            {
                handler(this, new PropertyChangedEventArgs(propValue));
            }
        }
        #endregion
    }
    #endregion
}

As you can see, this class has some special attributes which still haven't been talked about. It also inherits from a strange interface. So what's that all about, then? Well, [DataContract] specifies that the type defines or implements a data contract and is serializable by a serializer such as DataContractSerializer. The INotifyPropertyChanged interface is a special WPF interface that allows the class to notify and binding container of a change in a value. Thus, when a property is changed, the binding container will be informed. This applies to the Window1.xaml ListView in my case, as described in the WPF: Databinding section.

WPFChatter

Screenshot - WPFChatter.png

The app.config file and WCF proxy were created using the WCF scvutil.exe tool, as mentioned in the WCF: Creating the Proxy section. The finished WPF application is as shown below:

Screenshot - Main2.png

Screenshot - Main3.png

What I'm going to do now is explain a bit about how all the WCF/WPF stuff works in conjunction with each other, using the attached demo app. I think the best place to start would be to describe the actual ChatService (the server end) and why it's adorned the way it is. I will not go into all the inner workings of the ChatService.cs file just yet, as this will be visited later.

ChatService.cs

C#
[ServiceContract(SessionMode = SessionMode.Required,
    CallbackContract = typeof(IChatCallback))]
interface IChat
{
    [OperationContract(IsOneWay = true, IsInitiating = false,
        IsTerminating = false)]
    void Say(string msg);

    [OperationContract(IsOneWay = true, IsInitiating = false,
        IsTerminating = false)]
    void Whisper(string to, string msg);

    [OperationContract(IsOneWay = false, IsInitiating = true,
        IsTerminating = false)]
    Person[] Join(Person name);

    [OperationContract(IsOneWay = true, IsInitiating = false,
        IsTerminating = true)]
    void Leave();
}

interface IChatCallback
{
    [OperationContract(IsOneWay = true)]
    void Receive(Person sender, string message);

    [OperationContract(IsOneWay = true)]
    void ReceiveWhisper(Person sender, string message);

    [OperationContract(IsOneWay = true)]
    void UserEnter(Person person);

    [OperationContract(IsOneWay = true)]
    void UserLeave(Person person);
}

public class ChatEventArgs : EventArgs
{
    public MessageType msgType;
    public Person person;
    public string message;
}

[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession,
    ConcurrencyMode = ConcurrencyMode.Multiple)]
public class ChatService : IChat
{
...
}

There are several things to note here:

  1. There is an IChat interface which is implemented by the ChatService (server end) class. This is in order that the ChatService implements a common Chat interface that is known at compile time. Each of the interface methods are adorned with new WCF [OperationContract] attributes. I discussed all the possible values for this attribute earlier in this article. The general rule of thumb here is that if the method needs to return a value of IsOneWay = true. Otherwise, it will be IsOneWay = false. All these methods are also marked with the IsInitiating and IsTerminating properties. As the Join method is initialising the service, this is marked with IsInitiating = true and as the Leave method is quitting the service, this is marked with IsTerminating = true.
  2. There is also another IChatCallBack interface included within the ChatService class. This is to allow the ChatService to make callbacks to the clients using a well-known interface that is also known at compile time. It can be seen that the server-implemented IChat interface actually includes extra annotation to state whether there is a callback required. Let's have a look at [ServiceContract(SessionMode = SessionMode.Required, CallbackContract = typeof(IChatCallback))]. Notice how the CallbackContract is specified as being of type IChatCallback. That's how the WCF service knows how to communicate back to the client. Obviously, just having the CallbackContract property and not the actual IChatCallback interface is not enough. You need both.
  3. Lastly, we need to note that the actual ChatService is also adorned with extra attributes that allow the service itself to be managed correctly. I am using InstanceContextMode = InstanceContextMode.PerSession. PerSession instructs the service application to create a new service object when a new communication session is established between a client and the service application. Subsequent calls in the same session are handled by the same object. I am also using ConcurrencyMode = ConcurrencyMode.Multiple. A value of Multiple means that service objects can be executed by multiple threads at any one time. In this case, you must ensure thread safety.

So that's the ChatService's WCF attributes explained, but what about the client-side code? Remember that we used svcutil.exe to create a proxy object the way I previously described. So I am not going to go into that in too much detail. However, the full proxy code (ChatService.cs, in the WPFChatter project in the demo code) is shown below. With the descriptions just given, and the previously mentioned attribute tables, you should be able to see what's going on I hope.

C#
using Common;

/// <summary>
/// This class was auto generated by the svcutil.exe utility. 
/// The www.codeprject.com article will explain how this class
/// was generated, for those readers that just need to know. 
/// Basically, anyone like me.
/// </summary>
#region IChat interface
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", 
    "3.0.0.0")]
[System.ServiceModel.ServiceContractAttribute(CallbackContract = typeof(
    IChatCallback), SessionMode = System.ServiceModel.SessionMode.Required)]
public interface IChat
{
    [System.ServiceModel.OperationContractAttribute(AsyncPattern = true, 
        Action = http://tempuri.org/IChat/Join
        ReplyAction = "http://tempuri.org/IChat/JoinResponse")]
    System.IAsyncResult BeginJoin(Person name, System.AsyncCallback callback,
        object asyncState);

    Person[] EndJoin(System.IAsyncResult result);

    [System.ServiceModel.OperationContractAttribute(IsOneWay = true, 
        IsInitiating = false, Action = "http://tempuri.org/IChat/Leave")]
    void Leave();

    [System.ServiceModel.OperationContractAttribute(IsOneWay = true, 
        IsInitiating = false, Action = "http://tempuri.org/IChat/Say")]
    void Say(string msg);

    [System.ServiceModel.OperationContractAttribute(IsOneWay = true, 
        IsInitiating = false, Action = "http://tempuri.org/IChat/Whisper")]
    void Whisper(string to, string msg);
}
#endregion
#region IChatCallback interface 
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", 
    "3.0.0.0")]
public interface IChatCallback
{
    [System.ServiceModel.OperationContractAttribute(IsOneWay = true, 
        Action = "http://tempuri.org/IChat/Receive")]
    void Receive(Person sender, string message);

    [System.ServiceModel.OperationContractAttribute(IsOneWay = true, 
        Action = "http://tempuri.org/IChat/ReceiveWhisper")]
    void ReceiveWhisper(Person sender, string message);

    [System.ServiceModel.OperationContractAttribute(IsOneWay = true, 
        Action = "http://tempuri.org/IChat/UserEnter")]
    void UserEnter(Person person);

    [System.ServiceModel.OperationContractAttribute(IsOneWay = true, 
        Action = "http://tempuri.org/IChat/UserLeave")]
    void UserLeave(Person person);
}
#endregion
#region IChatChannel interface
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", 
    "3.0.0.0")]
public interface IChatChannel : IChat, System.ServiceModel.IClientChannel
{
}
#endregion
#region ChatProxy class
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", 
    "3.0.0.0")]
public partial class ChatProxy : System.ServiceModel.DuplexClientBase<IChat>,
    IChat
{
    public ChatProxy(System.ServiceModel.InstanceContext callbackInstance)
        :
            base(callbackInstance)
    {
    }

    public ChatProxy(System.ServiceModel.InstanceContext callbackInstance, 
        string endpointConfigurationName)
        :
            base(callbackInstance, endpointConfigurationName)
    {
    }

    public ChatProxy(System.ServiceModel.InstanceContext callbackInstance, 
        string endpointConfigurationName, string remoteAddress)
        :
            base(callbackInstance, endpointConfigurationName, remoteAddress)
    {
    }

    public ChatProxy(System.ServiceModel.InstanceContext callbackInstance, 
        string endpointConfigurationName, 
        System.ServiceModel.EndpointAddress remoteAddress)
        :
            base(callbackInstance, endpointConfigurationName, remoteAddress)
    {
    }

    public ChatProxy(System.ServiceModel.InstanceContext callbackInstance, 
        System.ServiceModel.Channels.Binding binding, 
        System.ServiceModel.EndpointAddress remoteAddress)
        :
            base(callbackInstance, binding, remoteAddress)
    {
    }

    public System.IAsyncResult BeginJoin(Person name, 
        System.AsyncCallback callback, object asyncState)
    {
        return base.Channel.BeginJoin(name, callback, asyncState);
    }

    public Person[] EndJoin(System.IAsyncResult result)
    {
        return base.Channel.EndJoin(result);
    }

    public void Leave()
    {
        base.Channel.Leave();
    }

    public void Say(string msg)
    {
        base.Channel.Say(msg);
    }

    public void Whisper(string to, string msg)
    {
        base.Channel.Whisper(to, msg);
    }
}
#endregion 

OK, so now that I've described the skeleton, boiler plate code that both the Server/Client use to carry out their communications, let's dive in to see how the actual code utilizes the ChatService (server end) and the client side proxy object also called ChatService. I think the best way to do this is to list out one of the operations (such as Join) and follow it from start to finish, from client to server.

One thing to note though, before I start going into this stuff fully, is that I needed to be able to call the client side proxy object methods from several different windows/controls within the attached WPF project. To this end, there is a class called Proxy_Singleton.cs that is really just a singleton wrapper object around all the different operations that need to be carried out for the ChatService to work correctly. It simply enables the client code to obtain the current singleton object and call its methods without having to worry about instantiating a new proxy object, or keeping track of the correct proxy object.

Join

For the Join operation, the following sequence is observed

  1. The user enters a name and assigns an image on the SignInControl.xaml control, where a new Person object is created which is accessible via a public property. This control then raises the AddButtonClickEvent where the container Window1.xaml is listening to this event. The container, Window1.xaml, then hides the SignInControl.xaml control. It grabs the new Person object that was created by the SignInControl.xaml and calls the Connect method of the Proxy_Singleton.cs, passing it the new Person object.
  2. Both Window1.xaml and ChatControl.xaml are then wired up to subscribe to the Proxy_Singleton.cs ProxyEvent event and ProxyCallBackEvent event, such that when the Proxy_Singleton.cs receives a callback message from the server (another chatter), it will fire these events and all listening parties will know what to do with the received data.
  3. The Proxy_Singleton.cs Connect method uses an asynchronous connection to call the Join method on the ChatService (local proxy). Recall that I told you about asynchronous delegates and how the asynchronous connections were handled by the svcutil.exe tool.
  4. The Proxy_Singleton.cs Connect method eventually calls (through WCF magic, tcpBninding actually) the Join method of the ChatService (server). This will return the new List of chatters to all connected chatters. The server does this by maintaining a multicast delegate that holds a list of delegates of the type public delegate void ChatEventHandler(object sender, ChatEventArgs e);. There is 1 each per connected chatter. Basically what happens is that whenever one of the IChat methods on the server is called, the delegate for the client may also called (dependant on message type). Obviously, some messages (such as Whisper) are private. As such, only the particular delegate for the receiver of the message is invoked. Nevertheless, the mechanism is the same. When a message is received by the server, the message type is examined. If it's private, only the particular delegate for the receiver (client) of the message is invoked. Otherwise, all attached client delegates are invoked. When the delegate is invoked by the server, it will use the IChatCallback interface to make the call back to the client. As the Proxy_Singleton.cs Connect class implements IChatCallback, it is capable of receiving and dealing with the callback messages. It will generally make the callback messages available to other objects via internally generated events which, as mentioned earlier, Window1.xaml and ChatControl.xaml have subscribed to.

This probably needs some code snippets to help explain, so let's start with the Proxy_Singleton.cs Connect code:

C#
///<summary>
/// Begins an asynchronous join operation on the
/// underlying <see cref="ChatProxy">ChatProxy</see>

/// which will call the OnEndJoin() method on completion
/// </summary>
/// <param name="p">The <see cref="Common.Person">chatter</see>
/// to try and join with</param>
public void Connect(Person p)
{
    InstanceContext site = new InstanceContext(this);
    proxy = new ChatProxy(site);
    IAsyncResult iar = proxy.BeginJoin(p,
        new AsyncCallback(OnEndJoin), null);
}

/// <summary>

/// Is called as a callback from the asynchronous call,
/// so simply get the list of
/// <see cref="Common.Person">Chatters</see> that will
/// be yielded as part of the Asynch Join call
/// </summary>
/// <param name="iar">The asnch result</param>
private void OnEndJoin(IAsyncResult iar)
{
    try
    {
        Person[] list = proxy.EndJoin(iar);
        HandleEndJoin(list);
    }
    catch (Exception e)
    {
        MessageBox.Show(e.Message, "Error",
            MessageBoxButton.OK, MessageBoxImage.Error);
    }
}
/// <summary>
/// If the input list is not empty, then call the
/// OnProxyEvent() event, to raise the event for subscribers
/// </summary>
/// <param name="list">The list of
/// <see cref="Common.Person">Chatters</see> </param>
private void HandleEndJoin(Person[] list)
{

    if (list == null)
    {
        MessageBox.Show("Error: List is empty", "Error",
            MessageBoxButton.OK, MessageBoxImage.Error);
        ExitChatSession();
    }
    else
    {
        ProxyEventArgs e = new ProxyEventArgs();
        e.list = list;
        OnProxyEvent(e);
    }
}

/// <summary>
/// Raises the event for connected subscribers
/// </summary>
/// <param name="e"><see cref=
///     "ProxyCallBackEventArgs">ProxyCallBackEventArgs</see>
/// event args</param>
protected void OnProxyCallBackEvent(ProxyCallBackEventArgs e)
{
    if (ProxyCallBackEvent != null)
    {
        // Invokes the delegates.
        ProxyCallBackEvent(this, e);
    }
}
/// <summary>
/// Raises the event for connected subscribers
/// </summary>
/// <param name="e"><see cref=
///     "ProxyEventArgs">ProxyEventArgs</see>
/// event args</param>
protected void OnProxyEvent(ProxyEventArgs e)
{
    if (ProxyEvent != null)
    {
        // Invokes the delegates.
        ProxyEvent(this, e);
    }
}
.....
/// <summary>

/// New chatter entered chat room, so call the
/// internal UserEnterLeave() method passing it the input parameters
/// and the <see cref="CallBackType">CallBackType.UserEnter</see>
/// type
/// </summary>
/// <param name="sender">The <see cref=
///     "Common.Person">current chatter</see></param>

/// <param name="message">The message</param>
public void UserEnter(Person person)
{
    ....
}

So this ends up calling the Join method in the ChatService (server), which then uses IChatCallback to call the client back using the appropriate IChatCallback method. For example:

  • A Join method in the ChatService will result in an IChatCallback UserEnter method being called.
  • A Leave method in the ChatService will result in an IChatCallback UserLeave method being called.
  • A Say method in the ChatService will result in an IChatCallback Receive method being called.
  • A Whisper method in the ChatService will result in an IChatCallback ReceiveWhisper method being called.

So let's have a look at the Join/UserEnter operation at the server end:

C#
    /// <summary>
    /// Takes a <see cref="Common.Person">Person</see> and allows them
    /// to join the chat room, if there is not already a chatter with
    /// the same name
    /// </summary>

    /// <param name="person"><see cref=
    ///    "Common.Person">Person</see> joining</param>
    /// <returns>An array of <see cref="Common.Person">Person</see>
    ///     objects</returns>

    public Person[] Join(Person person)
    {
        bool userAdded = false;
        //create a new ChatEventHandler delegate, pointing to the
        //MyEventHandler() method
        myEventHandler = new ChatEventHandler(MyEventHandler);

        //carry out a critical section that checks to see if the new
        //chatter name is already in use, if its not allow the new
        //chatter to be added to the list of chatters, using the
        //person as the key, and the
        //ChatEventHandler delegate as the value, for later invocation
        lock (syncObj)
        {
            if (!checkIfPersonExists(person.Name) && person != null)
            {
                this.person = person;
                chatters.Add(person, MyEventHandler);
                userAdded = true;
            }
        }

        //if the new chatter could be successfully added, get a
        //callback instance create a new message, and broadcast it to
        //all other chatters, and then return the list of al chatters
        //such that connected clients may show a list of all the chatters
        if (userAdded)
        {
            callback =
             OperationContext.Current.GetCallbackChannel<IChatCallback>();
            ChatEventArgs e = new ChatEventArgs();
            e.msgType = MessageType.UserEnter;
            e.person = this.person;
            BroadcastMessage(e);
            //add this newly joined chatters ChatEventHandler delegate,
            //to the global multicast delegate for invocation
            ChatEvent += myEventHandler;
            Person[] list = new Person[chatters.Count];
            //carry out a critical section that copy all chatters to
            //a new list
            lock (syncObj)
            {
                chatters.Keys.CopyTo(list, 0);
            }
            return list;
        }
        else
        {
            return null;
        }
    }
....

    /// <summary>
    /// This method is called when ever one of the chatters
    /// ChatEventHandler delegates is invoked. When this method
    /// is called it will examine the events ChatEventArgs to see
    /// what type of message is being broadcast, and will then
    /// call the corresponding method on the clients callback interface
    /// </summary>
    /// <param name="sender">the sender, which is not used</param>

    /// <param name="e">The ChatEventArgs</param>
    private void MyEventHandler(object sender, ChatEventArgs e)
    {
        try
        {
            switch (e.msgType)
            {
                case MessageType.Receive:
                    callback.Receive(e.person, e.message);
                    break;
                case MessageType.ReceiveWhisper:
                    callback.ReceiveWhisper(e.person, e.message);
                    break;
                case MessageType.UserEnter:
                    callback.UserEnter(e.person);
                    break;
                case MessageType.UserLeave:
                    callback.UserLeave(e.person);
                    break;
            }
        }
        catch
        {
            Leave();
        }
    }
.....

    /// <summary>
    ///loop through all connected chatters and invoke their
    ///ChatEventHandler delegate asynchronously, which will firstly call
    ///the MyEventHandler() method and will allow a Asynch callback to
    ///call
    ///the EndAsync() method on completion of the initial call
    /// </summary>
    /// <param name="e">The ChatEventArgs to use to send to all
    /// connected chatters</param>

    private void BroadcastMessage(ChatEventArgs e)
    {

        ChatEventHandler temp = ChatEvent;

        //loop through all connected chatters and invoke their
        //ChatEventHandler delegate asynchronously, which will firstly
        //call the MyEventHandler() method and will allow a asynch
        //callback to call the EndAsync() method on completion of the
        //initial call
        if (temp != null)
        {
            foreach (ChatEventHandler handler in temp.GetInvocationList())
            {
                handler.BeginInvoke(this, e, new AsyncCallback(EndAsync),
                    null);
            }
        }
    }

Say

The methodology for the Say operation is similar to the Join operation described above, with the following key differences:

  1. The Say operation is NOT called asynchronously
  2. A Say method in the ChatService will result in an IChatCallback Receive method being called
  3. The Say operation is NOT private, so ALL client delegates (at the server) will be invoked

Whisper

The methodology for the Whisper operation is similar to the Join operation, with the following key differences:

  1. The Whisper operation is NOT called asynchronously
  2. A Whisper method in the ChatService will result in an IChatCallback ReceiveWhisper method being called
  3. The Whisper operation is private, so only the particular recipients client delegate (at the server) will be invoked

Leave

The methodology for the Leave operation is similar to the Join operation, with the following key differences:

  1. The Leave operation is NOT called Asynchronously
  2. A Leave method in the ChatService will result in an IChatCallback UserLeave method being called
  3. The Leave operation is NOT private, so ALL client delegates (at the server) will be invoked

That's It

I hope this article shows that all these new Microsoft technologies can actually work together quite well. I have to say, I think this application looks cool. I have been messing around with WPF for a while, nothing serious, but with this one, I tried to have a go. So I hope some of you like it.

So What Do You Think?

I would just like to ask, if you liked the article, please vote for it. Also, leave some comments, as it lets me know if the article was at the right level or not, and whether it contained what people need to know.

Conclusion

I have enjoyed writing this WPF/WCF article and I have now become hopelessly addicted to .NET 3.0/3.5. So if you want to keep a work/life balance, I would suggest you stay far away from .NET 3.0/3.5 if at all possible, because once it's got you in its little tentacles, there really is no escape. "Resistance Is Futile. You Will Be Assimilated."

That said, once you get your head around the vast learning curve that is .NET 3.0, it's really much better IMHO than previous .NET 2.0 offerings.

History

  • v1.0 23/07/07: Initial issue
  • 22/7/11: Updated download demo zip

Other Stuff

I would personally like to thank Josh Smith for his help advice about threading in WPF and his unstoppable WPFing that seems to be popping up everywhere. He is a great source of knowledge for all things WPF, so spend some time visiting his Blog he has lots of good resources there.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)