次の方法で共有


How to create a peer-to-peer chat application using WPF and wcf in Visual Studio 2010

Introduction

In this post, I’ll demonstrate a chat application and provide complete source code and instructions for creating it. It can then serve as a great starting point for being modified to create any sort of peer-to-peer application with a flexible GUI interface.

Each one of those buzzwords in the title is important for this post, and I’ve tried to include enough information for this to be a helpful introduction to any one of them:

  • Peer-to-peer: No centralized server is required. each individual node in the chat network acts as its own client and server for communicating with its neighbors.
  • WPF (Windows Presentation Foundation): This is the library that will provide all the GUI elements (window, textbox, etc.) and handles the user interaction events (keystrokes, mouse clicks, etc.).
  • WCF (Windows Communication Foundation): This is the library we’ll use for all the networking functionality.
  • Visual Studio 2010: In principle, you could create this program in any development environment, but I’ll show you all the right buttons to press in Visual Studio, and we’ll take advantage of the GUI builder (the design view for WPF) and the features for specifying WPF and WCF code via XML.

Our application will consist of a single Visual Studio solution that contains two projects [see “Introduction to Solutions, Projects, and Items”]. The first project we’ll create will define the GUI front-end and the second will define all the application logic and networking functionality.

 

The GUI Front End

To begin, open Visual Studio 2010 and go to “File –> New –> Project”.

Use the “WPF Application” template to create a project with the name “ChatGUI” inside a new solution named “Chat”, as seen here:

WPF Template

The WPF Application template will open to a GUI builder (“designer mode” view) of a file called MainWindow.xaml [see “XAML Overview” ]. Click the “Toolbox” tab on the left side of the screen to expand the view of GUI elements, and drag two TextBox controls onto the screen. We’ll use one of the TextBox widgets to be the display screen where we see all the text written by our friends during a chat session, and we’ll use the other TextBox as the entry field for adding to the chat conversation. Rather than adding a submit button, we’ll just pay attention to the Enter key when a user is typing in the entry field. There are also plenty of other interesting controls, and we may come back in a later post to demonstrate some of them, like the RichTextBox for display formatted text with colors and images interspersed [see “WPF Controls” ].

MainWindow.xaml 1

Next, in the XAML editing pane on the bottom portion of the screen, let’s adjust the size, placement, and names of our TextBox elements, by changing the XAML code to read:

Listing 1: MainWindow.xaml

<Window x:Class="ChatGUI.MainWindow"
        xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="35*" />
            <RowDefinition Height="35" />
        </Grid.RowDefinitions>
        <TextBox Height="Auto" HorizontalAlignment="Stretch" Margin="6,6,6,6" Name="textBoxChatPane" VerticalAlignment="Stretch" Width="Auto" Grid.Row="0" />
        <TextBox Height="23" HorizontalAlignment="Stretch" Margin="6,6,6,6" Name="textBoxEntryField" VerticalAlignment="Stretch" Width="Auto" Grid.Row="1" KeyDown="textBoxEntryField_KeyDown" />
    </Grid>
</Window>

If you type this in manually (as opposed to cutting and pasting the whole thing), you’ll notice the editor’s Intellisense features helping you. For example, fields like HorizontalAlignment have a finite number of values that you can choose from. Similarly, there are a predefined set of events that a TextBox can respond to. We’ve chosen to bind the “KeyDown” event on the “textBoxEntryField” to a new method, and in fact I even let Visual Studio choose the name of that method for me (as you type KeyDown=””, a drop-down box appears, allowing you to select <New Event Handler>). Now click on the MainWindow.xaml.cs tab. This brings us to the C# “code-behind” for the GUI. Notice that an empty textBoxEntryField_KeyDown method has already been created for us in the MainWindow class (you can also get to this by highlighting “textBoxEntryField_KeyDown” in the XAML editor window, clicking the right mouse button, and choosing “Navigate to Event Handler”).

