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

WCF / WPF 聊天应用程序

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (466投票s)

2007 年 7 月 25 日

CPOL

29分钟阅读

viewsIcon

2084428

downloadIcon

61363

如何使用 Windows Communication Foundation 创建一个点对点聊天应用程序

目录

引言

对于阅读过我其他 CodeProject.com 文章的人来说,你们可能知道我并不害怕尝试新技术。这样做的好处之一是我通常会在这里分享我的学习心得,而这篇文章是我认为最难写的一篇之一, IMHO。本文将介绍如何使用 Windows Communication Foundation (WCF) 创建一个点对点聊天应用程序,以及如何使用 Windows Presentation Foundation (WPF) 使其外观更加美观。

当我第一次开始阅读 WCF 时,我首先查找的是 MSDN 的 WCF 示例(我读了很多),但它们并不那么好。我还发现了很多基于 MSDN 版本的聊天应用,它们都不够好,因为它们无法返回聊天应用程序内的用户列表。我希望创建一个具有已连接聊天者列表的漂亮 WPF 风格的应用。

于是我继续寻找,最终发现了一篇 Nikola Paljetak 写的非常出色/精彩的文章,我已将其改编为本文。我已经征得了 Nikola 的同意,原文内容 在此。老实说,原文简直是天才(值得一提的是 Nikola 是一位教授),但要理解其中的内容花了我一些时间,因为代码没有注释。我现在已为所有代码添加了注释,因此我认为对于刚接触 WCF/WPF 的人来说,这仍然是一篇非常有价值的讨论/文章。在此文章之前,我对 WCF 完全是新手,所以如果我能做到,你们也一定能做到。

所以,本文就是关于这个的。在文章结束时,我希望您至少能理解 WCF 的一些关键领域,并可能受到启发去关注本文的 WPF 部分。

关于演示应用程序的说明

在我开始用我附带的 WPF/WCF 应用程序的内部机制轰炸大家之前,我们是否先快速看一下最终产品?附加的演示应用程序中有 3 个主要区域:

一个登录屏幕

Screenshot - SignIn.png

一个主窗口,用户可以从中选择与谁聊天

Screenshot - Main.png

以及一个聊天者可以公开聊天的窗口

Screenshot - Chat.png

该应用程序基于使用 Visual Studio 2005 并安装了 The Visual Studio Designer for WPF,或者使用 Expression BLEND 和 Visual Studio 2005 的组合,或者如果你喜欢用 Wordpad 写东西,也可以用 Wordpad。显然,由于这是一个 WPF/WCF 应用程序,您还需要 .NET 3.0 框架。此应用程序将涵盖以下概念:

  • WCF
    • 新的面向服务属性
    • 接口的使用
    • 回调的使用
    • 异步委托
    • 创建代理
  • WPF
    • 样式
    • 模板
    • 动画
    • 数据绑定
    • WPF 应用程序的多线程处理

但是,这个应用程序并不是非常面向 WPF,因为 WPF 在 The Code Project 的许多其他 WPF 文章中都有介绍。WPF 部分基本上只是 WCF 文章的包装,而 WCF 文章才是本文的核心。尽管有一些漂亮的 WPF 部分,只是为了让聊天应用程序看起来比普通的控制台应用程序更漂亮。不过,我将讨论 WPF 实现中有趣的部分。

必备组件

  1. 要运行本文提供的代码,您需要安装 May 2006 LINQ CTP,该版本 在此。有一个新的 March 2007 CTP 可用,但完整安装大约需要 4GB(因为它不仅仅是 LINQ,而是整个代号为“Orcas”的下一代 Visual Studio),并且相当复杂,而且很可能会发生变化。因此,May 2006 LINQ CTP 对于本文旨在演示的内容来说是足够的。
  2. .NET 3.0 框架,可在此 下载

演示应用程序简述及我们的目标

在附件的演示应用程序中,我们试图实现以下功能:

  1. 允许聊天者选择自己的名字并选择一个图像(头像)来代表自己
  2. 允许聊天者加入点对点聊天
  3. 允许聊天者查看谁可以聊天
  4. 允许聊天者发送私人消息
  5. 允许聊天者发送全局消息
  6. 允许聊天者离开聊天环境
  7. 使用 WPF 使这一切看起来很漂亮(严格来说不是聊天应用程序的必需品,但我喜欢 WPF,所以请迁就我)

为了实现这一切,我开发了 3 个独立的程序集,希望您到最后能理解它们。

  • ChatService.Dll:WCF 聊天服务器,允许聊天客户端连接并充当主要消息路由器
  • Common.Dll:一个简单的可序列化类,由 ChatService.DllWPFChatter.Dll 文件共用
  • WPFChatter.Dll:WPF 包装器,用于漂亮的 WPF 客户端 WCF 功能

一些关键概念解释

为了理解整个应用程序(涵盖了很多内容),需要解释一些前面提到的关键概念。所以,我将一点一点地解释每个概念,这样最后的应用程序应该会更容易理解(至少这是我的想法)。

WCF:新的面向服务属性

WCF 有一些新的属性可以用来装饰我们的 .NET 类/接口,下面显示的是附加演示应用程序中使用的属性。

ServiceContractAttribute

指示接口或类在 Windows Communication Foundation (WCF) 应用程序中定义了服务合同。它具有以下成员:

