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.
The code to accomplish this follows. Of notable importance is the Border
tag which specifies a CornerRadius
of 10:
<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:
<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:
<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:
((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:
[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.
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:
public partial class WindowMain: IChat
{
...
}
Connecting to the peer mesh is simple, and is handled by the following commented function:
private void ConnectToMesh()
{
m_site = new InstanceContext(this);
m_binding = new NetPeerTcpBinding("WPFChatBinding");
m_channelFactory = new DuplexChannelFactory<IChatChannel>(m_site, "WPFChatEndpoint");
m_participant = m_channelFactory.CreateChannel();
o_statusHandler = m_participant.GetProperty<IOnlineStatus>();
o_statusHandler.Online += new EventHandler(ostat_Online);
o_statusHandler.Offline += new EventHandler(ostat_Offline);
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:
="1.0"="utf-8"
<configuration>
<system.serviceModel>
<client>
<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.
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.