65.9K
CodeProject 正在变化。 阅读更多。
Home

DrawMe - 一个探索 .NET 3.5、WPF 和 WCF 的网络绘画聊天应用程序

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.67/5 (18投票s)

2007年12月21日

GPL3

14分钟阅读

viewsIcon

118235

downloadIcon

3317

一个演示性的网络绘画聊天应用程序,探索 .NET 3.5、WPF 和 WCF 的一些方面。

DrawMe sample image

目录

引言

这个演示项目源于为 VS2008 竞赛准备文章的头脑风暴!我们想尝试并展示一些最新版 Visual Studio 中引入的 .NET 3.0(和 3.5)的优秀新技术。起初,我们想到了一个网络聊天程序——我们打算用 WPF 实现 GUI,用 WCF 实现网络通信。在对新的 WPF 控件进行了一些实验后,我们决定更有趣的是使用新的 InkCanvas 控件,并创建一个多用户网络绘画演示。DrawMe 是我们实验的结果,在这篇文章中,我们将回顾一下我们在过程中遇到的一些有趣的 WPF 和 WCF 代码特性。

最高层面上,DrawMe 使用客户端-服务器架构,服务器托管在其中一个客户端的计算机上。当用户启动 DrawMe 时,他们可以选择创建一个新的本地托管绘画的新服务器,或者连接到一个已有的 DrawMe 服务器,该服务器可以是本地的或远程的。当用户在墨迹画布上绘画时,绘画笔画会被广播到所有注册到主 DrawMe 服务器的客户端。这样,用户就可以参与实时协作绘画。尽管这个概念并不新鲜,但本文的目标是了解使用 WPF 和 WCF 实现它的简易程度。

使用演示

如果您只想尝试最终产品,您可以使用文章顶部的链接下载演示应用程序。很可能大多数人只会在一台计算机上测试它,在这种情况下,它应该可以正常工作,只需要很少的防火墙配置。只需启动几个 DrawMe.exe 实例,并将第一个设置为服务器。连接第二个实例时,将类型更改为客户端,并为服务器地址指定 localhost、计算机名或计算机 IP 地址。如果您想在局域网上的两个或更多独立的计算机上进行尝试,您可能需要允许 DrawMe 访问您正在运行的任何防火墙。如果您想在互联网上的两个或更多计算机上进行尝试,您可能还需要将 TCP 8000 端口从您使用的任何路由器转发到您的计算机等等。我们已经在列出的所有场景中成功地对其进行了测试,因此希望它也能为您工作!

使用 WPF 构建用户界面

DrawMe 的用户界面包含两个主要窗口

  • 登录控件 - 一个 WPF 用户控件,您可以在其中创建和/或连接到 DrawMe 服务器
  • 主应用程序窗口 - 所有绘画发生的地方

以下子章节将探讨用户界面这些区域的功能和创建。

登录控件

当用户首次启动 DrawMe 时,会显示一个登录屏幕。登录屏幕的目的是允许用户加入现有的 DrawMe 会话,或者创建一个新的 DrawMe 服务器并作为第一个客户端加入该会话。下面的截图显示了登录窗口的外观

The login screen

它是一个相对基本的 UI,但它不需要做太多事情,所以我们保持了简洁。WPF 带来了一些很棒的功能,我们应该指出一下

  • 通过为元素指定 CornerRadius 属性,可以轻松地为 UI 的矩形区域制作出视觉上令人愉悦的圆角
  • 通过将 LinearGradientBrush 应用于元素的背景,可以轻松地指定漂亮的颜色渐变

以下 XAML 代码列表显示了我们如何制作登录控件。我们发现处理原始 XAML 是快速实验和微调设计的最佳方法;但是,VS2008 中的布局设计管理器在您想要玩弄控件时也能做得很好。

<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>

主应用程序窗口

用户登录到 DrawMe 服务器后,应用程序会启用主 DrawMe 窗口,所有绘画都在其中进行。窗口包含四个主要部分

  • 信息栏 - 窗口顶部的 StackPanel,显示连接状态(以淡入/淡出动画的形式)以及谁最近进行了绘画的信息。还有一个退出按钮,供用户离开会话时使用。
  • 客户端列表 - 窗口左侧的 ListView,显示所有已连接客户端的用户名。
  • 墨迹工具选择 - 信息栏下方的 StackPanel,允许用户选择如何与墨迹画布交互。
  • 墨迹画布 - 一个 InkCanvas 控件,负责显示所有已连接客户端的墨迹绘画。