名称 描述
CallbackContract 获取或设置双工合同的合同类型。当合同是双工合同时。
ConfigurationName 获取或设置在应用程序配置文件中定位服务的名称。
HasProtectionLevel 获取一个值,该值指示成员是否已分配了保护级别。
名称 获取或设置 Web 服务描述语言 (WSDL) 中“portType”元素的名称。
命名空间 获取或设置 Web 服务描述语言 (WSDL) 中“portType”元素的命名空间。
ProtectionLevel 指定合同的绑定是否必须支持 ProtectionLevel 属性的值。
SessionMode 获取或设置会话是允许、不允许还是必需。
TypeId (从 Attribute 继承)

有关更多详细信息,请参阅 MSDN 文章

OperationContractAttribute

指示方法在 Windows Communication Foundation (WCF) 应用程序中定义了服务合同的一部分操作。它具有以下成员:

名称 描述
操作 获取或设置请求消息的 WS-Addressing 操作。
AsyncPattern 指示操作是使用服务合同中的 Begin<methodName> 和 End<methodName> 方法对异步实现的。
HasProtectionLevel 获取一个值,该值指示此操作的消息是否必须加密、签名或两者兼有。
IsInitiating 获取或设置一个值,该值指示方法是否实现了一个可以在服务器上启动会话的操作(如果存在这样的会话)。
IsOneWay 获取或设置一个值,该值指示操作是否返回回复消息。
IsTerminating 获取或设置一个值,该值指示服务操作是否在发送(如果有)回复消息后导致服务器关闭会话。
名称 获取或设置操作的名称。
ProtectionLevel 获取或设置一个值,该值指定操作的消息是否必须加密、签名或两者兼有。
ReplyAction 获取或设置操作的回复消息的 SOAP 操作值。
TypeId (从 Attribute 继承)

有关更多详细信息,请参阅 MSDN 文章

ServiceBehaviorAttribute

指定服务合同实现的内部执行行为。它具有以下成员:

名称 描述
AddressFilterMode 获取或设置 AddressFilterMode,调度程序使用它将传入消息路由到正确的终结点。
AutomaticSessionShutdown 指定当客户端关闭输出会话时是否自动关闭会话。
ConcurrencyMode 获取或设置服务是否支持单个线程、多个线程或重入调用。
ConfigurationName 获取或设置在应用程序配置文件中用于定位服务元素的 值。
IgnoreExtensionDataObject 获取或设置一个值,该值指定是否将未知序列化数据发送到网络。
IncludeExceptionDetailInFaults 获取或设置一个值,该值指定通用的未处理执行异常是否转换为类型的 System.ServiceModel.FaultException System.ServiceModel.ExceptionDetail 并作为故障消息发送。仅在开发期间将此值设置为 true 以排除服务故障。
InstanceContextMode 获取或设置一个值,该值指示何时创建新的服务对象。
MaxItemsInObjectGraph 获取或设置序列化对象中允许的最大项数。
名称 获取或设置 Web 服务描述语言 (WSDL) 中服务元素的 name 属性的值。
命名空间 获取或设置 Web 服务描述语言 (WSDL) 中服务的目标命名空间的值。
ReleaseServiceInstanceOnTransactionComplete 获取或设置一个值,该值指定在当前事务完成时是否释放服务对象。
TransactionAutoCompleteOnSessionClose 获取或设置一个值,该值指定在当前会话无错误关闭时是否完成待处理的事务。
TransactionIsolationLevel 指定服务内部创建的新事务的事务隔离级别,以及从客户端流传过来的传入事务。
TransactionTimeout 获取或设置事务必须在其中完成的时间段。
TypeId (从 Attribute 继承)
UseSynchronizationContext 获取或设置一个值,该值指定是否使用当前同步上下文来选择执行线程。
ValidateMustUnderstand 获取或设置一个值,该值指定系统或应用程序是否强制执行 SOAP MustUnderstand 头处理。

有关更多详细信息,请参阅 MSDN 文章。下面是一个在演示应用程序的服务项目 -> ChatService.cs 中使用这些新的 WCF 属性的示例。

[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:接口的使用

“合同的概念是构建 WCF 服务的关键。那些拥有经典 DCOM 或 COM 技术背景的人可能会惊讶地发现,WCF 合约是使用基于接口的编程技术来表达的(旧事物重现!)。虽然不是强制性的,但您的大部分 WCF 应用程序都将从定义一组 .NET 接口类型开始,这些类型用于表示给定 WCF 类型将支持的成员集合。具体来说,代表 WCF 合约的接口称为服务合同。实现它们的类(或结构)称为服务类型。”

Pro C# with .NET3.0, Apress. Andrew Troelsen

好了——一本不错的书是这样说的——但这对我们来说在代码中看起来是什么样的呢?嗯,实际的 ChatService.cs 类实现了前面提到的 IChat 接口,因此看起来是这样的:

[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession, 
    ConcurrencyMode = ConcurrencyMode.Multiple)]
public class ChatService : IChat
{

}

WCF:回调的使用

回想一下,当我提到 ServiceContractAttribute 时,我还提到它的一个属性是 CallBackContract,它被定义为允许 WCF 服务回调客户端。这种新的 WCF 方法非常好。我记得尝试用 remoting 和事件回调客户端来实现类似的功能,那一点也不好玩。我更喜欢这种方法。让我们看一下。回想一下,原始服务合同接口声明如下:

[ServiceContract(SessionMode = SessionMode.Required, 
    CallbackContract = typeof(IChatCallback))]
interface IChat
{
....
}

所以我们仍然需要定义一个接口来允许回调工作,因此一个例子可能是(如演示应用程序中所做的那样):

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:异步委托