At this point, you’ve already created enough code to build the application and pop up a GUI window. Try it out by hitting F5, and confirm that everything builds without errors, and when it runs a new window appears that looks something like this:

image

Assuming that worked, great. Now kill your application, either by closing the window or by hitting shift-F5 back in Visual Studio.

By the time we’re done, our MainWindow.xaml.cs code will have three methods: the MainWindow constructor, the textBoxEntryField_KeyDown event handler, and a DisplayMessage method that we’ll use for displaying text in the textBoxChatPane widget. We’ll also have one private member variable that will act as a handle to an object that represents all the back-end functionality. But before we go any further on the GUI implementation, let’s switch gears and create the back-end system.

 

The Back End Communication System

In the Solution Explorer pane, right-click the “Solution ‘Chat’” line, and go to “Add –> New Project… –> Class Library” and call the new project ChatBackend.

image

This project is going to contain all the communication code, and we could have used one of the WCF templates instead of just a generic C# class library – these are worth playing around with, and are perfect for creating a client/server app, but none of them happen to be exactly suited for the peer-to-peer configuration we’re going to create, so we’ll do it by hand.

In this project, we’ll end up with two C# files: the IChatBackend interface that defines the set of methods to be supported by a backend implementation, and the ChatBackend class with the actual logic. First we’ll create the interface. Right-click on “ChatBackend” in the Solution Explorer pane, and select “Add –> New Item –> Interface” and call it IChatBackend.cs.

image

Before you start typing in code for IChatBackend.cs, you should add the necessary references. We’ll be using WCF libraries, and you will need to specify references to System.ServiceModel and System.Runtime.Serialization. In the Solution Explorer pane, right-click on the References node under the ChatBackend project. Go to “Add Reference…”, click on the .NET tab, and add System.ServiceModel, then repeat the process for System.Runtime.Serialization.

In this code, we’ll say that any class which implements the IChatBackend interface has to define two methods: DisplayMessage and SendMessage. You can think of the usage of DisplayMessage in a remote procedure call sort of way – our friends on the peer-to-peer network will call our DisplayMessage method when they want us to display something. We will use our own SendMessage method the call the DisplayMessage methods owned by all of our friends.

We’ll say that DisplayMessage does not return any values, and is a OneWay communication mechanism (we tell our friends to display something, but they don’t have to respond).

In this file, we’ll also define a class called CompositeType, which is capable of holding two string values (a username and a message), and we’ll say that the DisplayMessage method should always be called with one of these CompositeType objects. This is not strictly necessary – we could have simply defined DisplayMessage to take two strings – but when you start to build on this application to send more complicated data objects over the network, the CompositeType will come in handy.

Lastly, we’ll define a delegate type DisplayMessageDelegate. This exists to help us separate the GUI code from the application logic and communication code. The idea is that the GUI will have some mechanism for showing a message on the screen, but the backend shouldn’t have to be aware of the implementation details. When we start up the system, the GUI code will use delegates to pass a function handle to the backend, which can be called whenever the backend wants the GUI to display a message to the user.

All of the code described above is shown here in the listing for IChatBackend.cs:

Listing 2: IChatBackend.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;

namespace ChatBackend
{
    [ServiceContract]
    public interface IChatBackend
    {
        [OperationContract(IsOneWay = true)]
        void DisplayMessage(CompositeType composite);

        void SendMessage(string text);
    }

    [DataContract]
    public class CompositeType
    {
        private string _username = "Anonymous";
        private string _message = "";

        public CompositeType() { }
        public CompositeType(string u, string m)
        {
            _username = u;
            _message = m;
        }

        [DataMember]
        public string Username
        {
            get { return _username; }
            set { _username = value; }
        }

        [DataMember]
        public string Message
        {
            get { return _message; }
            set { _message = value; }
        }
    }

    public delegate void DisplayMessageDelegate(CompositeType data);
}

 