下面的截图显示了主应用程序窗口的外观

The main application window

同样,这里也使用了一些很棒的 WPF 功能,我们应该指出一下

  • 可以为 XAML 元素指定 DropShadowBitmapEffect - 注意客户端列表周围的阴影。
  • 通过 DoubleAnimation 元素可以轻松地为文本设置动画 - 在接下来的 XAML 代码列表中,您可以看到我们将连接状态动画设置为每 5 秒在不透明和透明之间循环。
  • InkCanvas 控件无需任何修改即可直接使用 - 我们只需要为收集或擦除笔画时引发的事件设置一些处理程序。不同的交互模式(墨迹、按笔画擦除、按点擦除)是 InkCanvas 控件的标准内置编辑模式。
  • XAML 元素的属性可以绑定到底层类的属性(DependencyProperty) - 我们将当前的墨迹颜色存储在 FillColor 属性中,它只是一个 DependencyProperty 的包装器。有趣的是,当 FillColor 在代码隐藏文件中以编程方式更新时,不需要额外的努力来更新 GUI 中实际显示的颜色;一旦属性正确绑定,更新就会自动发生。

当用户点击颜色按钮时,会显示一个颜色选择器对话框。不幸的是,WPF 没有原生的颜色选择对话框。幸运的是,我们在一个 MSDN 博客上找到了 这个 颜色选择器对话框。我们对其进行了轻微修改,以使其与我们的配色方案保持一致,但基本上是按原样使用的。

以下 XAML 代码列表显示了我们如何制作主应用程序窗口

<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 流程图

为了帮助解释使用 DrawMe 的主要运行时场景,我们构建了一些 UML 顺序图来表示应用程序在不同阶段的状态。

登录

登录过程中最多可能发生四种主要事件

  • 启动服务器 - 如果用户正在启动新的 DrawMe 服务器,应用程序将生成一个线程来运行 DrawMeService,该服务将协调客户端之间的通信。我们使用 TCP 作为传输协议,但 WCF 允许轻松更改协议。
  • 启动客户端 - 构造一个 ClientCallBack(实现 IDrawMeServiceCallback)为服务器提供调用客户端上函数的方法。同时构造一个 DrawMeServiceClient 来处理与 DrawMe 服务器的 TCP 通信,并使用它连接到服务器。
  • 更新客户端列表 - 服务器利用客户端的回调来更新注册到服务器的用户列表。
  • 完成登录 - 关闭登录控件并开始墨迹聊天模式。

下面的代码列表显示了我们如何实现登录过程。请注意,为了简单起见,我们禁用了所有安全(参见 App.config)。我们还将通信端口硬编码为 8000。同样,这只是为了使演示更轻松。在实际应用程序中,我们可能不会这样做!

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://: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();
}

处理墨迹笔画

客户端连接到服务器后,应用程序就可以发送和接收墨迹笔画了。在此阶段可能发生的两个主要事件是

  • SendInkStrokes - 用户在画布上绘画,生成的笔画被发送到服务器,由服务器转发给所有客户端。
  • OnInkStrokesUpdate - 另一位用户进行了绘画,服务器然后使用注册的回调来更新所有客户端的画布。

DrawMe 中的所有墨迹笔画都作为 MemoryStream 对象(或其底层字节数组表示)发送。请注意,我们发送笔画的方式并不智能;我们发送整个墨迹画布的内容,而不是最近的更新。为了演示目的,这没关系,因为它使擦除代码易于处理(与绘画代码相同!)。我们已计划优化墨迹笔画的发送,但可惜,我们未能及时为本文实现。

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);
  }
}

一旦笔画发送到服务器,就会执行以下代码来更新所有已注册的客户端。请注意,当我们发送笔画更新回每个客户端时,我们如何对传入的内存流调用 GetBuffer()。起初,我们只是传递 MemoryStream 对象,但我们很快遇到了对象在被使用前被垃圾回收的问题。这是因为每个客户端都需要确保所有用户界面更新都发生在主 GUI 线程上,因此我们使用匿名委托将异步调用发布到 GUI 线程。到 GUI 线程处理更新时,MemoryStream 可能已经被垃圾回收了。现在看来这很明显,但当时它让我们困惑了几分钟!

