WPF+ASP.NET SignalR实现简易在线聊天功能

  • WPF+ASP.NET SignalR实现简易在线聊天功能已关闭评论
  • 15 次浏览
  • A+
所属分类:.NET技术
摘要

在实际业务中,当后台数据发生变化,客户端能够实时的收到通知,而不是由用户主动的进行页面刷新才能查看,这将是一个非常人性化的设计。有没有那么一种场景,后台数据明明已经发生变化了,前台却因为没有及时刷新,而导致页面显示的数据与实际存在差异,从而造成错误的判断。那么如何才能在后台数据变更时及时通知客户端呢?本文以一个简单的聊天示例,简述如何通过WPF+ASP.NET SignalR实现消息后台通知,仅供学习分享使用,如有不足之处,还请指正。

在实际业务中,当后台数据发生变化,客户端能够实时的收到通知,而不是由用户主动的进行页面刷新才能查看,这将是一个非常人性化的设计。有没有那么一种场景,后台数据明明已经发生变化了,前台却因为没有及时刷新,而导致页面显示的数据与实际存在差异,从而造成错误的判断。那么如何才能在后台数据变更时及时通知客户端呢?本文以一个简单的聊天示例,简述如何通过WPF+ASP.NET SignalR实现消息后台通知,仅供学习分享使用,如有不足之处,还请指正。

涉及知识点

在本示例中,涉及知识点如下所示:

  1. 开发工具:Visual Studio 2022 目标框架:.NET6.0
  2. ASP.NET SignalR,一个ASP .NET 下的类库,可以在ASP .NET 的Web项目中实现实时通信,目前新版已支持.NET6.0及以上版本。在本示例中,作为消息通知的服务端。
  3. WPF,是微软推出的基于Windows 的用户界面框架,主要用于开发客户端程序。

什么是ASP.NET SignalR?

ASP.NET SignalR 是一个面向 ASP.NET 开发人员的库,可简化将实时 web 功能添加到应用程序的过程。 实时 web 功能是让服务器代码将内容推送到连接的客户端立即可用,而不是让服务器等待客户端请求新数据的能力。

WPF+ASP.NET SignalR实现简易在线聊天功能

SignalR 提供了一个简单的 API,用于创建服务器到客户端远程过程调用 (RPC) ,该调用客户端浏览器 (和其他客户端平台中的 JavaScript 函数) 服务器端 .NET 代码。 SignalR 还包括用于连接管理的 API (,例如连接和断开连接事件) ,以及分组连接。

WPF+ASP.NET SignalR实现简易在线聊天功能

虽然聊天通常被用作示例,但你可以做更多的事情。每当用户刷新网页以查看新数据时,或者该网页实施 Ajax 长轮询以检索新数据时,它都是使用 SignalR 的候选者。SignalR 还支持需要从服务器进行高频更新的全新类型的应用,例如实时游戏。

在线聊天整体架构

在线聊天示例,主要分为服务端(ASP.NET Web API)和客户端(WPF可执行程序)。具体如下所示:

WPF+ASP.NET SignalR实现简易在线聊天功能

 

 

ASP.NET SignalR在线聊天服务端

服务端主要实现消息的接收,转发等功能,具体步骤如下所示:

1. 创建ASP.NET Web API项目

首先创建ASP.NET Web API项目,默认情况下SignalR已经作为项目框架的一部分而存在,所以不需要安装,直接使用即可。通过项目--依赖性--框架--Microsoft.AspNetCore.App可以查看,如下所示

WPF+ASP.NET SignalR实现简易在线聊天功能

 

2. 创建消息通知中心Hub

