代码改变世界

Silverlight4配合WCF net.tcp实现在线聊天应用攻略2

2010-07-01 01:33  NicolasZhang  阅读(1861)  评论(14编辑  收藏  举报

       接上篇,完成了开发文档中的例子后我学习了一下小笨蛋写的例子,但是发现他并没有把应用写完,我就狗尾续貂吧再来说道说道。

       上代码,在VS2010中创建一个Silverlight4的项目并使用ASP.NET网站承载。先实现服务端功能,由于要使用WCF net.tcp Activation服务所以在Web项目引用中添加对System.ServiceModel.Activation的引用。

       首先在Web项目新建一个接口,定义WCF服务接口和回调接口用以规约服务端与客户端通信的方式:

IDuplexService.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
using System.Threading;

namespace MyWebChat.Web
{
    // NOTE: You can use the "Rename" command on the "Refactor" menu to change the interface name "IDuplexService" in both code and config file together.
    /// <summary>
    /// WCF服务接口。定义Session模式为必须使用Session,定义回调契约类型为IChatCallBack
    /// </summary>
    [ServiceContract(SessionMode = SessionMode.Required, CallbackContract = typeof(IChatCallback))]
    public interface IChatService
    {
        //定义操作契约
        //IsOneWay = false,等待服务器完成对方法处理
        //IsInitiating = true,启动Session会话,
        //IsTerminating = false,设置服务器发送回复后不关闭会话
        /// <summary>
        /// 用户加入
        /// </summary>
        /// <param name="name">用户昵称</param>
        /// <returns>返回当前在线用户数组</returns>
        [OperationContract(IsOneWay = false, IsInitiating = true, IsTerminating = false)]
        string[] Join(string name);

        /// <summary>
        /// 用户群发消息
        /// </summary>
        /// <param name="senderName">消息发送者昵称</param>
        /// <param name="msg">消息内容</param>
        [OperationContract(IsOneWay = true, IsInitiating = false, IsTerminating = false)]
        void Say(string senderName, string msg);

        /// <summary>
        /// 用户发送私聊消息
        /// </summary>
        /// <param name="to">私聊接受者</param>
        /// <param name="msg">消息内容</param>
        [OperationContract(IsOneWay = true, IsInitiating = false, IsTerminating = false)]
        void Whisper(string to, string msg);

        //设置IsTerminating =true,用户离开后关闭Session
        /// <summary>
        /// 用户离开聊天室
        /// </summary>
        [OperationContract(IsOneWay = true, IsInitiating = false, IsTerminating = true)]
        void Leave();
    }

    /// <summary>
    /// 双向通信的回调接口
    /// </summary>
    public interface IChatCallback
    {
        /// <summary>
        /// 客户端接收群发消息
        /// </summary>
        /// <param name="senderName"></param>
        /// <param name="message"></param>
        [OperationContract(IsOneWay = true)]
        void Receive(string senderName, string message);

        /// <summary>
        /// 客户端接收私聊消息
        /// </summary>
        /// <param name="senderName">发送者昵称</param>
        /// <param name="message">消息内容</param>
        [OperationContract(IsOneWay = true)]
        void ReceiveWhisper(string senderName, string message);

        /// <summary>
        /// 用户进入
        /// </summary>
        /// <param name="name">进入的用户昵称</param>
        [OperationContract(IsOneWay = true)]
        void UserEnter(string name);

        /// <summary>
        /// 用户离开
        /// </summary>
        /// <param name="name">离开的用户昵称</param>
        [OperationContract(IsOneWay = true)]
        void UserLeave(string name);

        /// <summary>
        /// 更新在线用户列表
        /// </summary>
        /// <param name="list">在线用户列表</param>
        [OperationContract(IsOneWay = true)]
        void UpdateOnlineUserList(string[] list);
    }

    /// <summary>
    /// 设定消息的类型
    /// </summary>
    public enum MessageType { UserEnter, UserLeave, Receive, ReceiveWhisper, UpdateOnlineUserList };

    /// <summary>
    /// 定义一个本例的事件消息类. 创建包含有关事件的其他有用的信息的变量,只要派生自EventArgs即可。
    /// </summary>
    public class ChatEventArgs : EventArgs
    {
        public MessageType msgType;
        public string name;
        public string message;
    }
}

          接着是最重要的部分,创建一个Silverlight-enabled WCF Service用以实现刚才定义的服务端接口。小笨蛋的注释写的比较详细我又加了一些应该比较清楚了: IDuplexService.svc.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
using System.Threading;
using System.ServiceModel.Activation;

namespace MyWebChat.Web
{
    //// InstanceContextMode.PerSession 服务器为每个客户会话创建一个新的上下文对象。ConcurrencyMode.Multiple 异步的多线程实例
    [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession, ConcurrencyMode = ConcurrencyMode.Multiple)]
    [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
    public class ChatService : IChatService
    {
        private static Object syncObj = new Object();////定义一个静态对象用于线程部份代码块的锁定,用于lock操作
        IChatCallback callback = null;  //双向通信的回调接口
        public delegate void ChatEventHandler(object sender, ChatEventArgs e);//定义用于把处理程序赋予给事件的委托。
        public static event ChatEventHandler ChatEvent;//定义事件
        static Dictionary<string, ChatEventHandler> chatters = new Dictionary<string, ChatEventHandler>();//创建一个静态Dictionary(表示键和值)集合(字典),用于记录在线成员,Dictionary<(Of <(TKey, TValue>)>) 泛型类

        private string name;
        private ChatEventHandler myEventHandler = null;


        /// <summary>
        /// 用户加入
        /// </summary>
        /// <param name="name">登录用户的昵称</param>
        /// <returns>当前用户昵称列表</returns>
        public string[] Join(string name)
        {
            bool userAdded = false;
            myEventHandler = new ChatEventHandler(MyEventHandler);//将MyEventHandler方法作为参数传递给委托
            lock (syncObj)//线程的同步性,同步访问多个线程的任何变量,利用lock(独占锁),确保数据访问的唯一性。
            {
                //判断在线用户列表中是否已经存在此用户昵称,昵称为空和null的情况可以从客户端判断
                if (name != null && name != "" && !chatters.ContainsKey(name))
                {
                    this.name = name;
                    chatters.Add(name, MyEventHandler);//把当前用户加入到在线用户列表中
                    userAdded = true;
                }
            }
            if (userAdded)
            {
                /*
                 * 获取当前操作客户端实例的通道给IChatCallback接口的实例callback,此通道是一个定义为IChatCallback类型的泛类型,
                 * 通道的类型是事先服务契约协定好的双工机制。
                 */
                callback = OperationContext.Current.GetCallbackChannel<IChatCallback>();
                //实例化事件消息类ChatEventArgs
                ChatEventArgs e = new ChatEventArgs
                {
                    msgType = MessageType.UserEnter,
                    name = name
                };
                BroadcastMessage(e);
                UpdateOnlineUserList();//更新其他用户的在线用户列表
                ChatEvent += myEventHandler;
                string[] list = new string[chatters.Count];
                //以下代码返回当前进入聊天室成员的称列表
                lock (syncObj)
                {
                    chatters.Keys.CopyTo(list, 0);//将字典中记录的用户信息复制到数组中返回。
                }
                return list;
            }
            else
            {
                return null;
            }
        }


        /// <summary>
        /// 广播消息
        /// </summary>
        /// <param name="e"></param>
        private void BroadcastMessage(ChatEventArgs e)
        {
            switch (e.msgType)
            {
                //广播用户进入消息
                case MessageType.UserEnter:
                    Say("系统消息", "欢迎[" + e.name + "]加入聊天室!");
                    break;
                //广播用户离开消息
                case MessageType.UserLeave:
                    Say("系统消息", "[" + e.name + "]已经离开聊天室!");
                    break;
                default: throw new Exception();
            }
        }

        /// <summary>
        /// 更新客户端列表
        /// </summary>
        /// <returns></returns>
        public string[] GetOnlineUserList()
        {
            string[] list = new string[chatters.Count];
            //以下代码返回当前进入聊天室成员的称列表
            lock (syncObj)
            {
                chatters.Keys.CopyTo(list, 0);//将字典中记录的用户信息复制到数组中返回。
            }
            return list;
        }

        /// <summary>
        /// 对所有人说
        /// </summary>
        /// <param name="msg"></param>
        public void Say(string senderName, string msg)
        {
            ChatEventArgs e = new ChatEventArgs
            {
                msgType = MessageType.Receive,
                name = senderName,
                message = msg
            };
            try
            {
                ChatEventHandler chatterTo;//创建一个临时委托实例
                lock (syncObj)
                {
                    foreach (var chatter in chatters)
                    {
                        //不再发送给发送者本人
                        if (chatter.Key != senderName)
                        {
                            chatterTo = chatter.Value; //查找成员字典中,找到要接收者的委托调用 
                            chatterTo.BeginInvoke(this, e, new AsyncCallback(EndAsync), null);//异步方式调用接收者的委托调用
                        }
                    }
                }
            }
            catch (KeyNotFoundException) { }
        }

        /// <summary>
        /// 私聊信息
        /// </summary>
        /// <param name="to"></param>
        /// <param name="msg"></param>
        public void Whisper(string to, string msg)
        {
            ChatEventArgs e = new ChatEventArgs
            {
                msgType = MessageType.ReceiveWhisper,
                name = this.name,
                message = msg
            };

            try
            {
                ChatEventHandler chatterTo;//创建一个临时委托实例
                lock (syncObj)
                {
                    chatterTo = chatters[to]; //查找成员字典中,找到要接收者的委托调用
                }
                chatterTo.BeginInvoke(this, e, new AsyncCallback(EndAsync), null);//异步方式调用接收者的委托调用
            }
            catch (KeyNotFoundException) { }
        }

        /// <summary>
        /// 用户加入
        /// </summary>
        public void Leave()
        {
            if (this.name == null)
                return;

            lock (syncObj)
            {
                chatters.Remove(this.name);
            }
            ChatEvent -= myEventHandler;
            ChatEventArgs e = new ChatEventArgs
            {
                msgType = MessageType.UserLeave,
                name = this.name
            };

            this.name = null;
            BroadcastMessage(e);
            UpdateOnlineUserList();//更新其他用户的在线用户列表
        }

        /// <summary>
        /// 更新在线用户列表
        /// </summary>
        private void UpdateOnlineUserList()
        {
            ChatEventArgs e = new ChatEventArgs
            {
                msgType = MessageType.UpdateOnlineUserList
            };

            try
            {
                ChatEventHandler chatterTo;//创建一个临时委托实例
                lock (syncObj)
                {
                    foreach (var chatter in chatters)
                    {
                        chatterTo = chatter.Value; //查找成员字典中,找到要接收者的委托调用 
                        chatterTo.BeginInvoke(this, e, new AsyncCallback(EndAsync), null);//异步方式调用接收者的委托调用
                    }
                }
            }
            catch (KeyNotFoundException) { }
        }

        //回调,根据客户端动作通知对应客户端执行对应的操作
        private void MyEventHandler(object sender, ChatEventArgs e)
        {
            try
            {
                switch (e.msgType)
                {
                    case MessageType.Receive:
                        callback.Receive(e.name, e.message);
                        break;
                    case MessageType.ReceiveWhisper:
                        callback.ReceiveWhisper(e.name, e.message);
                        break;
                    case MessageType.UserEnter:
                        callback.UserEnter(e.name);
                        break;
                    case MessageType.UserLeave:
                        callback.UserLeave(e.name);
                        break;
                    case MessageType.UpdateOnlineUserList:
                        callback.UpdateOnlineUserList(GetOnlineUserList());
                        break;
                }
            }
            catch
            {
                Leave();
            }
        }

        //广播中线程调用完成的回调方法功能:清除异常多路广播委托的调用列表中异常对象(空对象)
        private void EndAsync(IAsyncResult ar)
        {
            ChatEventHandler d = null;
            try
            {
                //封装异步委托上的异步操作结果
                System.Runtime.Remoting.Messaging.AsyncResult asres = (System.Runtime.Remoting.Messaging.AsyncResult)ar;
                d = ((ChatEventHandler)asres.AsyncDelegate);
                d.EndInvoke(ar);
            }
            catch
            {
                ChatEvent -= d;
            }
        }
    }
}

        需要注意的是此处用到delegate的BeginInvoke和EndInvoke进行多线程异步处理委托,所以需注意在操作静态对象时需要加锁。 完成之后得对Web.config进行相应修改:

 Web.config

<?xml version="1.0"?>

<!--
  For more information on how to configure your ASP.NET application, please visit
  http://go.microsoft.com/fwlink/?LinkId=169433
  -->

<configuration>
  <system.web>
    <compilation debug="true" targetFramework="4.0" />
  </system.web>
  <system.serviceModel>
    <behaviors>
      <serviceBehaviors>
        <behavior name="">
          <serviceMetadata httpGetEnabled="true" />
          <serviceDebug includeExceptionDetailInFaults="false" />
        </behavior>
      </serviceBehaviors>
    </behaviors>
    <bindings>
      <netTcpBinding>
        <binding name="tcpBindingNoSecurity">
          <security mode="None" />
        </binding>
      </netTcpBinding>
    </bindings>
    <serviceHostingEnvironment aspNetCompatibilityEnabled="true"
      multipleSiteBindingsEnabled="true" />
    <services>
      <service name="MyWebChat.Web.ChatService">
        <endpoint address="net.tcp://localhost:4502/MyWebChat.Web/DuplexService.svc"
          binding="netTcpBinding" bindingConfiguration="tcpBindingNoSecurity"
          contract="MyWebChat.Web.IChatService" />
        <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" />
      </service>
    </services>
  </system.serviceModel>
</configuration>

      生成一下解决方案服务端的工作就完成了,接下来进行客户端的创建。 在Silverlight项目中添加服务引用,点击Discover便可获得刚才创建的WCF服务,设置命名空间为ChatServiceReference并引入。 若此步能正确引入此服务则说明刚才创建、配置的WCF服务正确无误,否则会遇到各种错误,这时请参考上一篇Silverlight4配合WCF net.tcp实现在线聊天应用攻略1中提到设置要点进行检查、修正并重新生成服务项目,直到正确引入为止。

      创建一个ChatServiceClientManager类以统一控制WCF服务代理的使用。 ChatServiceClientManager.cs

using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Collections.Generic;
using MyWebChat.ChatServiceReference;

namespace MyWebChat.ServiceClient
{
    public class ChatServiceClientManager
    {
       public static ChatServiceClient client = new ChatServiceClient();  //实例化一个WCF服务客户端
        public static bool hasSignedIn = false;//记录是否已经登录
        public static string currentUserName = "";//记录当前用户昵称
        public static List<string> onlineUserList = new List<string>();//记录在线用户列表

        /// <summary>
        /// 调用WCF服务给指定昵称的客户端发送消息
        /// </summary>
        /// <param name="receiverName">接收者昵称</param>
        /// <param name="messageText">消息内容</param>
        /// <returns>消息发送后更新的显示</returns>
        public static string SendMessage(string receiverName, string messageText)
        {
            if (receiverName == "所有人")
            {
                //异步调用服务端定义的Say方法
                client.SayAsync(ChatServiceClientManager.currentUserName, messageText);
                return "你对所有人说:" + messageText + Environment.NewLine;
            }
            else
            {
                //异步调用服务端定义的Whisper方法
                client.WhisperAsync(receiverName, messageText);
                return "你对" + receiverName + "悄悄说:" + messageText + Environment.NewLine;
            }
        }
    }
}

       添加一个Silverlight Child Window用来做登录界面:

 LoginWindow.xaml

<controls:ChildWindow x:Class="MyWebChat.LoginWindow"
           xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
           xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
           xmlns:controls="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls"
           Width="400" Height="255" 
           Title="登录" HasCloseButton="False" FontSize="12">
    <Canvas x:Name="LayoutRoot" Background="Beige">
        <TextBlock Text="Silverlight在线聊天室" Canvas.Left="64" Canvas.Top="53" FontSize="25"></TextBlock>
        <TextBlock Text="昵称:" Canvas.Left="77" Canvas.Top="135"  FontSize="15" Height="30" VerticalAlignment="Center"></TextBlock>
        <TextBox x:Name="txtUserName" Height="30" Width="116" FontSize="16" Canvas.Left="129" Canvas.Top="132" KeyDown="txtUserName_KeyDown" ></TextBox>
        <Button x:Name="btnSignIn" Content="登录" FontSize="15" Height="30" Width="65" Canvas.Left="251" Canvas.Top="132" Click="btnSignIn_Click"></Button>
    </Canvas>
</controls:ChildWindow>

LoginWindow.xaml.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using MyWebChat.ChatServiceReference;
using MyWebChat.ServiceClient;
namespace MyWebChat
{
    public partial class LoginWindow : ChildWindow
    {
        public string actualUserName = "";//最终用户名

        public LoginWindow()
        {
            InitializeComponent();
        }

        /// <summary>
        /// 登录按钮方法
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnSignIn_Click(object sender, RoutedEventArgs e)
        {
            //监听Join方法以获得返回的在线用户列表
            ChatServiceClientManager.client.JoinCompleted += new EventHandler<JoinCompletedEventArgs>(client_JoinCompleted);
            actualUserName = txtUserName.Text == string.Empty ? "测试用户" + DateTime.Now.ToString() : txtUserName.Text;
            ChatServiceClientManager.client.JoinAsync(actualUserName);//异步调用Join方法加入聊天室
        }

        /// <summary>
        /// 处理Join方法的返回值
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void client_JoinCompleted(object sender, JoinCompletedEventArgs e)
        {
            if (e.Result != null)
            {
                foreach (var userName in e.Result)
                {
                    ChatServiceClientManager.onlineUserList.Add(userName);
                }
                Close();//成功登录后关闭此窗口
            }
            else
            {
                MessageBox.Show("已存在重名用户,请重新输入昵称!", "提示", MessageBoxButton.OK);
                txtUserName.Text = string.Empty;
            }
        }

        /// <summary>
        /// 昵称输入框点击Enter即触发登录
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void txtUserName_KeyDown(object sender, KeyEventArgs e)
        {
            if (e.Key == Key.Enter)
            {
                btnSignIn_Click(null, null);
            }
        }
    }
}

      接下来开始制作聊天主窗体,界面做的比较烂,以后再改进吧。

 MainPage.xaml

<UserControl x:Class="MyWebChat.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    d:DesignHeight="600" d:DesignWidth="800">
    <StackPanel  x:Name="LayoutRoot" Background="White">
        <StackPanel Orientation="Horizontal" Background="AliceBlue">
            <ScrollViewer x:Name="scrollChatContentContainer" Height="500" Width="600">
                <TextBlock x:Name="tbChatContent" Margin="0" Width="575"  FontSize="15" TextWrapping="Wrap"  SizeChanged="tbChatContent_SizeChanged">
                </TextBlock>
            </ScrollViewer>
            <ListBox x:Name="lstOnlineUser" Height="500" Width="200" Background="LightGray" ScrollViewer.VerticalScrollBarVisibility="Auto"  SelectionChanged="lstOnlineUser_SelectionChanged"></ListBox>
        </StackPanel>
        <StackPanel Orientation="Horizontal">
            <StackPanel>
                <StackPanel Orientation="Horizontal">
                    <TextBlock x:Name="tbMyName"  FontSize="13"></TextBlock>
                    <TextBlock Text="对" Width="19" FontSize="13"></TextBlock>
                    <ComboBox x:Name="cbbSpeakTo" Width="200"></ComboBox>
                    <TextBlock Text="说" Width="19" TextAlignment="Center" FontSize="13"></TextBlock>
                </StackPanel>
                <TextBox x:Name="txtChatContentInput" VerticalScrollBarVisibility="Auto" TextWrapping="Wrap" Height="80" Width="600" FontSize="14"  KeyDown="txtChatContentInput_KeyDown"></TextBox>
            </StackPanel>
            <Button x:Name="btnSend" Content="发送" Height="100" Width="200" FontSize="20" Click="btnSend_Click"></Button>
        </StackPanel>
        <TextBlock x:Name="reply" Margin="0,0,0,0" FontSize="18"></TextBlock>
    </StackPanel>
</UserControl>

MainPage.xaml.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.ServiceModel;
using System.ServiceModel.Channels;
using MyWebChat.ChatServiceReference;
using System.Windows.Browser;
using MyWebChat.ServiceClient;

namespace MyWebChat
{
    public partial class MainPage : UserControl
    {
        LoginWindow loginWin = null;//登录子窗体

        public MainPage()
        {
            InitializeComponent();
            // HtmlPage.RegisterScriptableObject("MainPage", this);
            this.Loaded += new RoutedEventHandler(MainPage_Loaded);
        }

        /// <summary>
        /// 主界面加载完成事件
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void MainPage_Loaded(object sender, RoutedEventArgs e)
        {

            //为各个事件添加监听
            ChatServiceClientManager.client.ReceiveReceived += new EventHandler<ReceiveReceivedEventArgs>(client_ReceiveReceived);
            ChatServiceClientManager.client.ReceiveWhisperReceived += new EventHandler<ReceiveWhisperReceivedEventArgs>(client_ReceiveWhisperReceived);
            ChatServiceClientManager.client.UpdateOnlineUserListReceived += new EventHandler<UpdateOnlineUserListReceivedEventArgs>(client_UpdateOnlineUserListReceived);

            //打开注册子窗体
            loginWin = new LoginWindow();
            loginWin.Closed += new EventHandler(login_Closed);
            loginWin.Show();
        }

        //[ScriptableMember()]
        /// <summary>
        /// 用户退出方法,通知服务端用户退出聊天室
        /// </summary>
        public void UserQuit()
        {
            ChatServiceClientManager.client.LeaveAsync();
        }

        //收到私聊消息处理方法
        void client_ReceiveWhisperReceived(object sender, ReceiveWhisperReceivedEventArgs e)
        {
            //此处不太明白为什么Error不为空时才是有效的呢???
            if (e.Error == null)
            {
                tbChatContent.Text += "[" + e.senderName + "]悄悄对你说:" + e.message + Environment.NewLine;
            }
        }

        //收到群发消息处理方法
        void client_ReceiveReceived(object sender, ReceiveReceivedEventArgs e)
        {
            //此处不太明白为什么Error不为空时才是有效的呢???
            if (e.Error == null)
            {
                string msg = e.senderName == "系统消息" ? e.senderName + ":" + e.message :
                    "[" + e.senderName + "]对所有人说:" + e.message;
                tbChatContent.Text += msg + Environment.NewLine;
            }
        }

        //收到更新在线用户列表处理方法
        void client_UpdateOnlineUserListReceived(object sender, UpdateOnlineUserListReceivedEventArgs e)
        {
            e.list.Insert(0, "所有人");
            lstOnlineUser.ItemsSource = e.list;
            List<string> userListForSpeakTo = new List<string>();
            userListForSpeakTo.AddRange(e.list);
            userListForSpeakTo.Remove(ChatServiceClientManager.currentUserName);
            cbbSpeakTo.ItemsSource = userListForSpeakTo;
        }

        //登录窗口关闭处理方法
        void login_Closed(object sender, EventArgs e)
        {
            ChatServiceClientManager.hasSignedIn = true;
            tbMyName.Text = loginWin.actualUserName;
            ChatServiceClientManager.currentUserName = loginWin.actualUserName;
            lstOnlineUser.ItemsSource = ChatServiceClientManager.onlineUserList;
        }

        //发送消息
        private void btnSend_Click(object sender, RoutedEventArgs e)
        {
            //若用户未选择聊天对象,默认设置为对所有人说
            if (cbbSpeakTo.SelectedValue == null)
            {
                cbbSpeakTo.SelectedIndex = 0;
            }
            //不能发送空消息
            if (txtChatContentInput.Text.Trim() == string.Empty)
            {
                MessageBox.Show("不能发送空消息!", "提示", MessageBoxButton.OK);
                txtChatContentInput.Focus();
            }
            else
            {
                tbChatContent.Text += ChatServiceClientManager.SendMessage(cbbSpeakTo.SelectedValue.ToString(), txtChatContentInput.Text);
                txtChatContentInput.Text = string.Empty;
            }
        }

        /// <summary>
        /// 列表点击事件,点击在线用户列表后自动设置说话对象为所点击对象
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void lstOnlineUser_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            cbbSpeakTo.SelectedValue = lstOnlineUser.SelectedValue;
        }

        /// <summary>
        /// 聊天框按键事件,监听快捷键Ctrl+Enter发送消息
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void txtChatContentInput_KeyDown(object sender, KeyEventArgs e)
        {
            ModifierKeys keys = Keyboard.Modifiers;
            if (e.Key == Key.Enter && keys == ModifierKeys.Control)
            {
                btnSend_Click(null, null);
            }
        }

        /// <summary>
        /// 聊天内容展示区域尺寸变化事件,用来实现自动滚动到最底
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void tbChatContent_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            scrollChatContentContainer.ScrollToVerticalOffset(tbChatContent.ActualHeight);
        }
    }
}

          最后在App.xaml.cs中的Application_Exit方法中添加:

        private void Application_Exit(object sender, EventArgs e)
        {
            if (ChatServiceClientManager.hasSignedIn == true)
            {
                //用户关闭网页时通知服务端用户离开并关闭连接
                ChatServiceClientManager.client.LeaveAsync();
                ChatServiceClientManager.client.CloseAsync();
            }
        }

       终于大功告成,调试一下,程序启动后弹出登录框,登录后即可在聊天框内进行聊天了,可以自己多开几个浏览器进行测试。

       下一步准备改进一下界面,并把现在传递的string类型的消息改为富文本的以支持用户设定的字体、字号等风格,当然没有表情也是不行的,回头再尝试做个窗口抖动什么的。

      现在看程序写的比较乱而且基本是只追求功能实现,不知大牛们能不能给点改进的建议,应用点什么模式好使代码更优雅些,多谢支持!

源代码下载