传说中的 DarwMe

DrawMe - 使用WPF/WCF创建的网络绘图板聊天程序

 

翻译
Tim Callaghan, Alvin Lim 著 DrawMe - A network ink-chat application exploring .NET 3.5, WPF and WCF
flanker 翻译 in msproject

简介

这个演示项目是一个头脑风暴的结果,本来是准备写文章参加VS2008比赛的。我们打算尝试Visual Studio最新版本引入的.NET 3.0(和3.5)中的一些特性。最初我们提出了一个网络聊天程序的概念,打算用WPF来实现界面,用WCF实现网络通讯。试验了一些WPF的新控件后,我们认为使用InkCanvas控件会比较好,并做了一个多用户网络画图演示程序。DrawMe就是最后的结果,在本文中,我们会讲解我们遇到的一些有意思的WPF和WCF特性。

在高层次上,DrawMe使用了C/S的结构。当用户运行DrawMe后,有两个选择——建立一个新服务器或者连接到一个已存在的服务器。当某个用户在画布上绘画时,墨水笔迹将会广播到每一个登录在服务器上的客户端,这样就可以建立实时协作绘画。虽然这不是一个新概念,但是本文可以说明使用WPF和WCF实现的话会十分简单。

使用演示程序

如果你仅想要试一下最终程序,你可以下载演示程序。大多数人可能会在一台计算机上试验,这样也许不需要配置防火墙。运行DrawMe.exe两个实例,并把第一个设为服务器。在第二个实例中,把类型设为客户端,然后输入localhost、服务器机器名或者IP地址即可。如果你想在局域网内的多台机器上试验,也许你需要在防火墙中设置允许DrawMe通过。如果你想在Internet上的多台机器试验,也许需要把路由器的8000端口映射到你的机器上。上面几种情况我们都已成功测试过,希望你的也正常。

使用WPF建立用户界面

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

  • 登录控件 - 建立或连接服务器的WPF用户控件
  • 主程序窗体 - 所有墨水笔绘画的地方

下面几个小节分别介绍每个界面的功能和创建过程

登录控件

用户启动DrawMe后,他们会看到登录界面。用户界面的作用是让用户选择是加入一个已经存在的服务器,还是建立一个服务器并加入进去。下面的截图就是登录窗口的样子。

