|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Services
Chapters
Feature Zones
|
Contents
IntroductionFor 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 AppBefore 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
A main window, from which the user can choose who to chat with:
And a window where chatters may openly chat:
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:
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
A Brief Overview of the Demo App and What We are Trying to AchieveIn the attached demo application, we are trying to carry our the following functionality:
In order to achieve all of this I have developed 3 separate assemblies, which by the end I hope you will understand.
Some Key Concepts ExplainedThere 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 the final application should be a little easier to explain (well that's the idea anyway). WCF: the New Service Orientated AttributesThere 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. ServiceContractAttributeIndicates that an interface or a class defines a service contract in a Windows Communication Foundation (WCF) application. It has the following members:
See the MSDN article for more details. OperationContractAttributeIndicates 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:
See the MSDN article for more details ServiceBehaviorAttributeSpecifies the internal execution behavior of a service contract implementation. It has the following members:
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. [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 [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession,
ConcurrencyMode = ConcurrencyMode.Multiple)]
public class ChatService : IChat
{
}
WCF: the Use of CallbacksRecall earlier, when I mentioned the [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): 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 ProxyIn 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:
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. //---------------------------------------------------------------------------
// <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 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
...instead of:
...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: [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: [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 The When the client invokes a method of the form As the attached demo application makes use of asynchronous methods for the WCF: ConfigurationAs 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:
WPF: Styles / TemplatesWPF 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
All that has been done is that I have applied a style to a standard .NET <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: AnimationsAnimations 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 In <!-- 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: //get Storyboard animation from window resources
((Storyboard)this.Resources["showChatWindow"]).Begin(this);
WPF: DatabindingThe styled <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 ApplicationThreading 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: 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 : /// <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 ApplicationWell 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). 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
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 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:
For the demo application, I am using CommonThis is a very simple class that is used by the
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()
{
}
/// | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||