“.NET Framework 允许您异步调用任何方法。为此,您可以定义一个与要调用的方法具有相同签名的委托;通用语言运行时会自动为此委托定义 BeginInvoke 和 EndInvoke 方法,并具有适当的签名。

BeginInvoke 方法启动异步调用。它具有要异步执行的方法相同的参数,加上两个额外的可选参数。第一个参数是一个 AsyncCallback 委托,它引用异步调用完成后要调用的方法。第二个参数是一个用户定义的对象,它将信息传递给回调方法。BeginInvoke 立即返回,不等待异步调用完成。BeginInvoke 返回一个 IAsyncResult,可用于监视异步调用的进度。

EndInvoke 方法检索异步调用的结果。它可以在 BeginInvoke 之后的任何时间调用;如果异步调用尚未完成,EndInvoke 将阻止调用线程,直到它完成。”

异步调用同步方法

WCF:创建代理

为了让客户端与 WCF 服务通信,我们需要一个代理对象。这可能是一项艰巨的任务(说实话,有点复杂)。幸运的是,就像 .NET 3.0/3.5 中的许多事物一样,提供了工具来让我们的生活更轻松(尽管您仍然需要知道它们),WCF 也不例外。它有一个名为 **“svcutil”** 的小工具,它来提供帮助。

那么,如何使用 **svcutil** 为我们的小型 WCF 服务(演示应用程序的 ChatService.exe)创建一个代理呢?好吧,我读到一篇文章说,您只需启动 WCF 服务,将 **svcutil** 指向正在运行的 WCF 服务,然后就可以以这种方式创建客户端代理。但我必须说,我根本无法让它工作。如果搜索互联网,这似乎是一个常见的抱怨。所以我让它工作的办法如下:

  1. 打开 Visual Studio 命令提示符,并切换到包含 WCF 服务的目录。
  2. 运行以下命令行:svcutil <YOUR_SERVICE.exe>
  3. 这将列出几个文件,即 *.wsdl*.xsd,以及一个 schemas.microsoft.com.2003.10.Serialization.xsd 文件。
  4. 接下来,您需要运行以下命令行:svcutil *.wsdl *.xsd /language:C# /out:MyProxy.cs /config:app.config
  5. 现在您有了两个新的客户端文件:MyProxy.csapp.config,您可以将它们复制到您的客户端应用程序。

为了让您了解 svcutil.exe 在客户端文件方面会产生什么,让我们来看看。这是 MyProxy.cs C# 文件,由 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();
    }
}

这是由 svcutil.exe 自动生成的客户端 App.Config

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

因此,正如您所看到的,这些文件可以直接在您自己的客户端应用程序中使用,以与 WCF 服务通信。但等等,我们还没有完成 svcutil.exe。回想一下,我提到了异步委托——我为什么要这样做呢?嗯,svcutil.exe 还允许我们使用命令行开关创建异步代理代码。为此,我们使用以下命令行(注意 /a 选项):

svcutil *.wsdl *.xsd /a /language:C# /out:MyProxy.cs /config:app.config

...而不是

svcutil *.wsdl *.xsd /language:C# /out:MyProxy.cs /config:app.config

我们之前使用的。这将改变我们得到的 C#(或 VB .NET)文件的格式。现在,对于每个 WCF 服务方法,我们都会得到一个异步方法。因此,我们会得到以下结果:

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

...而不是

    [System.ServiceModel.OperationContractAttribute(IsOneWay=true, 
        IsInitiating=false, Action="http://tempuri.org/IChat/Say")]
    void Say(string msg);

希望您能看到这与前面提到的 WCF:异步委托 部分的联系。但为了确保,这里是对正在发生的事情的更详细描述。使用 svcutil.exe/a 开关,您可以生成一个包含异步方法和同步方法的代理。对于原始合同中的每个操作,异步代理和合同将包含以下形式的两个额外方法:

OperationContractAttribute 提供了 AsyncPattern 布尔属性。AsyncPattern 仅对合同的客户端副本有意义。您只能将 AsyncPattern 设置为 true,前提是方法具有与 Begin<Operation>( ) 兼容的签名。定义合同还必须有一个具有与 End<Operation>( ) 兼容的签名的匹配方法。这些要求将在代理加载时进行验证。AsyncPattern 的作用是将底层同步方法与 Begin/End 对进行绑定,并将同步执行与异步执行关联起来。

当客户端调用 Begin<Operation>( ) 形式的方法且 AsyncPattern 设置为 true 时,它会告诉 WCF 不要尝试直接在服务上调用具有该名称的方法。相反,它将使用线程池中的一个线程来同步调用底层方法(由 Action 名称标识)。同步调用将阻塞来自线程池的线程,而不是调用客户端。客户端仅在将调用请求分派到线程池所需的最短时间内被阻塞。同步调用的回复方法与 End<Operation>( ) 方法相关联。

由于附件的演示应用程序对 Join 操作使用了异步方法,因此代码中使用异步委托来实现这一点。

WCF:配置

与所有 .NET 应用程序一样,WCF 应用程序允许通过配置文件进行配置。稍后将讨论这一点,目前您只需知道 WCF 应用程序可以在 App.Config 文件中配置以下项:

  • 服务地址
  • 服务类型
  • 行为配置
  • 终结点
  • 绑定类型
  • 服务安全

WPF:样式/模板

WPF 样式和模板允许我们更改标准组件的外观。这是一个文档记录非常完善的功能,但我想说的是,通过使用一点样式,就可以将一个相当普通的 ListView 转换成如下图所示的 ListView。我认为相当不错。这是如何做到的呢?嗯,这完全取决于样式。下图显示了一个标准的 ListView 项,它经过了样式设置并分配了一些自定义数据模板。ListView 中的每个项实际上是一个 Common.Person 对象,稍后将对此进行讨论。

