Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Hosted-services / serverless

WPF.WCF Chat Application Simplified via P2P

4.65/5 (32 votes)
11 Nov 2007CPOL7 min read 2   16.2K  
Using the WCF netPeerTcp binding to chat without a central server.

Login Screen

Login Screen

Introduction

This article attempts to further the efforts put forth by Sacha Barber who demonstrated in his fantastic article how to use WCF to create a TCP based chat service and hook up the output to a WPF front-end. If you haven't read the article, I highly recommend you do so first in order to get a good explanation of WCF transports and building service classes. While Sacha has done an excellent job illustrating the ease of use of WCF and has furthermore illustrated an elegant pattern for wiring the service up to WPF, I recognized there were a couple of areas that could be improved and/or simplified.

Specifically I intend to show the following:

On the WCF side:

  • How to use the Peer Name Resolution Protocol (PNRP) via the netPeerTcp binding to make a truly server-less P2P chat application with a minimal amount of code. Sacha's solution uses the netTcpBinding binding and a separate service host process which all clients would need to connect to, thus creating a single point of failure risk as most client/server solutions incur.
  • How to use a WPF window to act as a service host to eliminate the need for a separate service host app.
  • How to determine whether your service host is online or offline from the peer node mesh as WCF provides an interface for this out of the box, even though their definitions for online/offline differ from what we're used to.

On the WPF side:

  • How to use style templates to round textboxes and rich text boxes.
  • How to trigger storyboard animations from code and XAML.

What I don't intend to do:

  • This isn't a production application, so you won't find a lot of try/catches. Naturally, your production code will be rock solid.
  • Since my intent is to use minimal amount of code (I'm a busy guy), you won't find the architectural elegance Sacha demonstrates in his solution. However, you will find only one project in the solution and a small amount of code to understand, as well as a lot of comments.
  • I don't want to recreate the wheel, so I won't be teaching you all the basics about WCF/WPF; I will, however, link to resources where you can learn all about the technologies I've included in this article.

Background

Peer Name Resolution Protocol (PNRP)

If you want to know all about PNRP, read the following:

From MSDN:

"In order to connect to a (peer) mesh, a peer node requires the IP addresses of other nodes. This is accomplished by contacting a resolver service. This service is given a mesh ID, and returns a list of addresses corresponding to nodes registered with that particular mesh ID. The resolver keeps a list of registered addresses, which is created by having each node in the mesh register with the service. PeerChannel supports the following two types of resolvers: Peer Name Resolution Protocol (PNRP) - This is a distributed, serverless name resolver service and is used by default. PNRP is included by default in Windows Vista, and can also be used on Windows XP SP2 by installing the Advanced Networking Pack. Any two clients running the same version of PNRP can locate each other using this protocol, provided that certain conditions are met (such as the lack of an intervening corporate firewall). Note that the version of PNRP that ships with Windows Vista is newer than the version included in the Advanced Networking Pack shipped for XP SP2. Check Microsoft Download Center for updates to PNRP on XP SP2."

Windows Communication Framework (WCF)

The absolute best resource I can recommend for WCF is to read the SDK straight through. Then, download and try the samples... then read it again. :-\

Windows Presentation Foundation (WPF)

I learned WPF by reading Windows Presentation Foundation Unleashed by Adam Nathan several times through and trying all the examples. ...and googling. Lots and lots of googling.

System requirements

I've included the source code and the release build output from my source code as downloads. The source code was developed with VS2008 B2; however, you only need .NET 3.0 to execute the binary. In order to use PNRP on Windows XP, you need SP2 and this service pack. If you are already running Vista, the binary should work out of the box.

Using the code

If you want to build/run the code or execute the binary, make sure you run more than one instance. That way, if there is nobody else on the P2P mesh, you'll be able to see another client running you can chat with. If you want to change to your own private mesh, simply change the endpointaddress attribute in the app.config to the mesh name of your choice.

Due to the simplicity of this solution, there are only three files to review:

  • Main.xaml - holds all the presentation information, including animations.
  • Main.xaml.cs - the code-behind which has the service definition and behavior code.
  • App.config - contains the service and binding configuration info.

Main.xaml

Template Styling

Of note in this file is the styling info. See how we can change the control template to make our textboxes, buttons, and other controls rounded? ...Not too shabby.

Login Screen

The code to accomplish this follows. Of notable importance is the Border tag which specifies a CornerRadius of 10:

XML
<Style x:Key="roundedTextBox" BasedOn="{x:Null}" TargetType="{x:Type TextBox}">
    <Setter Property="Foreground" 
        Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
    <Setter Property="Background" 
        Value="{DynamicResource {x:Static SystemColors.WindowBrushKey}}"/>
    <Setter Property="BorderBrush" 
        Value="{StaticResource TextBoxBorder}"/>
    <Setter Property="BorderThickness" Value="1"/>
    <Setter Property="Padding" Value="1"/>
    <Setter Property="AllowDrop" Value="true"/>
    <Setter Property="FocusVisualStyle" Value="{x:Null}"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type TextBox}">
            <Border Name="Border" CornerRadius="10" 
                        Padding="2" BorderThickness="1" 
                        Background="#FFEBECC2">
                    <ScrollViewer 
                        SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" 
                        x:Name="PART_ContentHost"/>
                </Border>
                <ControlTemplate.Triggers>
                  <Trigger Property="IsEnabled" Value="false">
                    <Setter Property="Background" TargetName="Border" 
                      Value="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"/>
                    <Setter Property="Foreground" 
                      Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
                  </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