public class DrawMeService : IDrawMeService
{
  public void SendInkStrokes(MemoryStream memoryStream)
  {
      IDrawMeServiceCallback client = 
        OperationContext.Current.GetCallbackChannel<idrawmeservicecallback>();
      
      foreach (IDrawMeServiceCallback callbackClient in s_dictCallbackToUser.Keys)
      {
          if (callbackClient != 
              OperationContext.Current.GetCallbackChannel<idrawmeservicecallback>())
          {
              callbackClient.OnInkStrokesUpdate(
                s_dictCallbackToUser[client], memoryStream.GetBuffer());
          }
      }
  }

  ...
}

注销

当用户注销时,应用程序会联系服务器并通知它客户端应从已注册客户端列表中删除。如果用户还托管服务器,那么所有客户端都会断开连接并返回到登录模式。

The Log-off Process

以下是客户端离开时在服务器上执行的代码

public void Leave(ChatUser chatUser)
{
  IDrawMeServiceCallback client = OperationContext.Current.GetCallbackChannel();
  if (s_dictCallbackToUser.ContainsKey(client))
  {
      s_dictCallbackToUser.Remove(client);
  }

  foreach (IDrawMeServiceCallback callbackClient in s_dictCallbackToUser.Keys)
  {
      if (chatUser.IsServer)
      {
          if (callbackClient != client)
          {
              //server user logout, disconnect clients
              callbackClient.ServerDisconnected();
          }
      }
      else
      {
          //normal user logout
          callbackClient.UpdateUsersList(s_dictCallbackToUser.Values.ToList());
      }
  }

  if (chatUser.IsServer)
  {
      s_dictCallbackToUser.Clear();
  }
}

使用 WCF 进行通信

到目前为止,我们还没有过多地讨论我们是如何使用 WCF 来实现 DrawMe 应用程序实例之间通信的。在本节中,我们将概述我们使用的关键 WCF 功能。我们需要解决三个主要问题才能使通信正常工作

  • 序列化自定义对象 - 提供一种通过网络发送类实例对象的方法
  • 定义服务合同 - 指定服务器将实现的接口
  • 提供客户端回调函数 - 指定服务器可以利用的回调接口来调用每个客户端实例上的方法

WCF 为所有这些问题提供了解决方案!

序列化对象

.NET 中许多内置类型默认是可序列化的。这意味着它们可以以标准方式表示,以便在网络连接上传输。但是,当您定义一个新类时,它默认是不可序列化的。为了存储有关每个 DrawMe 客户端用户的信息,我们创建了一个 ChatUser 类。为了通过网络连接传递 ChatUser 对象,我们需要指定它们是可序列化的。

我们将 ChatUser 类设置为使用 WCF 的 System.Runtime.Serialization - [DataContract] 属性。将此属性应用于类表示我们有兴趣对其进行序列化。要序列化类的特定成员,我们需要应用 [DataMember] 属性。这是因为数据合同的设计采用了“选择加入”的编程模型。也就是说,任何未显式标记 DataMember 属性的内容都不会被序列化。以下代码片段显示了我们如何将这些属性应用于 ChatUser 类。有关完整的实现,请参见 ChatUser.cs

[DataContract]
public class ChatUser
{
    ...
    
    [DataMember]
    public string NickName
    {
        get { return m_strNickName; }
        set { m_strNickName = value; }
    }
    
    ...
}

服务合同

为了让每个 DrawMe 客户端能够与服务器通信,需要建立一个合同。合同的目的是发布服务器将实现的接口,以便客户端知道服务器上可用的方法。在 WCF 中,可以通过将 ServiceContract 属性应用于接口来指定合同。应用此属性时,还可以指定 CallbackContract,它表示客户端将实现的表示回调接口。您可以看到我们在下面的代码中如何使用该属性

[
   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);
}

每个客户端只需要了解 IDrawMeService 接口;但是,服务器需要包含实现。在提供实现时,可以指定 ServiceBehavior 属性。DrawMe 服务器使用以下服务行为

  • ConcurrencyMode - Single。服务一次只处理一个传入呼叫。
  • InstanceContextMode - Single。所有传入呼叫都使用一个 DrawMeService 对象,并且在呼叫后不进行回收。如果 DrawMe 服务对象不存在,则会创建一个。这实际上是一个单例。