The next step is to create the implementation of our ChatBackend. When we created the ChatBackend project, it generated an empty Class1.cs file, which you can “File –> Save Class1.cs As…” ChatBackend.cs to rename it.

The ChatBackend class will have the following methods inside it:

  • A constructor that takes a DisplayMessageDelegate as an argument. As discussed above, this delegate is a callback that allows the backend to request that something be displayed in the GUI, without having to know anything about the GUI implementation.
  • DisplayMessage(), which calls the DisplayMessageDelegate.
  • SendMessage(), which uses the communication channel to call the DisplayMessage() methods provided by each of our friends.
  • StartService(), which is required as part of the WCF code to create the communication channel.
  • StopService(), which closes the communication channel gracefully.

Here’s the code for the implementation:

 

Listing 3: ChatBackend.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;

namespace ChatBackend
{
    [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]
    public class ChatBackend : IChatBackend
    {

        #region Everything we need to receive messages

        DisplayMessageDelegate _displayMessageDelegate = null;

        /// <summary>
        /// The default constructor is only here for testing purposes.
        /// </summary>
        private ChatBackend()
        {
        }

        /// <summary>
        /// ChatBackend constructor should be called with a delegate that is capable of displaying messages.
        /// </summary>
        /// <param name="dmd">DisplayMessageDelegate</param>
        public ChatBackend(DisplayMessageDelegate dmd)
        {
            _displayMessageDelegate = dmd;
            StartService();
        }

        /// <summary>
        /// This method gets called by our friends when they want to display a message on our screen.
        /// We're really only returning a string for demonstration purposes ... it might be cleaner
        /// to return void and also make this a one-way communication channel.
        /// </summary>
        /// <param name="composite"></param>
        public void DisplayMessage(CompositeType composite)
        {
            if (composite == null)
            {
                throw new ArgumentNullException("composite");
            }
            if (_displayMessageDelegate != null)
            {
                _displayMessageDelegate(composite);
            }
        }

        #endregion // Everything we need to receive messages

        #region Everything we need for bi-directional communication

        private string _myUserName = "Anonymous";
        private ServiceHost host = null;
        private ChannelFactory<IChatBackend> channelFactory = null;
        private IChatBackend _channel;

        /// <summary>
        /// The front-end calls the SendMessage method in order to broadcast a message to our friends
        /// </summary>
        /// <param name="text"></param>
        public void SendMessage(string text)
        {
            if (text.StartsWith("setname:", StringComparison.OrdinalIgnoreCase))
            {
                _myUserName = text.Substring("setname:".Length).Trim();
                _displayMessageDelegate(new CompositeType("Event", "Setting your name to " + _myUserName));
            }
            else
            {
                // In order to send a message, we call our friends' DisplayMessage method
                _channel.DisplayMessage(new CompositeType(_myUserName, text));
            }
        }

        private void StartService()
        {
            host = new ServiceHost(this);
            host.Open();
            channelFactory = new ChannelFactory<IChatBackend>("ChatEndpoint");
            _channel = channelFactory.CreateChannel();

            // Information to send to the channel
            _channel.DisplayMessage(new CompositeType("Event", _myUserName + " has entered the conversation."));

            // Information to display locally
            _displayMessageDelegate(new CompositeType("Info", "To change your name, type setname: NEW_NAME"));
        }

        private void StopService()
        {
            if (host != null)
            {
                _channel.DisplayMessage(new CompositeType("Event", _myUserName + " is leaving the conversation."));
                if (host.State != CommunicationState.Closed)
                {
                    channelFactory.Close();
                    host.Close();
                }
            }
        }

        #endregion // Everything we need for bi-directional communication

    }
}

 

Now that we have the ChatBackend in place, let’s go back to the GUI code to wire everything together.

The GUI contains code that will be instantiating a ChatBackend object and calling its methods, so the ChatGUI project will need to contain a reference to the ChatBackend project. Specify this by right-clicking the References item under the ChatGui project in the Solution Explorer. Select “Add Reference…” and in the Projects tab, select ChatBackend.