Animation Storyboards

It will behoove you to use a tool like Microsoft Blend to create complex animations since the XAML can get a bit hairy the more elements and properties you try to animate. Regardless, you define a Storyboard as either a window or application resource like so, where we take a grid element and scale it from a height/width of 0% to 100% in a period of .42 seconds:

XML
<Window.Resources>
    <Storyboard x:Key="OnLoaded1">
        <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" 
               Storyboard.TargetName="grdLogin" 
               Storyboard.TargetProperty="(UIElement.RenderTransform).
                      (TransformGroup.Children)[0].(ScaleTransform.ScaleX)">
            <SplineDoubleKeyFrame KeyTime="00:00:00" Value="0"/>
            <SplineDoubleKeyFrame KeyTime="00:00:00.1940000" Value="1.2"/>
            <SplineDoubleKeyFrame KeyTime="00:00:00.4230000" Value="1"/>
        </DoubleAnimationUsingKeyFrames>
        <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" 
                Storyboard.TargetName="grdLogin" 
                Storyboard.TargetProperty="(UIElement.RenderTransform).
                       (TransformGroup.Children)[0].(ScaleTransform.ScaleY)">
            <SplineDoubleKeyFrame KeyTime="00:00:00" Value="0"/>
            <SplineDoubleKeyFrame KeyTime="00:00:00.1940000" Value="1.2"/>
            <SplineDoubleKeyFrame KeyTime="00:00:00.4230000" Value="1"/>
        </DoubleAnimationUsingKeyFrames>
    </Storyboard>

XAML elements are targeted by the animations via the Storyboard.TargetName attribute. The named storyboard can be triggered in XAML:

XML
<Window.Triggers>
    <EventTrigger RoutedEvent="FrameworkElement.Loaded">
        <BeginStoryboard Storyboard="{StaticResource OnLoaded1}"/>
    </EventTrigger>
    <EventTrigger RoutedEvent="ButtonBase.Click" SourceName="btnConnect">
        <BeginStoryboard x:Name="OnConnect_BeginStoryboard" 
                  Storyboard="{StaticResource OnConnect}"/>
    </EventTrigger>
</Window.Triggers>

...or in code:

C#
//start the HideConnectStatus storyboard which is a resource of this window
((Storyboard)this.Resources["HideConnectStatus"]).Begin(this);

Super easy.

Main.xaml.cs

A good place to start when writing netPeerTcp bound service apps is the sample applications that ship with the .NET 3.0 SDK. Once you review the SDK and samples, you'll see creating a P2P app consists of three steps.

Create your service contract

Here is mine:

C#
//this is our simple service contract
[ServiceContract(Namespace = "http://rolandrodriguez.net.samples.wpfchat", 
             CallbackContract = typeof(IChat))]
public interface IChat
{
    [OperationContract(IsOneWay = true)]
    void Join(string Member);

    [OperationContract(IsOneWay = true)]
    void Chat(string Member, string Message);

    [OperationContract(IsOneWay = true)]
    void Whisper(string Member, string MemberTo, string Message);

    [OperationContract(IsOneWay = true)]
    void Leave(string Member);

    [OperationContract(IsOneWay = true)]
    void InitializeMesh();

    [OperationContract(IsOneWay = true)]
    void SynchronizeMemberList(string Member);
}

Create your channel interface

This interface is required by DuplexChannelFactory to create a channel instance. The channel interface simply needs to inherit from both your service contract interface and System.ServiceModel.IClientChannel. No need to define any additional methods.