这是个比较基础的界面,它不用做很多事儿所以比较简单。这里需要指出一些WPF带来的一些很棒的特性。

 

 

  • 很容易创建漂亮的圆角矩形界面,只用指定CornerRadius属性
  • 很容易设置漂亮的渐变颜色,只需给元素的背景上添加一个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>
    
    ="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,显示所有连接客户端的绘画。

    下面的截图就是主程序窗体的样子。

    同样,这里我们需要指出WPF的一些新特性:

    • 可以给XAML元素指定一个DropShadowBitmapEffect - 注意客户列表的阴影效果。
    • 可以很容易的通过DoubleAnimation元素做出动画文字 - 在下面的XAML代码中,你可以看到我们设置连接状态为5秒循环的淡入淡出。
    • InkCancas不需要修改就可以使用 - 我们仅需接通一些事件处理而已,InkCancas已经包含一些标准编辑模式(墨水笔、擦除笔迹、擦除点)。
    • 元素的属性(Attribute)可以绑定到类的属性(Property)上 - 我们把当前墨水笔颜色存在FillColor属性中,它是一个DependencyProperty的包装。值得注意的是当在代码编程中更新FillColor时,不用费力去更新界面上的实际显示颜色,只要属性绑定正确,更新会自动完成。

    当用户点击颜色按钮时,弹出一个颜色选择器。不过,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服务器的通讯,并用它连接服务器。
    • 更新用户列表 - 服务器使用客户端的回调来更新已登录的用户列表。
    • 结束登录 - 关闭登录控件,进入聊天模式。

    下面的代码说明了我们如何实现登录过程。注意为了简化程序,我们关闭了安全检查(见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://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();
    }


    处理墨水笔迹

    一旦用户连接服务器后,程序就可以发送和接受笔迹了。在这一步骤有两个主要事件:

    • SendInkStrokes - 用户在画布上绘画,笔迹会发送给服务器以便转发给所有客户端。
    • OnInkStrokesUpdate - 其他用户绘画后,服务器使用回调来更新每个用户的画布。

    在DrawMe所有的笔迹都以MemeoryStream对象传送(或者以底层的字节数组形式)。注意我们没有用巧妙的方法来传送笔迹,我们传送了画布上的所有内容而不是最后更新的那部分。这作为演示可以很简单的来处理擦除模式(和绘画模式是一样的)。我们本来要优化笔迹传送,但是可惜最后没有时间实现了。下面的代码表明了我们如何实现客户端里的功能。

    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对象,但是我们马上遇到了问题:在我们使用前,对象就被GC回收了。这是因为每个客户端需要保证在GUI线程上进行全部更新,所以我们使用了一个匿名代理来向GUI线程发送一个异步调用。当GUI线程在处理更新时,MemoryStream对象就有可能被GC回收了。现在看来这个问题很明显,但当时的确困扰了我们不少时间。

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

    登出

    当一个用户登出时,程序会通知服务器将它从用户列表中移出。如果这个用户就是服务器,那所有用户都会断开连接并返回到登录窗口。

    这是当用户登出时执行的代码:

    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来实现程序间的通讯。在这一节中,我们给出WCF关键特性的概述。在通讯上我们需要解决三个主要问题:

    • 序列化自定义对象 - 提供一个方法来在网络上传送我们的实例对象
    • 定义服务合同 - 指定一个服务器要实现的接口
    • 提供客户端回调函数 - 指定一个回调,服务器可以用来调用每个客户端的方法

    WCF提供了每个问题的解决

    序列化自定义对象

    很多.NET内置类型默认都是可序列化的,这意味着他们在网络通讯中可以用标准方式表示。但是,当你定义一个新的类时,却不是默认可序列化的。我们创建了ChatUser类来储存每个用户的信息,为了在网络上传送CharUser对象,我们需要指定它为可序列化的。

    我们给ChatUser类加上WCF的System.Runtime.Serialization - [DataContract]特性,应用这个特性表示我们打算序列化这个类。要序列化类的特定成员,我们要给它加上[DataMember]特性,这是因为DataContract被设计为了“Opt-in”模式(同意才加入),也就是说,任何没有指定DataMemeber特性的成员是不会被序列化的。下面的代码片断说明了我们如何给ChatUser类应用这些特性的。在ChatUser.cs中可以看到全部信息。

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

    服务合同

    为了使每个客户端都能与服务器通讯,需要建立一个服务合同。合同的目的是公开服务的接口,这样客户端就知道了服务端可以使用的方法。在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服务对象不存在,则创建一个。这一点很像单例模式。

    下面是我们如何在DrawMeService实现上添加ServiceBehavior特性的。

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

    客户回调

    DrawMe有一个IDrawMeServiceCallback接口,允许服务器来给客户端发送消息。例如,新用户加入聊天室,服务器使用回调机制来通知每个用户。回调接口在共享的DrawMeInterfaces.dll中定义,并在客户端上实现,见ClientCallBack.cs

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

    • UpdateUsersList - 当新用户加入时,服务器通知每一个用户
    • 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/),它是微软的开源项目网站。我们发现CodePlex是一个很好用的工具,可以协同工作和掌握事件。而且,CodePlex有一个直观的界面,我们可以很轻松的使用它。

    CodePlex的后端使用了Team Foundation Server(TFS)系统来储存社区项目。使用Team Explore 2008可以使VS2008与TFS更紧密地集成。Team Explore 2008是微软的一个免费的简单的TFS客户端,可以直接集成到VS2008开发环境中。不过Team Explore 2008不能用于VS2008 Beta2(下载了387MB后才痛苦发现)。但最后也没什么大碍,因为我们使用了TortoiseSVN(一个Windows的Subversion客户端)来访问TFS。更多信息可见于CodePlex FAQ列表。

    一旦我们获得了源代码控制访问,就可以很方便的来协同工作。CodePlex让我们很喜欢的一点是它集成的Issue Tracker事件追踪器,可以很方便的提交一个事件,给每个人指定任务。总之,如果你打算建立一个多人开发的开源项目,使用CodePlex是一个很好的选择。

  • posted @ 2008-05-20 10:19  齐.net  阅读(719)  评论(1编辑  收藏  举报