|
||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
Note: This is an unedited contribution. If this article is inappropriate,
needs attention or copies someone else's work without reference then please
Report This Article
Table of Contents
IntroductionThis demo project came about as a result of brainstorming on ideas for making an article to enter into the VS2008 contest! We wanted to experiment with and showcase a few of the great new .NET 3.0 (and 3.5) technologies that have been introduced with the lastest version of Visual Studio. Initially we came up with the concept of a network chat program - we were going to implement the GUI with WPF and the network communication with WCF. After some experimenting around with the new WPF controls we decided it would be more fun to use the new At the highest level, DrawMe uses a client-server architecture with the server hosted on one of the client's computers. When a user starts DrawMe they are given the choice of creating a new server to locally host ink drawings, or connecting to an existing DrawMe server which can be either local or remote. When a user draws on the ink canvas, the drawing strokes are broadcast to all clients registered with the main DrawMe server. In this way users can participate in real-time collaborative drawing. Although this concept isn't new, the goal of this article was to see how easy this would be to implement using WPF and WCF. Using the DemoIf you just want to try out the finished product you can download the demo application using the link at the top of the article. Chances are high that most people will just be testing it out on one computer, in which case it should work with minimal firewall configuration required. Simply start up a couple of instances of the DrawMe.exe and set the first one to be the server. When connecting the second instance change the type to Client and specify either localhost, the machine name, or the machine IP address for the server address. If you want to try it out on two or more separate computers on a LAN you will likely need to allow DrawMe access through any firewalls you have running. If you want to try it out on two or more computers across the internet you will also probably need to port-forward TCP 8000 to your computer from any router you are using etc. We've successfully tested it in all of the scenarios listed here so hopefully it should work for you too! Building the user interface using WPFThe user interface for DrawMe consists of two main windows:
The following sub-sections explore the function and creation of each of these areas of the user interface. The login controlWhen the user first starts DrawMe they are presented with a login screen. The purpose of the login screen is to allow the user to either join an existing DrawMe session or to create a new DrawMe server and also join that session as the first client. The following screenshot shows how the login window looks.
It's a relatively basic UI, but it doesn't need to do much so we've kept it simple. There's a few nice features that WPF brings to the table that we should point out:
The following xaml code listing shows how we made the login control. We found that working with the raw xaml was the fastest way to experiement and fine tune the design; however, the layout design manager in VS2008 also does a nice job if you want to play around with the controls. <UserControl x:Class="DrawMe.LoginControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Height="300" Width="350" Loaded="UserControl_Loaded">
<StackPanel>
<Border Height="50" BorderBrush="#FFFFFFFF" Background="Black" BorderThickness ="2,2,2,0" CornerRadius="5,5,0,0">
<Label Content="Welcome to DrawMe" HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="20" Foreground="White"/>
</Border>
<Border Height="220" BorderBrush="#FFFFFFFF" BorderThickness="2,2,2,0" CornerRadius="5,5,0,0">
<Border.Background>
<LinearGradientBrush EndPoint="0.713,0.698" StartPoint="0.713,-0.139">
<GradientStop Color="#FFFFFFFF" Offset="0.933"/>
<GradientStop Color="LightBlue" Offset="0.337"/>
</LinearGradientBrush>
</Border.Background>
<StackPanel Name="infoPanel" Orientation="Vertical" Margin="10,10,10,10">
<StackPanel Name="typePanel" Orientation="Horizontal">
<Label Name="lblChatType" FontSize="20" Width="120" HorizontalContentAlignment="Right" VerticalContentAlignment="Center">Type:</Label>
<RadioButton Name="chatTypeServer" FontSize="20" VerticalAlignment="Center" Margin="0,0,20,0"
Checked="chatTypeServer_Checked" VerticalContentAlignment="Center">Server</RadioButton>
<RadioButton Name="chatTypeClient" FontSize="20" VerticalAlignment="Center"
Checked="chatTypeClient_Checked" VerticalContentAlignment="Center">Client</RadioButton>
</StackPanel>
<StackPanel Name="serverPanel" Orientation="Horizontal" Margin="0,10,0,0">
<Label Name="lblServer" FontSize="20" Width="120" HorizontalContentAlignment="Right" VerticalContentAlignment="Center">Server:</Label>
<TextBox Height="30" Name="txtServer" Width="160" FontSize="20" VerticalContentAlignment="Center" />
</StackPanel>
<StackPanel Name="usernamePanel" Orientation="Horizontal" Margin="0,10,0,10">
<Label Name="lblUserName" FontSize="20" Width="120" HorizontalContentAlignment="Right">User Name:</Label>
<TextBox Height="30" Name="txtUserName" Width="160" FontSize="20" VerticalContentAlignment="Center" />
</StackPanel>
<StackPanel Name="buttonPanel" Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
<Button Name="btnLogin" Width="120" FontSize="20" Margin="10,10,10,10" Click="btnLogin_Click">Connect</Button>
<Button Name="btnCancel" Width="120" FontSize="20" Margin="10,10,10,10" Click="btnCancel_Click">Cancel</Button>
</StackPanel>
</StackPanel>
</Border>
<Border Height="30" Background="#FF2E2E2E" BorderBrush="#FFFFFFFF" BorderThickness="2,0,2,2" CornerRadius="0,0,5,5">
<Label Content="DrawMe is using .NET 3.5 (WPF and WCF)" FontSize="9" Foreground="#FFFFFFFF"
HorizontalAlignment="Center" VerticalAlignment="Center" Background="#00FFFFFF"/>
</Border>
</StackPanel>
</UserControl>
The main application windowAfter the user has logged in to a DrawMe server the application enables the main DrawMe window where all drawing takes place. The window consists of four major sections:
The following screenshot shows how the main application window looks.
Again, there's a few nice WPF features in use that we should point out:
When the user clicks on the colour button a colour picker dialog is displayed. Unfortunately,WPF has no native colour selection dialog. Luckily, we found this colour picker dialog on one of the MSDN blogs. We modified it slightly to be consistent with our colour scheme, but essentially it's been used as-is. The following xaml code listing shows how we made the main application window. <Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Name="DrawMeMainWindow"
x:Class="DrawMe.DrawMeWindow"
Title="DrawMeWindow" Height="600" Width="800"
Background="#FF3B3737" Loaded="Window_Loaded" MinWidth="800" MinHeight="500">
<Grid x:Name="LayoutRoot" >
<Grid.RowDefinitions>
<RowDefinition Height="65" />
<RowDefinition Height="50" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Border Grid.Column="0" Grid.Row="0" Grid.ColumnSpan="2" BorderBrush="Gray" BorderThickness="1,1,1,1" CornerRadius="8,8,8,8">
<StackPanel Name="loginStackPanel" Orientation="Horizontal" HorizontalAlignment="Left">
<StackPanel Orientation="Vertical" Margin="10,10,20,0">
<TextBlock Name="ApplicationTypeMessage" Width="120" Height="25" FontSize="10" Foreground="White" TextAlignment="Center">
Waiting for connection...
<TextBlock.Triggers>
<EventTrigger RoutedEvent="TextBlock.Loaded">
<BeginStoryboard>
<Storyboard Name="ApplicationTypeMessageStoryBoard">
<DoubleAnimation Name="ApplicationTypeMessageAnimation"
Storyboard.TargetName="ApplicationTypeMessage"
Storyboard.TargetProperty="(TextBlock.Opacity)"
From="1.0" To="0.0" Duration="0:0:5"
AutoReverse="True" RepeatBehavior="Forever"
/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</TextBlock.Triggers>
</TextBlock>
<Button Name="btnLeave" Width="100" Height="20" FontSize="10" Click="btnLeave_Click">
Sign Out
</Button>
</StackPanel>
<TextBlock Name="AnimatedMessage" FontSize="35" FontWeight="Bold" Foreground="White" VerticalAlignment="Center">
Welcome to DrawMe
</TextBlock>
</StackPanel>
</Border>
<Border Name="BorderUsersList" Grid.Column="0" Grid.Row="1" Grid.RowSpan="2" CornerRadius="8,8,8,8" Background="LightBlue" BorderThickness="4,4,4,4">
<ListView Name="lvUsers" Margin="10" FontSize="20">
<ListView.BitmapEffect>
<DropShadowBitmapEffect />
</ListView.BitmapEffect>
</ListView>
</Border>
<Border Name="BorderEditingType" Grid.Column="1" Grid.Row="1" CornerRadius="8,8,8,8" Background="LightBlue" BorderThickness="0,4,4,4">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<RadioButton Name="rbInk" Content="Ink" Margin="15,0,0,0" VerticalAlignment="Center" FontSize="20" IsChecked="True"
Tag="{x:Static InkCanvasEditingMode.Ink}" Click="rbInkType_Checked">
</RadioButton>
<RadioButton Name="rbEraserByStroke" Content="Erase By Stroke" Margin="15,0,0,0" VerticalAlignment="Center" FontSize="20"
Tag="{x:Static InkCanvasEditingMode.EraseByStroke}" Click="rbInkType_Checked">
</RadioButton>
<RadioButton Name="rbEraserByPoint" Content="Erase By Point" Margin="15,0,0,0" VerticalAlignment="Center" FontSize="20"
Tag="{x:Static InkCanvasEditingMode.EraseByPoint}" Click="rbInkType_Checked">
</RadioButton>
<TextBlock Margin="25,0,10,0" VerticalAlignment="Center" FontSize="20" >Colour:</TextBlock>
<Button Margin="0,0,0,0" Background="White" Height="28" Width="64" Click="OnSetFill">
<Rectangle Width="54" Height="20" Stroke="Black" StrokeThickness="2">
<Rectangle.Fill>
<SolidColorBrush Color="{Binding ElementName=DrawMeMainWindow, Path=FillColor}" />
</Rectangle.Fill>
</Rectangle>
</Button>
</StackPanel>
</Border>
<Border Name="BorderInkCanvas" Grid.Column="1" Grid.Row="2" Background="LightBlue" BorderThickness="0,0,4,4" CornerRadius="8,8,8,8" >
<InkCanvas x:Name="inkCanv" Margin="10" Background="White"
StrokeCollected="inkCanv_StrokeCollected" StrokeErasing="inkCanv_StrokeErasing"
StrokeErased="inkCanv_StrokeErased">
</InkCanvas>
</Border>
<Canvas Name="loginCanvas" Grid.Column="1" Grid.Row="2" Width="500" Height="300" VerticalAlignment="Top" HorizontalAlignment="Center" />
</Grid>
</Window>
DrawMe Sequence FlowTo help explain the main runtime scenarios when using DrawMe, we have constructed some UML sequence diagrams to represent the state of the application at various stages. LoginDuring login up to four main events can occur:
The following code listing shows how we've implemented the login process. Note that for the sake of simplicity we've disabled all security (see App.config <bindings>
<netTcpBinding>
<binding name="DrawMeNetTcpBinding">
<security mode="None">
<transport clientCredentialType="None" />
<message clientCredentialType="None" />
</security>
</binding>
</netTcpBinding>
</bindings>
LoginControl.xaml.cs private void btnLogin_Click(object sender, RoutedEventArgs e)
{
EndpointAddress serverAddress;
if (this.chatTypeServer.IsChecked == true)
{
DrawMe.App.s_IsServer = true;
serverAddress = new EndpointAddress("net.tcp://localhost:8000/DrawMeService/service");
}
else
{
DrawMe.App.StopServer();
DrawMe.App.s_IsServer = false;
if (txtServer.Text.Length == 0)
{
MessageBox.Show("Please enter server name");
return;
}
serverAddress = new EndpointAddress(string.Format("net.tcp://{0}:8000/DrawMeService/service", txtServer.Text));
}
if (txtUserName.Text.Length == 0)
{
MessageBox.Show("Please enter username");
return;
}
if (DrawMeServiceClient.Instance == null)
{
if (App.s_IsServer)
{
DrawMe.App.StartServer();
}
try
{
ClientCallBack.Instance = new ClientCallBack(SynchronizationContext.Current, m_mainWindow);
DrawMeServiceClient.Instance = new DrawMeServiceClient
(
new DrawMeObjects.ChatUser
(
txtUserName.Text,
System.Environment.UserName,
System.Environment.MachineName,
System.Diagnostics.Process.GetCurrentProcess().Id,
App.s_IsServer
),
new InstanceContext(ClientCallBack.Instance),
"DrawMeClientTcpBinding",
serverAddress
);
DrawMeServiceClient.Instance.Open();
}
catch (System.Exception ex)
{
DrawMe.App.StopServer();
DrawMeServiceClient.Instance = null;
MessageBox.Show(string.Format("Failed to connect to chat server, {0}", ex.Message),this.m_mainWindow.Title);
return;
}
}
if (DrawMeServiceClient.Instance.IsUserNameTaken(DrawMeServiceClient.Instance.ChatUser.NickName))
{
DrawMeServiceClient.Instance = null;
MessageBox.Show("Username is already in use");
return;
}
if (DrawMeServiceClient.Instance.Join() == false)
{
MessageBox.Show("Failed to join chat room");
DrawMeServiceClient.Instance = null;
DrawMe.App.StopServer();
return;
}
this.m_mainWindow.ChatMode();
}
Process Ink StrokesOnce the client has connected to a server the application is ready to send and receive inkstrokes. The two main events that can happen at this stage are:
All inkstrokes in DrawMe are sent as private void SaveGesture()
{
try
{
MemoryStream memoryStream = new MemoryStream();
this.inkCanv.Strokes.Save(memoryStream);
memoryStream.Flush();
DrawMeServiceClient.Instance.SendInkStrokes(memoryStream);
}
catch (Exception exc)
{
MessageBox.Show(exc.Message, Title);
}
}
Once the strokes are sent to the server, the following code is executed to update all the registered clients. Notice how we call public class DrawMeService : IDrawMeService
{
public void SendInkStrokes(MemoryStream memoryStream)
{
IDrawMeServiceCallback client = OperationContext.Current.GetCallbackChannel
LogOffWhen a user logs off, the application contacts the server and notifies it that the client should be removed from the registered clients list. If the user is also hosting the server then all clients are disconnected and returned to login mode.
Here's the code that gets executed on the server when a client leaves. public void Leave(ChatUser chatUser)
{
IDrawMeServiceCallback client = OperationContext.Current.GetCallbackChannel
Communicating using WCFSo far we haven't really talked too much about how we used WCF to implement the communication between instances of the DrawMe application. In this section we provide an overview of the key WCF features that we used. There were three main problems that we needed to solve to get the communiation working:
WCF provides solutions to all of these problems! Serializing objectsMany in-built types in .NET are serializable by default. This means that they can be represented in a standard manner for passing over network connections. However, when you define a new class it is not serializable by default. To store information about each DrawMe client user we created a We have setup the [DataContract]
public class ChatUser
{
...
[DataMember]
public string NickName
{
get { return m_strNickName; }
set { m_strNickName = value; }
}
...
}
Service contractIn order for each DrawMe client to be able to communicate with the server, a contract needs to be established. The purpose of the contract is to publish the interface that the server will implement so that clients know what methods are available on the server. In WCF, a contract can be specified by applying the [
ServiceContract
(
Name = "DrawMeService",
Namespace = "http://DrawMe/DrawMeService/",
SessionMode = SessionMode.Required,
CallbackContract = typeof(IDrawMeServiceCallback)
)
]
public interface IDrawMeService
{
[OperationContract()]
bool Join(ChatUser chatUser);
[OperationContract()]
void Leave(ChatUser chatUser);
[OperationContract()]
bool IsUserNameTaken(string strUserName);
[OperationContract()]
void SendInkStrokes(MemoryStream memoryStream);
}
Each client only needs to know about the
Here is how we have applied the [
ServiceBehavior
(
ConcurrencyMode = ConcurrencyMode.Single,
InstanceContextMode = InstanceContextMode.Single
)
]
public class DrawMeService : IDrawMeService
{
...
}
Client callbacksDrawMe has a IDrawMeServiceCallback interface which allows the DrawMe server to send messages back to the client application. For example, when a new user has joined the chat room, the server uses the callback mechanism to notify all other users. The callback interface is defined in a shared DrawMeInterfaces.dll; the implementation is located at the client side - see ClientCallBack.cs DrawMe clients implement three callback functions:
It's possible to specify an public interface IDrawMeServiceCallback
{
[OperationContract(IsOneWay = true)]
void UpdateUsersList(List
ConclusionThis article has hopefully given you an overview of some of the available features in WCF. In terms of meeting our goal of implementing a collaborative drawing program, we've demonstrated that this is not only possible but relatively easy to do using some of the cool new WCF functionality. Actually, we think we probably spent more time writing this acticle than we did writing the code for it, so that should give you some indication of how powerful the WCF framework is (assuming we're not terrible writers!). Please feel free to download the source code and delve into the structure, and leave you comments or questions below! Appendix - Collaborating via CodePlexDuring the design and coding stages of this small project we wanted a way to collaborate without having to keep going to each other's house every time we wanted to work on the project. Using a web-based free source control system was the obvious solution. We decided to try out CodePlex (http://www.codeplex.com/) which is Microsoft's open source project hosting web site. We found CodePlex to be a very useful tool in terms of coordinating the work effort and keeping track of what still needed to be implemented. What's more, CodePlex has a very intuitive user interface and neither of us had any difficulty in using it. The backend of CodePlex uses a system of Team Foundation Server (TFS) databases to store all the community projects on. Given that VS2008 integrates tightly with TFS we originally planned to use Team Explore 2008 as the source control client. Team Explore 2008 is a free, simplified TFS client from Microsoft that can integrate with the VS2008 development environment directly. Unfortunately Team Explore 2008 doesn't work with VS2008 Beta 2 (we found this out the hard way after downloading 387Mb). But in the end it didn't really matter because we were able to use TortoiseSVN (a subversion client for windows) to access the TFS that our project was stored on. Information on how to do this is readily available in the CodePlex FAQ. Once we got the source control access sorted out it was very easy for us to collaborate on the project. The thing we really liked about CodePlex was the integrated Issue Tracker; raising issues was painless and easy, as was assigning issues to each other for work. In summary, if you're thinking about starting up an open-source project with more than one developer then using CodePlex is definitely an option worth exploring. If you are interested in "checking out" the DrawMe project on CodePlex head on over to http://www.codeplex.com/drawme and take a look around. The "Issue Tracker" and "Source Code" tabs are probably the most interesting in terms of seeing the workflow process we went through.
| |||||||||||||||||||||||||||||||||||||||||