Screenshot - listview.png

所做的就是我应用了一个样式到标准的 .NET ListView 控件。这是实现这一点的 XAML。不过,我说过我不会过多纠缠于这些 WPF 功能,因为它们文档齐全,而且并非本文的主要内容。我只是想让那些以前没接触过 WPF 的读者知道 WPF 可以做什么。

 <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:动画

动画是 WPF 的另一个元素(同样文档齐全,所以我不会深入探讨)。在演示应用程序中,我没有过度使用动画,但我确实使用了两次动画(因为如果开发 WPF 东西,就必须用)。

一次用于加载 ChatControl,一次用于隐藏 ChatControl。唯一特别的是我使用动画的方式。它们是 Window1.xaml 的一部分,但触发器是在代码隐藏逻辑中执行的。这可能对某些人有用,我将举一个小例子。

Window1.xaml 中,我声明了一个动画如下,下面这个动画通过在 1 秒内从 0 X/Y 比例增长到 100% X/Y 比例来加载 ChatControl,并在用户单击 ListView 项时触发。

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

我直接从代码隐藏触发此动画。那么我该怎么做呢?好吧,让我们看一下代码,好吗?这相当容易;实现这一点就是这样做:

//get Storyboard animation from window resources
((Storyboard)this.Resources["showChatWindow"]).Begin(this);

WPF:数据绑定

我在 Window1.xaml 中使用的样式化 ListView 利用数据绑定来绑定 Person 对象列表。这是通过使用模板实现的,正如在 WPF : Styles/Templates 部分中所示。但万一您对所有这些 WPF 内容不太确定,实际发生的是我使用了一个 DataTemplate,它指定了 ListView 如何显示其数据。为了做到这一点,我定义了以下 DataTemplates,这些 DataTemplates 包含 Binding 值。这允许将 Person 对象集合绑定到 ListView

  <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:WPF 应用程序的多线程处理

WPF 中的线程处理与 .NET 2.0/WinForms 非常相似,您仍然会遇到不在 UI 组件同一所有者线程上的线程需要封送到正确线程的问题。唯一的区别是关键字。例如,在 .NET 2.0 中,人们可能会这样做:

if (this.InvokeRequired)
{
    this.Invoke(new EventHandler(delegate
    {
        progressBar1.Value = e.ProgressValue;
    }));
}
else
{
    progressBar1.Value = e.ProgressValue;
}

...而在 WPF 中,我们会(并且我确实)使用以下语法。注意:CheckAccess() 被标记为 [Browsable(false)],所以不要指望在 IntelliSense 中看到它。但是,它确实有效。

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

好吧,你知道吗,如果你能看到这里而不睡着,我认为你已经准备好处理附件演示应用程序的内部工作了。现在应该很容易了,因为我们已经涵盖了所有关键元素。没有什么新的要说的了,除了演示应用程序如何使用所有这些东西(尽管其中一些我们已经讨论过)。所以现在只需要解释一下。

演示应用程序中的实现方式

好吧,我们从一个序列图开始(我知道 UML 对分布式应用程序来说不是那么好,所以我已经用注释对其进行了标注,但希望您能了解大概意思)。

Screenshot - Join_Operations.png

我很抱歉这张图上的文字很小,这是由于 Code Project 对图像大小的限制。我甚至会给你们一些类图,给那些喜欢它们的人。请记住,有 3 个独立的程序集(ChatService / Common / WPFChatter),我之前已经提到过。

ChatService

Screenshot - ChatService.png

为了使此服务正常工作,需要一个特殊的配置文件。也可以在代码中完成,但 App.Config 更具灵活性。所以,让我们看一下 ChatService.exe App.Config,好吗?嗯,它看起来是这样的:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <appSettings>

    <add key="addr" value="net.tcp://: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>

正如您所看到的,这个 App.Config 文件包含了使服务能够运行所需的所有信息。WCF 支持许多不同的绑定选项,例如:

绑定类型 描述
BasicHttpBinding 一种适合与符合 WS-Basic Profile 的 Web 服务通信的绑定,例如 ASMX 基础的服务。此绑定使用 HTTP 作为传输,并使用 Text/XML 作为默认消息编码。
WSHttpBinding 一种安全且可互操作的绑定,适用于非双工服务合同。
WSDualHttpBinding 一种安全且可互操作的绑定,适用于双工服务合同或通过 SOAP 中间件进行通信。
WSFederationHttpBinding 一种安全且可互操作的绑定,支持 WS-Federation 协议,使联盟中的组织能够有效地进行用户身份验证和授权。
NetTcpBinding 一种安全且经过优化的绑定,适用于 WCF 应用程序之间的跨计算机通信。
NetNamedPipeBinding

一种安全、可靠、经过优化的绑定,适用于 WCF 应用程序之间的单机通信。

NetMsmqBinding 一种队列绑定,适用于 WCF 应用程序之间的跨计算机通信。
NetPeerTcpBinding

一种实现安全、多计算机通信的绑定。

MsmqIntegrationBinding 一种适合 WCF 应用程序与现有 MSMQ 应用程序之间进行跨计算机通信的绑定。

对于演示应用程序,我使用的是 NetTcpBinding。有关 WCF 应用程序中绑定的更多信息,请参阅 此链接

Common