Now start editing MainWindow.xaml.cs under the ChatGui project. We’ll need a private member variable to keep a reference to a ChatBackend object, and we’ll need to fill out the constructor, the event handler and the DisplayMessage methods.

Here’s the resulting source code:

Listing 4: MainWindow.xaml.cs

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;

namespace ChatGUI
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        private ChatBackend.ChatBackend _backend;

        public MainWindow()
        {
            InitializeComponent();
            _backend = new ChatBackend.ChatBackend(this.DisplayMessage);
        }

        public void DisplayMessage(ChatBackend.CompositeType composite)
        {
            string username = composite.Username == null ? "" : composite.Username;
            string message = composite.Message == null ? "" : composite.Message;
            textBoxChatPane.Text += (username + ": " + message + Environment.NewLine);
        }

        private void textBoxEntryField_KeyDown(object sender, KeyEventArgs e)
        {
            if (e.Key == Key.Return || e.Key == Key.Enter)
            {
                _backend.SendMessage(textBoxEntryField.Text);
                textBoxEntryField.Clear();
            }
        }
    }
}

 

The WCF Configuration File

This is all the C# source code that we’ll need for our application, but we have on last missing piece, which is that we need to create a configuration file that will tell WCF how to behave. For background information on the configuration file we’re about to create, see “Windows Communication Foundation Configuration Schema” and  “Configuration Editor Tool (SvcConfigEditor.exe)” .

In Visual Studio, go to “Tools –> WCF Service Configuration Editor”. This will launch the configuration editor in a new window. Select “File –> New Config” to bring up an empty configuration, which should look something like this:

image

Select “Create a New Service” from the right-hand pane.

The “Service type” should be ChatBackend.ChatBackend

The “Service contract” should be ChatBackend.IChatBackend

Select “Peer to Peer” as the communication mode.

The address for the endpoint should be: net.p2p://Chat

image

image

Next, click on “Endpoint: (Empty Name)” and give the endpoint the name “Chat”

image

At this point, you should save the configuration into <your projects directory>\Chat\ChatGUI\App.config (where <your projects directory> is typically something like C:\Users\username\Documents\Visual Studio 2010\Projects).

Now, continuing in the configuration editor, let’s create a client. Since this is a peer-to-peer application, our program acts as both a service provider and a client for that same service.

In the left-hand Configuration pane, click on “Client”, and then in the right-hand pane, “Create a New Client”. We’ll want to create a client based on the service configuration we’ve already created, so in the “Generate a client config from the config of the service” field, browse to this same configuration file that you just saved, and click Next.

There should be just one service endpoint to select (the ChatBackend.ChatBackend endpoint), and name your Client configuration ChatEndpoint.

image

Last, we’ll need to define the bindings. Click on “Bindings” in the left-hand pane, followed by “Create New Binding” and select the netPeerTcpBinding type. Change the binding configuration name to be “Wimpy” (because we’ll start with something completely unsecure… later we can come back and require different varieties of authentication).

On the Security tab for this binding, choose Mode: None. Then in the left-hand pane, under Bindings –> Wimpy –> resolver, click “resolver” and set Mode to be Pnrp.

Now go back to “Services –> ChatBackend.ChatBackend –> Endpoints –> Chat” in the left-hand pane and set BindingConfiguration to be “Wimpy”.

Do the same for “Client –> Endpoints –> ChatEndpoint –> BindingConfiguration”

Finally, save and close the Configuration window.

Next, in the Solution Explorer, right-click on the ChatGUI project, select Add Existing Item, and add the App.config file to the project (you’ll have to change the file filters to allow all file types in order to browse to it).

Double-click the App.config file in order to double-check the work that was done by the configuration editor. You should see the following XML code. If it isn’t quite right, you can always fix it up by hand:

Listing 5: App.config