以下是我们如何将 ServiceBehavior 属性应用于 DrawMeService 实现

[
    ServiceBehavior
    (
        ConcurrencyMode = ConcurrencyMode.Single, 
        InstanceContextMode = InstanceContextMode.Single
    )
]
public class DrawMeService : IDrawMeService
{
    ...
}

客户端回调

DrawMe 具有一个 IDrawMeServiceCallback 接口,该接口允许 DrawMe 服务器将消息发送回客户端应用程序。例如,当一个新用户加入聊天室时,服务器使用回调机制通知所有其他用户。回调接口定义在共享的 DrawMeInterfaces.dll 中;实现位于客户端端 - 请参阅 ClientCallBack.cs

DrawMe 客户端实现三个回调函数

  • UpdateUsersList - 当新用户加入聊天室时,DrawMe 服务器通知所有活动客户端
  • OnInkStrokesUpdate - 由 DrawMe 服务器将最新的墨迹笔画发送给所有活动客户端
  • ServerDisconnected - 当服务器断开连接时,此函数用于通知所有活动客户端

可以为每个回调函数指定 OperationContract 属性。在 DrawMe 中,我们选择使用 IsOneWay=true 属性来实现回调,即操作不向服务器传递任何有关它们是否成功的信息。

public interface IDrawMeServiceCallback
{
    [OperationContract(IsOneWay = true)]
    void UpdateUsersList(List listChatUsers);

    [OperationContract(IsOneWay = true)]
    void OnInkStrokesUpdate(ChatUser chatUser, byte[] bytesStroke);

    [OperationContract(IsOneWay = true)]
    void ServerDisconnected();
}

结论

本文希望为您提供了 WCF 中可用功能的一览。在满足我们实现协作绘画程序的目标方面,我们已经证明,使用一些很棒的新 WCF 功能,这不仅是可能的,而且相对容易。实际上,我们认为我们花在写这篇文章上的时间比写代码的时间还要多,所以这应该能让您对 WCF 框架的强大程度有所了解(假设我们不是糟糕的作家!)。请随时下载源代码,深入了解其结构,并在下方留下您的评论或问题!

附录 - 通过 CodePlex 协作

在这个小型项目的设计和编码阶段,我们希望有一种协作方式,而无需每次想在项目上工作时都去对方家。使用基于 Web 的免费源代码管理系统是显而易见的解决方案。我们决定尝试 CodePlex(http://www.codeplex.com/),这是 Microsoft 的开源项目托管网站。我们发现 CodePlex 在协调工作工作和跟踪仍需实现的内容方面非常有用。此外,CodePlex 具有非常直观的用户界面,我们中的任何一个人在使用它时都没有遇到任何困难。

CodePlex 的后端使用 Team Foundation Server (TFS) 数据库系统来存储所有社区项目。鉴于 VS2008 与 TFS 的紧密集成,我们最初计划使用 Team Explore 2008 作为源代码管理器。Team Explore 2008 是 Microsoft 提供的一款免费的简化版 TFS 客户端,可以直接集成到 VS2008 开发环境中。不幸的是,Team Explore 2008 不能与 VS2008 Beta 2 一起使用(我们在下载了 387 MB 之后才艰难地发现了这一点)。但最终,这并不重要,因为我们可以使用 TortoiseSVN(Windows 的 Subversion 客户端)来访问我们项目存储的 TFS。有关如何执行此操作的信息在 CodePlex FAQ 中很容易找到。

一旦我们解决了源代码管理访问问题,协作处理项目就变得非常容易了。我们真正喜欢 CodePlex 的地方是集成的 Issue Tracker;提出问题非常轻松简单,就像将问题分配给我们彼此工作一样。总而言之,如果您正在考虑与多个开发者一起启动一个开源项目,那么使用 CodePlex 绝对是一个值得探索的选项。

如果您有兴趣在 CodePlex 上“签出”DrawMe 项目,请前往 http://www.codeplex.com/drawme 看看。在“Issue Tracker”和“Source Code”选项卡可能是在了解我们经历的工作流程方面最有趣的地方。

© . All rights reserved.