这是一个非常简单的类,由 ChatServiceWPFChatter 程序集使用。该类表示一个聊天者,由于应用于该类的特殊 WCF 注释,它可以作为 WCF 通道传输。整个类列在下面,因为它不大。

Screenshot - Common.png
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()
        {
        }
        
        /// <summary>

        /// Assign constructor
        /// </summary>
        /// <param name="imageURL">Image url to allow a picture to be 
        /// created for this chatter</param>
        /// <param name="name">The name to use for this chatter</param>

        public Person(string imageURL, string name)
        {
            this.imageURL = imageURL;
            this.name = name;
        }
        #endregion
        #region Public Properties
        /// <summary>

        /// The chatters image url
        /// </summary>
        [DataMember]
        public string ImageURL
        {
            get { return imageURL; }
            set
            {
                imageURL = value;
                // Call OnPropertyChanged whenever the property is updated
                OnPropertyChanged("ImageURL");
            }
        }
        /// <summary>
        /// The chatters Name
        /// </summary>

        [DataMember]
        public string Name
        {
            get { return name; }
            set
            {
                name = value;
                // Call OnPropertyChanged whenever the property is updated
                OnPropertyChanged("Name");
            }
        }
        #endregion
        #region OnPropertyChanged (for correct well behaved databinding)
        /// <summary>
        /// Notifies the parent bindings (if any) that a property
        /// value changed and that the binding needs updating
        /// </summary>

        /// <param name="propValue">The property which changed</param>
        protected void OnPropertyChanged(string propValue)
        {
            PropertyChangedEventHandler handler = PropertyChanged;
            if (handler != null)
            {
                handler(this, new PropertyChangedEventArgs(propValue));
            }
        }
        #endregion
    }
    #endregion
}

正如您所看到的,该类具有一些尚未讨论过的特殊属性。它还继承自一个奇怪的接口。这都是关于什么的呢?嗯,[DataContract] 指定类型定义或实现了数据协定,并且可以被 DataContractSerializer 等序列化器序列化。INotifyPropertyChanged 接口是一个特殊的 WPF 接口,它允许类通知绑定容器值发生了更改。因此,当属性更改时,绑定容器将被告知。在我的情况下,这适用于 Window1.xaml 中的 ListView,正如在 WPF:数据绑定 部分中所述。

WPFChatter

Screenshot - WPFChatter.png

app.config 文件和 WCF 代理是使用 WCF scvutil.exe 工具创建的,正如在 WCF:创建代理 部分中所述。完成的 WPF 应用程序如下图所示:

Screenshot - Main2.png

Screenshot - Main3.png

现在,我将解释一下所有 WCF/WPF 内容是如何协同工作的,并使用附加的演示应用程序。我认为最好的起点是描述实际的 ChatService(服务器端)以及它为何如此装饰。我不会深入研究 ChatService.cs 文件的所有内部工作原理,因为这将在后面讨论。

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

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

    public class ChatEventArgs : EventArgs
    {
        public MessageType msgType;
        public Person person;
        public string message;
    }

    [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession, 
        ConcurrencyMode = ConcurrencyMode.Multiple)]
    public class ChatService : IChat
    {
    ...
    }

这里有几点需要注意:

  1. 有一个 IChat 接口,由 ChatService(服务器端)类实现。这是为了使 ChatService 实现一个在编译时已知的通用聊天接口。每个接口方法都用新的 WCF [OperationContract] 属性进行装饰。我之前在本篇文章中讨论了此属性的所有可能值。这里的经验法则是,如果方法需要返回值,则 IsOneWay = true。否则,它将是 IsOneWay = false。所有这些方法都标记了 IsInitiatingIsTerminating 属性。由于 Join 方法正在初始化服务,因此它被标记为 IsInitiating = true,并且由于 Leave 方法正在退出服务,因此它被标记为 IsTerminating = true
  2. ChatService 类中还包含另一个 IChatCallBack 接口。这是为了允许 ChatService 使用一个在编译时已知的已知接口来回调客户端。可以看到,服务器实现的 IChat 接口实际上包含额外的注释,用于说明是否需要回调。让我们来看一下 [ServiceContract(SessionMode = SessionMode.Required, CallbackContract = typeof(IChatCallback))]。注意 CallbackContract 如何被指定为 IChatCallback 类型。这就是 WCF 服务知道如何回调客户端的方式。显然,仅有 CallbackContract 属性而没有实际的 IChatCallback 接口是不够的。您需要两者。
  3. 最后,我们需要注意的是,实际的 ChatService 还用额外的属性进行了装饰,这些属性允许正确管理服务本身。我使用的是 InstanceContextMode = InstanceContextMode.PerSessionPerSession 指示服务应用程序在客户端和服务应用程序之间建立新的通信会话时创建一个新的服务对象。同一会话中的后续调用由同一对象处理。我还使用 ConcurrencyMode = ConcurrencyMode.MultipleMultiple 值表示服务对象可以随时由多个线程执行。在这种情况下,您必须确保线程安全。

因此,ChatService 的 WCF 属性已解释完毕,但客户端代码怎么样?请记住,我们使用 svcutil.exe 以前面描述的方式创建了代理对象。所以我不会详细介绍。但是,完整的代理代码(在演示代码的 WPFChatter 项目中,ChatService.cs)如下所示。通过上面的描述以及前面提到的属性表,我希望您能理解其中的内容。

using Common;

/// <summary>
/// This class was auto generated by the svcutil.exe utility. 
/// The www.codeprject.com article will explain how this class
/// was generated, for those readers that just need to know. 
/// Basically, anyone like me.
/// </summary>
#region IChat interface
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", 
    "3.0.0.0")]