<?xml version="1.0" encoding="utf-8"?>
<configuration>
    <system.serviceModel>
        <bindings>
            <netPeerTcpBinding>
                <binding name="Wimpy">
                    <resolver mode="Pnrp" />
                    <security mode="None">
                        <transport credentialType="Password" />
                    </security>
                </binding>
            </netPeerTcpBinding>
        </bindings>
        <client>
            <endpoint address="net.p2p://Chat" binding="netPeerTcpBinding"
                bindingConfiguration="Wimpy" contract="ChatBackend.IChatBackend"
                name="ChatEndpoint" kind="" endpointConfiguration="">
                <identity>
                    <certificateReference storeName="My" storeLocation="LocalMachine"
                        x509FindType="FindBySubjectDistinguishedName" />
                </identity>
            </endpoint>
        </client>
        <services>
            <service name="ChatBackend.ChatBackend">
                <endpoint address="net.p2p://Chat" binding="netPeerTcpBinding"
                    bindingConfiguration="Wimpy" name="Chat" contract="ChatBackend.IChatBackend" />
            </service>
        </services>
    </system.serviceModel>
</configuration>

Now we’re done!

Let’s build and run the final program.

To do so, hit F5 in Visual Studio.

Since the program will try to establish a service that listens on a port, you’ll see a security dialog like the following:

image

Click the “Allow Access” button.

Then, because a chat application isn’t any fun without at least two instances, run another copy. you can browse to the program and run it by double-clicking <your projects folder>\Chat\ChatGUI\bin\Debug\ChatGUI.exe

And we have two chat windows open that can talk to each other:

 image