C#
//this channel interface provides a multiple
//inheritance adapter for our channel factory
//that aggregates the two interfaces need to create the channel
public interface IChatChannel : IChat, IClientChannel
{
}

Create your service host

Now we simply need a class to host our service contract. In this case, we're going to use the main WPF window like so:

C#
public partial class WindowMain: IChat
{
    ...
}

Connecting to the peer mesh is simple, and is handled by the following commented function:

C#
//this method gets called from a background thread to 
//connect the service client to the p2p mesh specified
//by the binding info in the app.config
private void ConnectToMesh()
{
    //since this window is the service behavior use it as the instance context
    m_site = new InstanceContext(this);

    //use the binding from the app.config with default settings
    m_binding = new NetPeerTcpBinding("WPFChatBinding");

    //create a new channel based off of our composite interface "IChatChannel" and the 
    //endpoint specified in the app.config
    m_channelFactory = new DuplexChannelFactory<IChatChannel>(m_site, "WPFChatEndpoint");
    m_participant = m_channelFactory.CreateChannel();

    //the next 3 lines setup the event handlers for handling online/offline events
    //in the MS P2P world, online/offline is defined as follows:
    //Online: the client is connected to one or more peers in the mesh
    //Offline: the client is all alone in the mesh
    o_statusHandler = m_participant.GetProperty<IOnlineStatus>();
    o_statusHandler.Online += new EventHandler(ostat_Online);
    o_statusHandler.Offline += new EventHandler(ostat_Offline);

    //this is an empty unhandled method on the service interface.
    //why? because for some reason p2p clients don't try to connect to the mesh
    //until the first service method call. so to facilitate connecting i call this method
    //to get the ball rolling.
    m_participant.InitializeMesh();
}

App.config

You'll notice that the function above references to WPFChatBinding and WPFChatEndPoint. Where do they come from? From the config file, of course. Here's how simple it is to use PNRP to create a P2P app:

XML
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.serviceModel>

    <client>
      <!-- chat instance participating in the mesh -->
      <endpoint name="WPFChatEndpoint"
                address="net.p2p://WPFChatMesh/rolandrodriguez.net/wpfchat"
                binding="netPeerTcpBinding"
                bindingConfiguration="WPFChatBinding"
                contract="WPFChatViaP2P.IChat">
      </endpoint>
    </client>
    <bindings>
      <netPeerTcpBinding>
        <binding name="WPFChatBinding" port="0">
          <resolver mode="Auto"/>
          <security mode="None"/>
        </binding>
      </netPeerTcpBinding>
    </bindings>
  </system.serviceModel>
</configuration>

Some points of interest:

  • See the endpoint address? net.p2p://WPFChatMesh/rolandrodriguez.net/wpfchat. Change it to the unique name of your choice, and you'll have your own private mesh for your own chat rooms. Create additional rooms or private rooms by communicating with multiple meshes.
  • You'll notice in the WPFChatBinding definition, we specify a port of "0". This simply tells the framework to use the first available port so we don't have to specify one explicitly. Easy peasy.
  • The securitymode can be set to use none, password, or x.509 certificates.

What about online/offline status?

You'll notice in the ConnectToMesh function that the IChatChannel instance m_participant exposes a property of System.ServiceModel.IOnlineStatus and that we attach a couple of events to it - Online and Offline.

These two events don't mean online/offline in terms of network connectivity status. In the PNRP world, online means that we have connected with at least one other peer in the mesh. If we are the only node in the mesh, we are considered offline. This means you can be connected to the internet but still be listed as offline if nobody else is in the mesh with you.

C#
//the next 3 lines setup the event handlers for handling online/offline events
//in the MS P2P world, online/offline is defined as follows:
//Online: the client is connected to one or more peers in the mesh
//Offline: the client is all alone in the mesh
o_statusHandler = m_participant.GetProperty<ionlinestatus>();
o_statusHandler.Online += new EventHandler(ostat_Online);
o_statusHandler.Offline += new EventHandler(ostat_Offline);

Summary

That about wraps it up. Again, please have a look at Sacha's article for a more elegantly architected solution utilizing the TCP transport and WPF. However, his solution requires the use of a TCP service address which, as we have seen, is not needed in the netPeerTcp bound world. I'm interested to see what you folks come up with now that you know how to create true server-less P2P apps with WCF and WPF. Also, if you feel this article was valuable, please log into CodeProject and vote. Thanks for reading.

History

  • 11/11/2007 - Updated source code to fix the "MSB3323: Unable to find manifest signing certificate in the certificate store" compile error.

License

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