[System.ServiceModel.ServiceContractAttribute(CallbackContract = typeof(
    IChatCallback), SessionMode = System.ServiceModel.SessionMode.Required)]
public interface IChat
{
    [System.ServiceModel.OperationContractAttribute(AsyncPattern = true, 
        Action = http://tempuri.org/IChat/Join
        ReplyAction = "http://tempuri.org/IChat/JoinResponse")]
    System.IAsyncResult BeginJoin(Person name, System.AsyncCallback callback,
        object asyncState);

    Person[] EndJoin(System.IAsyncResult result);

    [System.ServiceModel.OperationContractAttribute(IsOneWay = true, 
        IsInitiating = false, Action = "http://tempuri.org/IChat/Leave")]
    void Leave();

    [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);
}
#endregion
#region IChatCallback interface 
[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(Person sender, string message);

    [System.ServiceModel.OperationContractAttribute(IsOneWay = true, 
        Action = "http://tempuri.org/IChat/ReceiveWhisper")]
    void ReceiveWhisper(Person sender, string message);

    [System.ServiceModel.OperationContractAttribute(IsOneWay = true, 
        Action = "http://tempuri.org/IChat/UserEnter")]
    void UserEnter(Person person);

    [System.ServiceModel.OperationContractAttribute(IsOneWay = true, 
        Action = "http://tempuri.org/IChat/UserLeave")]
    void UserLeave(Person person);
}
#endregion
#region IChatChannel interface
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", 
    "3.0.0.0")]
public interface IChatChannel : IChat, System.ServiceModel.IClientChannel
{
}
#endregion
#region ChatProxy class
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", 
    "3.0.0.0")]
public partial class ChatProxy : System.ServiceModel.DuplexClientBase<IChat>,
    IChat
{
    public ChatProxy(System.ServiceModel.InstanceContext callbackInstance)
        :
            base(callbackInstance)
    {
    }

    public ChatProxy(System.ServiceModel.InstanceContext callbackInstance, 
        string endpointConfigurationName)
        :
            base(callbackInstance, endpointConfigurationName)
    {
    }

    public ChatProxy(System.ServiceModel.InstanceContext callbackInstance, 
        string endpointConfigurationName, string remoteAddress)
        :
            base(callbackInstance, endpointConfigurationName, remoteAddress)
    {
    }

    public ChatProxy(System.ServiceModel.InstanceContext callbackInstance, 
        string endpointConfigurationName, 
        System.ServiceModel.EndpointAddress remoteAddress)
        :
            base(callbackInstance, endpointConfigurationName, remoteAddress)
    {
    }

    public ChatProxy(System.ServiceModel.InstanceContext callbackInstance, 
        System.ServiceModel.Channels.Binding binding, 
        System.ServiceModel.EndpointAddress remoteAddress)
        :
            base(callbackInstance, binding, remoteAddress)
    {
    }

    public System.IAsyncResult BeginJoin(Person name, 
        System.AsyncCallback callback, object asyncState)
    {
        return base.Channel.BeginJoin(name, callback, asyncState);
    }

    public Person[] EndJoin(System.IAsyncResult result)
    {
        return base.Channel.EndJoin(result);
    }

    public void Leave()
    {
        base.Channel.Leave();
    }

    public void Say(string msg)
    {
        base.Channel.Say(msg);
    }

    public void Whisper(string to, string msg)
    {
        base.Channel.Whisper(to, msg);
    }
}
#endregion 

好了,现在我已经描述了服务器/客户端用于进行通信的骨架样板代码,让我们深入了解实际代码如何利用 ChatService(服务器端)和也称为 ChatService 的客户端代理对象。我认为最好的方法是列出其中一个操作(例如 Join),并从客户端到服务器跟踪它。

不过,在我开始全面介绍这些内容之前,有一点需要注意,我需要在附件的 WPF 项目的几个不同窗口/控件中调用客户端代理对象的方法。为此,有一个名为 Proxy_Singleton.cs 的类,它实际上只是围绕所有需要为 ChatService 正确运行而执行的操作的一个单例包装器对象。它只需允许客户端代码获取当前的单例对象并调用其方法,而无需担心实例化新的代理对象或跟踪正确的代理对象。

Join

对于 Join 操作,观察到以下序列:

  1. 用户在 SignInControl.xaml 控件上输入名字并分配图像,在那里创建了一个新的 Person 对象,该对象可以通过公共属性访问。然后,该控件引发 AddButtonClickEvent,容器 Window1.xaml 正在监听此事件。容器 Window1.xaml 然后隐藏 SignInControl.xaml 控件。它获取由 SignInControl.xaml 创建的新 Person 对象,并调用 Proxy_Singleton.csConnect 方法,将新 Person 对象传递给它。
  2. Window1.xamlChatControl.xaml 都被连接起来以订阅 Proxy_Singleton.csProxyEvent 事件和 ProxyCallBackEvent 事件,这样当 Proxy_Singleton.cs 从服务器(另一个聊天者)收到回调消息时,它将触发这些事件,所有监听方都会知道如何处理收到的数据。
  3. Proxy_Singleton.cs Connect 方法使用异步连接调用 ChatService(本地代理)上的 Join 方法。回想一下,我告诉过您关于异步委托以及 svcutil.exe 工具如何处理异步连接。
  4. Proxy_Singleton.cs Connect 方法最终(通过 WCF 魔术,实际上是 tcpBninding)调用 ChatService(服务器)的 Join 方法。这将把新的聊天者列表返回给所有已连接的聊天者。服务器通过维护一个多播委托来实现这一点,该委托保存了一个类型为 public delegate void ChatEventHandler(object sender, ChatEventArgs e); 的委托列表。每个已连接的聊天者都有一个。基本上,当服务器上的任何一个 IChat 方法被调用时,该客户端的委托也可能被调用(取决于消息类型)。显然,有些消息(如 Whisper)是私有的。因此,仅调用接收者的特定委托。尽管如此,机制是相同的。当服务器收到消息时,会检查消息类型。如果是私有的,则仅调用接收者(客户端)的消息的特定委托。否则,将调用所有附加的客户端委托。当服务器调用委托时,它将使用 IChatCallback 接口回调到客户端。由于 Proxy_Singleton.cs Connect 类实现了 IChatCallback,因此它能够接收和处理回调消息。它通常会通过内部生成的事件将回调消息提供给其他对象,如前所述,Window1.xamlChatControl.xaml 已订阅这些事件。