Comments

  • Anonymous
    June 19, 2010
    The comment has been removed

  • Anonymous
    July 15, 2010
    The comment has been removed

  • Anonymous
    July 15, 2010
    The comment has been removed

  • Anonymous
    July 15, 2010
    A little more detail: The project build successfully earlier, when it was just the raw WPF application, displaying only the text boxes.  However, once everything else was added in, this compile error is thrown.

  • Anonymous
    August 08, 2010
    Does this project works across the Internet

  • Anonymous
    September 23, 2010
    Hi can we implement same for group chat application

  • Anonymous
    September 23, 2010
    The comment has been removed

  • Anonymous
    October 11, 2010
    The comment has been removed

  • Anonymous
    December 15, 2010
    I get exactly the same error (XamlParseException). Perhaps this should be addressed soon? It seems to occur when serviceHost.Open() is called. It throws System.InvalidOperationException when I catch it. Other than that, great guide.

  • Anonymous
    February 02, 2011
    Hey man, im following your tutorial as we speak, but i get the following error halfway Class1.Class1'  does not implement interface member 'Class1,Interface1.Sendmessage(string)' any idea on how to solve this? thanks !

  • Anonymous
    April 27, 2011
    The comment has been removed

  • Anonymous
    May 03, 2011
    Comment out following code <!--<identity>          <certificateReference storeName="My" storeLocation="LocalMachine"              x509FindType="FindBySubjectDistinguishedName" />        </identity>-->

  • Anonymous
    May 17, 2011
    The comment has been removed

  • Anonymous
    January 03, 2012
    The comment has been removed

  • Anonymous
    January 20, 2012
    The comment has been removed

  • Anonymous
    March 28, 2012
    The comment has been removed

  • Anonymous
    April 02, 2012
    The comment has been removed

  • Anonymous
    May 08, 2012
    hai, It's working fine when i open the to windows in my won Pc... But, when i install it in another system it didn't recognize the network attached pc's and didn't communicate with them from my system... how to chat to the particular system using my be with port or ip address... please provide the source to implement this please

  • Anonymous
    May 22, 2012
    Hi All, You can check my WPF and Silverlight 5 Chat Appliaction(partially complete) at: iconnect.arshdeep-virdi.com/web This application is using net.tcp protocol to connect to the WCF service. Also you can download the fully functional WPF client for testing. Regards, Arsh

  • Anonymous
    August 09, 2012
    The comment has been removed

  • Anonymous
    August 09, 2012
    The comment has been removed

  • Anonymous
    August 10, 2012
    Hi, I have tried making this application using winForms and it works really well.... getting the same xamlparseexception in WPF though.... please let me know if anybody knows the solution to this exception....

  • Anonymous
    August 17, 2012
    The comment has been removed

  • Anonymous
    October 07, 2012
    The comment has been removed

  • Anonymous
    November 05, 2012
    Hi everyone I think i solved the xamlparseexception ! go into the StartService method and change "ChatEndpoint" to "ChatEndPoint"

  • Anonymous
    February 24, 2013
    javascript:WebForm_DoPostBackWithOptions(new%20WebForm_PostBackOptions("ctl00$content$ctl00$w_52320$_ae2cd5$ctl00$ctl00$ctl00$ctl05$bpCommentForm$ctl05$btnSubmit",%20"",%20true,%20"BlogPostCommentForm-ctl00_content_ctl00_w_52320__ae2cd5_ctl00_ctl00",%20"",%20false,%20true))

  • Anonymous
    June 13, 2013
    hello, I was trying to implement this code but it gives an error in DisplayMessageDelegate _displayMessageDelegate = null; in ChatBackend.cs file. Even I did not get this, we are no where creating the DisplayMessageDelegate class.

  • Anonymous
    September 19, 2013
    Do we have a solution for 'The invocation of the constructor on type " exception? Commenting out the identity section or changing ChatEndpoint to ChatEndPoint did not help.

  • Anonymous
    November 07, 2013
    Yes, I was getting the 'The invocation of the constructor on type 'ChatGUI.MainWindow' error too but luckily resolved it. Go to App.config file and make sure that the endpoint address="net.p2p://chat" for both the client and service. If they are different, then it means that the client and service are trying to communicate on two different addresses. Also note the Client Endpoint name in App.config. It should be "ChatEndPoint". Make sure that it is the same in ChatBackend.cs in the statement "channelFactory = new ChannelFactory<IChatBackend>("ChatEndPoint"); ". If they are not same, then it is an error because the client and service have two different endpoints. Hence, the service is looking for an endpoint that does not actually exist. This is why Visual Studio is throwing an error.

  • Anonymous
    November 25, 2013
    Which Shows this error... ChatGUI.exe' does not contain a static 'Main' method suitable for an entry point

  • Anonymous
    November 29, 2013
    I'm trying to make the same in VS2013, everything looks fine, but it underlines "_backend" in "_backend = new ChatBackend.ChatBackend(this.DisplayMessage);" saying that "The name '_backend' does not exist in the current context". What is wrong?

  • Anonymous
    January 23, 2014
    hello i have run the app successfully but when i run the .exe on other pc then my messages were not showing on that pc nor the messages on written on that pc are showing on mine. how to correct this so i can use this app on different computers using the same network. please reply asap.

  • Anonymous
    January 30, 2014
    I cannot find the WCF Service Configuration Editor under tools it doesnt exist

  • Anonymous
    April 12, 2014
    Everything is wrong, no matter how carefully I make it from here http://noiamarkmik.net/grr.jpg

  • Anonymous
    April 24, 2014
    Is it mandatory to set endpoint address="net.p2p://Chat" can i use different address like tcp://(localhost)......   something like this ?

  • Anonymous
    May 14, 2014
    The comment has been removed

  • Anonymous
    May 21, 2014
    definitly wrong... you should had post the source code.... with that.. Zero Points

  • Anonymous
    May 31, 2014
    The comment has been removed

  • Anonymous
    August 29, 2014
    thanks, it got me sometime but it correctly worked for after solving some problems.

  • Anonymous
    October 06, 2014
    The comment has been removed

  • Anonymous
    December 31, 2014
    The comment has been removed