在项目中新建Chat文件夹,然后创建ChatHub类,并继承Hub基类。主要包括登录(Login),聊天(Chat)等功能。如下所示:

 1 using Microsoft.AspNetCore.SignalR;  2   3 namespace SignalRChat.Chat  4 {  5     public class ChatHub:Hub  6     {  7         private static Dictionary<string,string> dictUsers = new Dictionary<string,string>();  8   9  10         public override Task OnConnectedAsync() 11         { 12             Console.WriteLine($"ID:{Context.ConnectionId} 已连接"); 13             return base.OnConnectedAsync(); 14         } 15  16         public override Task OnDisconnectedAsync(Exception? exception) 17         { 18             Console.WriteLine($"ID:{Context.ConnectionId} 已断开"); 19             return base.OnDisconnectedAsync(exception); 20         } 21  22         /// <summary> 23         /// 向客户端发送信息 24         /// </summary> 25         /// <param name="msg"></param> 26         /// <returns></returns> 27         public Task Send(string msg) {  28             return Clients.Caller.SendAsync("SendMessage",msg); 29         } 30  31         /// <summary> 32         /// 登录功能,将用户ID和ConntectionId关联起来 33         /// </summary> 34         /// <param name="userId"></param> 35         public void Login(string userId) { 36             if (!dictUsers.ContainsKey(userId)) {  37                 dictUsers[userId] = Context.ConnectionId; 38             } 39             Console.WriteLine($"{userId}登录成功,ConnectionId={Context.ConnectionId}"); 40             //向所有用户发送当前在线的用户列表 41             Clients.All.SendAsync("Users", dictUsers.Keys.ToList()); 42         } 43  44         /// <summary> 45         /// 一对一聊天 46         /// </summary> 47         /// <param name="userId"></param> 48         /// <param name="targetUserId"></param> 49         /// <param name="msg"></param> 50         public void Chat(string userId, string targetUserId, string msg) 51         { 52             string newMsg = $"{userId}|{msg}";//组装后的消息体 53             //如果当前用户在线 54             if (dictUsers.ContainsKey(targetUserId)) 55             { 56                 Clients.Client(dictUsers[targetUserId]).SendAsync("ChatInfo",newMsg); 57             } 58             else {  59                 //如果当前用户不在线,正常是保存数据库,等上线时加载,暂时不做处理 60             } 61         } 62  63         /// <summary> 64         /// 退出功能,当客户端退出时调用 65         /// </summary> 66         /// <param name="userId"></param> 67         public void Logout(string userId) 68         { 69             if (dictUsers.ContainsKey(userId)) 70             { 71                 dictUsers.Remove(userId); 72             } 73             Console.WriteLine($"{userId}退出成功,ConnectionId={Context.ConnectionId}"); 74         } 75     } 76 }

3. 注册服务和路由

聊天类创建成功后,需要配置服务注入和路由,在Program中,添加代码,如下所示:

 1 using SignalRChat.Chat;  2   3 var builder = WebApplication.CreateBuilder(args);  4   5 // Add services to the container.  6   7 builder.Services.AddControllers();  8 // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle  9 builder.Services.AddEndpointsApiExplorer(); 10 builder.Services.AddSwaggerGen(); 11 //1.添加SignalR服务 12 builder.Services.AddSignalR(); 13 var app = builder.Build(); 14  15 // Configure the HTTP request pipeline. 16 if (app.Environment.IsDevelopment()) 17 { 18     app.UseSwagger(); 19     app.UseSwaggerUI(); 20 } 21 app.UseRouting(); 22 app.UseHttpsRedirection(); 23  24 app.UseAuthorization(); 25  26 app.MapControllers(); 27 //2.映射路由 28 app.UseEndpoints(endpoints => { 29     endpoints.MapHub<ChatHub>("/chat"); 30 }); 31  32 app.Run();

4. ASP.NET SignalR中心对象生存周期

你不会实例化 Hub 类或从服务器上自己的代码调用其方法;由 SignalR Hubs 管道为你完成的所有操作。 SignalR 每次需要处理中心操作(例如客户端连接、断开连接或向服务器发出方法调用时)时,SignalR 都会创建 Hub 类的新实例。

由于 Hub 类的实例是暂时性的,因此无法使用它们来维护从一个方法调用到下一个方法的状态。 每当服务器从客户端收到方法调用时,中心类的新实例都会处理消息。 若要通过多个连接和方法调用来维护状态,请使用一些其他方法(例如数据库)或 Hub 类上的静态变量,或者不派生自 Hub的其他类。 如果在内存中保留数据,请使用 Hub 类上的静态变量等方法,则应用域回收时数据将丢失。

如果要从在 Hub 类外部运行的代码将消息发送到客户端,则无法通过实例化 Hub 类实例来执行此操作,但可以通过获取对 Hub 类的 SignalR 上下文对象的引用来执行此操作。

注意:ChatHub每次调用都是一个新的实例,所以不可以有私有属性或变量,不可以保存对像的值,所以如果需要记录一些持久保存的值,则可以采用静态变量,或者中心以外的对象。

SignalR客户端

1. 安装SignalR客户端依赖库

客户端如果要调用SignalR的值,需要通过NuGet包管理器,安装SignalR客户端,如下所示:

WPF+ASP.NET SignalR实现简易在线聊天功能

 

2. 客户端消息接收发送

在客户端实现消息的接收和发送,主要通过HubConntection实现,核心代码,如下所示:

  1 namespace SignalRClient   2 {   3     public class ChatViewModel:ObservableObject   4     {   5         #region 属性及构造函数   6    7         private string targetUserName;   8    9         public string TargetUserName  10         {  11             get { return targetUserName; }  12             set { SetProperty(ref targetUserName , value); }  13         }  14   15   16         private string userName;  17   18         public string UserName  19         {  20             get { return userName; }  21             set  22             {  23                 SetProperty(ref userName, value);  24                 Welcome = $"欢迎 {value} 来到聊天室";  25             }  26         }  27   28         private string welcome;  29   30         public string Welcome  31         {  32             get { return welcome; }  33             set { SetProperty(ref welcome , value); }  34         }  35   36         private List<string> users;  37   38         public List<string> Users  39         {  40             get { return users; }  41             set {SetProperty(ref users , value); }  42         }  43   44         private RichTextBox richTextBox;  45   46         private HubConnection hubConnection;  47   48         public ChatViewModel() {   49               50         }  51   52         #endregion  53   54         #region 命令  55   56         private ICommand loadedCommand;  57   58         public ICommand LoadedCommand  59         {  60             get  61             {  62                 if (loadedCommand == null)  63                 {  64                     loadedCommand = new RelayCommand<object>(Loaded);  65                 }  66                 return loadedCommand;  67             }  68         }  69   70         private void Loaded(object obj)  71         {  72             //1.初始化  73             InitInfo();  74             //2.监听  75             Listen();  76             //3.连接  77             Link();  78             //4.登录  79             Login();  80             //  81             if (obj != null) {  82                 var eventArgs = obj as RoutedEventArgs;  83   84                 var window= eventArgs.OriginalSource as ChatWindow;  85                 this.richTextBox = window.richTextBox;  86             }  87         }  88   89         private IRelayCommand<string> sendCommand;  90   91         public IRelayCommand<string> SendCommand  92         {  93             get {  94                 if (sendCommand == null) {  95                     sendCommand = new RelayCommand<string>(Send);  96                 }  97                 return sendCommand; }  98         }  99  100         private void Send(string msg) 101         { 102             if (string.IsNullOrEmpty(msg)) { 103                 MessageBox.Show("发送的消息为空"); 104                 return; 105             } 106             if (string.IsNullOrEmpty(this.TargetUserName)) { 107                 MessageBox.Show("发送的目标用户为空"); 108                 return ; 109             } 110             hubConnection.InvokeAsync("Chat",this.UserName,this.TargetUserName,msg); 111             if (this.richTextBox != null) 112             { 113                 Run run = new Run(); 114                 Run run1 = new Run(); 115                 Paragraph paragraph = new Paragraph(); 116                 Paragraph paragraph1 = new Paragraph(); 117                 run.Foreground = Brushes.Blue; 118                 run.Text = this.UserName; 119                 run1.Foreground= Brushes.Black; 120                 run1.Text = msg; 121                 paragraph.Inlines.Add(run); 122                 paragraph1.Inlines.Add(run1); 123                 paragraph.LineHeight = 1; 124                 paragraph.TextAlignment = TextAlignment.Right; 125                 paragraph1.LineHeight = 1; 126                 paragraph1.TextAlignment = TextAlignment.Right; 127                 this.richTextBox.Document.Blocks.Add(paragraph); 128                 this.richTextBox.Document.Blocks.Add(paragraph1); 129                 this.richTextBox.ScrollToEnd(); 130             } 131         } 132  133         #endregion 134  135         /// <summary> 136         /// 初始化Connection对象 137         /// </summary> 138         private void InitInfo() { 139             hubConnection = new HubConnectionBuilder().WithUrl("https://localhost:7149/chat").WithAutomaticReconnect().Build(); 140             hubConnection.KeepAliveInterval =TimeSpan.FromSeconds(5); 141         } 142  143         /// <summary> 144         /// 监听 145         /// </summary> 146         private void Listen() { 147             hubConnection.On<List<string>>("Users", RefreshUsers); 148             hubConnection.On<string>("ChatInfo",ReceiveInfos); 149         } 150  151         /// <summary> 152         /// 连接 153         /// </summary> 154         private async void Link() { 155             try 156             { 157                await hubConnection.StartAsync(); 158             } 159             catch (Exception ex) 160             { 161                 MessageBox.Show(ex.Message); 162             } 163         } 164  165         private void Login() 166         { 167             hubConnection.InvokeAsync("Login", this.UserName); 168         } 169  170         private void ReceiveInfos(string msg) 171         { 172             if (string.IsNullOrEmpty(msg)) { 173                 return; 174             } 175             if (this.richTextBox != null) 176             { 177                 Run run = new Run(); 178                 Run run1 = new Run(); 179                 Paragraph paragraph = new Paragraph(); 180                 Paragraph paragraph1 = new Paragraph(); 181                 run.Foreground = Brushes.Red; 182                 run.Text = msg.Split("|")[0]; 183                 run1.Foreground = Brushes.Black; 184                 run1.Text = msg.Split("|")[1]; 185                 paragraph.Inlines.Add(run); 186                 paragraph1.Inlines.Add(run1); 187                 paragraph.LineHeight = 1; 188                 paragraph.TextAlignment = TextAlignment.Left; 189                 paragraph1.LineHeight = 1; 190                 paragraph1.TextAlignment = TextAlignment.Left; 191                 this.richTextBox.Document.Blocks.Add(paragraph); 192                 this.richTextBox.Document.Blocks.Add(paragraph1); 193                 this.richTextBox.ScrollToEnd(); 194             } 195         } 196  197         private void RefreshUsers(List<string> users) {  198             this.Users = users; 199         } 200     } 201 }

运行示例

在示例中,需要同时启动服务端和客户端,所以以多项目方式启动,如下所示:

WPF+ASP.NET SignalR实现简易在线聊天功能

 

 运行成功后,服务端以ASP.NET Web API的方式呈现,如下所示:

WPF+ASP.NET SignalR实现简易在线聊天功能

 

 客户端需要同时运行两个,所以在调试运行启动一个客户端后,还要在Debug目录下,手动双击客户端,再打开一个,并进行登录,如下所示:

WPF+ASP.NET SignalR实现简易在线聊天功能

 

 WPF+ASP.NET SignalR实现简易在线聊天功能

 

 系统运行时,后台日志输出如下所示:

WPF+ASP.NET SignalR实现简易在线聊天功能

 

备注

以上就是WPF+ASP.NET SignalR实现在线聊天的全部内容,关于SignalR的应用,不仅仅局限于在线聊天,这只是一个简单的入门示例,希望可以抛砖引玉,一起学习,共同进步。学习编程,从关注【老码识途】开始!!!