这可能需要一些代码片段来帮助解释,所以让我们从 Proxy_Singleton.cs Connect 代码开始。

        ///<summary>
        /// Begins an asynchronous join operation on the 
        /// underlying <see cref="ChatProxy">ChatProxy</see>

        /// which will call the OnEndJoin() method on completion
        /// </summary>
        /// <param name="p">The <see cref="Common.Person">chatter</see> 
        /// to try and join with</param>
        public void Connect(Person p)
        {
            InstanceContext site = new InstanceContext(this);
            proxy = new ChatProxy(site);
            IAsyncResult iar = proxy.BeginJoin(p, 
                new AsyncCallback(OnEndJoin), null);
        }

        /// <summary>

        /// Is called as a callback from the asynchronous call, 
        /// so simply get the list of 
        /// <see cref="Common.Person">Chatters</see> that will
        /// be yielded as part of the Asynch Join call
        /// </summary>
        /// <param name="iar">The asnch result</param>
        private void OnEndJoin(IAsyncResult iar)
        {
            try
            {
                Person[] list = proxy.EndJoin(iar);
                HandleEndJoin(list);
            }
            catch (Exception e)
            {
                MessageBox.Show(e.Message, "Error",
                    MessageBoxButton.OK, MessageBoxImage.Error); 
            }
        }
        /// <summary>
        /// If the input list is not empty, then call the 
        /// OnProxyEvent() event, to raise the event for subscribers
        /// </summary>
        /// <param name="list">The list of 
        /// <see cref="Common.Person">Chatters</see> </param>
        private void HandleEndJoin(Person[] list)
        {

            if (list == null)
            {
                MessageBox.Show("Error: List is empty", "Error",
                    MessageBoxButton.OK, MessageBoxImage.Error); 
                ExitChatSession();
            }
            else
            {
                ProxyEventArgs e = new ProxyEventArgs();
                e.list = list;
                OnProxyEvent(e);
            }
        }

        /// <summary>
        /// Raises the event for connected subscribers
        /// </summary>
        /// <param name="e"><see cref=
        ///     "ProxyCallBackEventArgs">ProxyCallBackEventArgs</see> 
        /// event args</param>
        protected void OnProxyCallBackEvent(ProxyCallBackEventArgs e)
        {
            if (ProxyCallBackEvent != null)
            {
                // Invokes the delegates. 
                ProxyCallBackEvent(this, e);
            }
        }
        /// <summary>
        /// Raises the event for connected subscribers
        /// </summary>
        /// <param name="e"><see cref=
        ///     "ProxyEventArgs">ProxyEventArgs</see> 
        /// event args</param>
        protected void OnProxyEvent(ProxyEventArgs e)
        {
            if (ProxyEvent != null)
            {
                // Invokes the delegates. 
                ProxyEvent(this, e);
            }
        }
        .....
        /// <summary>

        /// New chatter entered chat room, so call the 
        /// internal UserEnterLeave() method passing it the input parameters
        /// and the <see cref="CallBackType">CallBackType.UserEnter</see>
        /// type
        /// </summary>
        /// <param name="sender">The <see cref=
        ///     "Common.Person">current chatter</see></param>

        /// <param name="message">The message</param>
        public void UserEnter(Person person)
        {
            ....
        }

这样最终会调用 ChatService(服务器)中的 Join 方法,然后该方法使用 IChatCallback 通过相应的 IChatCallback 方法回调客户端。例如:

  • ChatService 中的 Join 方法将导致调用 IChatCallback UserEnter 方法。
  • ChatService 中的 Leave 方法将导致调用 IChatCallback UserLeave 方法。
  • ChatService 中的 Say 方法将导致调用 IChatCallback Receive 方法。
  • ChatService 中的 Whisper 方法将导致调用 IChatCallback ReceiveWhisper 方法。

那么,让我们看一下服务器端的 Join/UserEnter 操作。

        /// <summary>
        /// Takes a <see cref="Common.Person">Person</see> and allows them
        /// to join the chat room, if there is not already a chatter with
        /// the same name
        /// </summary>

        /// <param name="person"><see cref=
        ///    "Common.Person">Person</see> joining</param>
        /// <returns>An array of <see cref="Common.Person">Person</see> 
        ///     objects</returns>

        public Person[] Join(Person person)
        {
            bool userAdded = false;
            //create a new ChatEventHandler delegate, pointing to the 
            //MyEventHandler() method
            myEventHandler = new ChatEventHandler(MyEventHandler);

            //carry out a critical section that checks to see if the new 
            //chatter name is already in use, if its not allow the new 
            //chatter to be added to the list of chatters, using the 
            //person as the key, and the
            //ChatEventHandler delegate as the value, for later invocation
            lock (syncObj)
            {
                if (!checkIfPersonExists(person.Name) && person != null)
                {
                    this.person = person;
                    chatters.Add(person, MyEventHandler);
                    userAdded = true;
                }
            }

            //if the new chatter could be successfully added, get a 
            //callback instance create a new message, and broadcast it to 
            //all other chatters, and then return the list of al chatters 
            //such that connected clients may show a list of all the chatters
            if (userAdded)
            {
                callback = 
                 OperationContext.Current.GetCallbackChannel<IChatCallback>();
                ChatEventArgs e = new ChatEventArgs();
                e.msgType = MessageType.UserEnter;
                e.person = this.person;
                BroadcastMessage(e);
                //add this newly joined chatters ChatEventHandler delegate, 
                //to the global multicast delegate for invocation
                ChatEvent += myEventHandler;
                Person[] list = new Person[chatters.Count];
                //carry out a critical section that copy all chatters to 
                //a new list
                lock (syncObj)
                {
                    chatters.Keys.CopyTo(list, 0);
                }
                return list;
            }
            else
            {
                return null;
            }
        }
    ....

        /// <summary>
        /// This method is called when ever one of the chatters
        /// ChatEventHandler delegates is invoked. When this method
        /// is called it will examine the events ChatEventArgs to see
        /// what type of message is being broadcast, and will then
        /// call the corresponding method on the clients callback interface
        /// </summary>
        /// <param name="sender">the sender, which is not used</param>

        /// <param name="e">The ChatEventArgs</param>
        private void MyEventHandler(object sender, ChatEventArgs e)
        {
            try
            {
                switch (e.msgType)
                {
                    case MessageType.Receive:
                        callback.Receive(e.person, e.message);
                        break;
                    case MessageType.ReceiveWhisper:
                        callback.ReceiveWhisper(e.person, e.message);
                        break;
                    case MessageType.UserEnter:
                        callback.UserEnter(e.person);
                        break;
                    case MessageType.UserLeave:
                        callback.UserLeave(e.person);
                        break;
                }
            }
            catch
            {
                Leave();
            }
        }    
    .....

        /// <summary>
        ///loop through all connected chatters and invoke their 
        ///ChatEventHandler delegate asynchronously, which will firstly call
        ///the MyEventHandler() method and will allow a Asynch callback to 
        ///call
        ///the EndAsync() method on completion of the initial call
        /// </summary>
        /// <param name="e">The ChatEventArgs to use to send to all 
        /// connected chatters</param>

        private void BroadcastMessage(ChatEventArgs e)
        {

            ChatEventHandler temp = ChatEvent;

            //loop through all connected chatters and invoke their 
            //ChatEventHandler delegate asynchronously, which will firstly 
            //call the MyEventHandler() method and will allow a asynch 
            //callback to call the EndAsync() method on completion of the 
            //initial call
            if (temp != null)
            {
                foreach (ChatEventHandler handler in temp.GetInvocationList())
                {
                    handler.BeginInvoke(this, e, new AsyncCallback(EndAsync),
                        null);
                }
            }
        }

Say

Say 操作的方法与上面描述的 Join 操作类似,但有以下主要区别:

  1. Say 操作不是异步调用的。
  2. ChatService 中的 Say 方法将导致调用 IChatCallback Receive 方法。
  3. Say 操作不是私有的,因此将调用所有客户端委托(在服务器端)。

Whisper

Whisper 操作的方法与 Join 操作类似,但有以下主要区别:

  1. Whisper 操作不是异步调用的。
  2. ChatService 中的 Whisper 方法将导致调用 IChatCallback ReceiveWhisper 方法。
  3. Whisper 操作是私有的,因此只调用接收者在服务器端的客户端委托。

Leave

Leave 操作的方法与 Join 操作类似,但有以下主要区别:

  1. Leave 操作不是异步调用的。
  2. ChatService 中的 Leave 方法将导致调用 IChatCallback UserLeave 方法。
  3. Leave 操作不是私有的,因此将调用所有客户端委托(在服务器端)。

就这些

我希望本文能说明所有这些新的微软技术实际上可以很好地协同工作。我必须说,我认为这个应用程序看起来很酷。我一直在摆弄 WPF,但没什么认真的,但有了这个,我试着去尝试。所以希望你们中的一些人喜欢它。

您怎么看?

我只想问一下,如果您喜欢这篇文章,请投票支持。另外,请留下评论,因为它让我知道文章是否达到了适当的水平,以及它是否包含了人们需要知道的内容。

结论

我很高兴写这篇 WPF/WCF 文章,我现在已经完全沉迷于 .NET 3.0/3.5 了。所以,如果您想保持工作/生活平衡,我建议您尽可能远离 .NET 3.0/3.5,因为一旦它抓住您的小触角,就真的无法逃脱了。“抵抗是徒劳的。你们将被同化。”

话虽如此,一旦您掌握了 .NET 3.0 的庞大学习曲线, IMHO 它比以前的 .NET 2.0 产品要好得多。

历史

  • v1.0 2007/07/23:首次发布
  • 2011/07/22:更新下载演示 zip 文件

其他

我个人要感谢 Josh Smith,感谢他对 WPF 中多线程处理的帮助和建议,以及他似乎无处不在的 WPF 创新。他是所有 WPF 知识的宝贵来源,所以花些时间访问他的 博客,那里有很多好的资源。

© . All rights reserved.