在实际业务中,当后台数据发生变化,客户端能够实时的收到通知,而不是由用户主动的进行页面刷新才能查看,这将是一个非常人性化的设计。有没有那么一种场景,后台数据明明已经发生变化了,前台却因为没有及时刷新,而导致页面显示的数据与实际存在差异,从而造成错误的判断。那么如何才能在后台数据变更时及时通知客户端呢?本文以一个简单的聊天示例,简述如何通过WPF+ASP.NET SignalR实现消息后台通知,仅供学习分享使用,如有不足之处,还请指正。
在本示例中,涉及知识点如下所示:
ASP.NET SignalR 是一个面向 ASP.NET 开发人员的库,可简化将实时 web 功能添加到应用程序的过程。 实时 web 功能是让服务器代码将内容推送到连接的客户端立即可用,而不是让服务器等待客户端请求新数据的能力。

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

虽然聊天通常被用作示例,但你可以做更多的事情。每当用户刷新网页以查看新数据时,或者该网页实施 Ajax 长轮询以检索新数据时,它都是使用 SignalR 的候选者。SignalR 还支持需要从服务器进行高频更新的全新类型的应用,例如实时游戏。
在线聊天示例,主要分为服务端(ASP.NET Web API)和客户端(WPF可执行程序)。具体如下所示:

服务端主要实现消息的接收,转发等功能,具体步骤如下所示:
首先创建ASP.NET Web API项目,默认情况下SignalR已经作为项目框架的一部分而存在,所以不需要安装,直接使用即可。通过项目--依赖性--框架--Microsoft.AspNetCore.App可以查看,如下所示

在项目中新建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 }
聊天类创建成功后,需要配置服务注入和路由,在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();
你不会实例化 Hub 类或从服务器上自己的代码调用其方法;由 SignalR Hubs 管道为你完成的所有操作。 SignalR 每次需要处理中心操作(例如客户端连接、断开连接或向服务器发出方法调用时)时,SignalR 都会创建 Hub 类的新实例。
由于 Hub 类的实例是暂时性的,因此无法使用它们来维护从一个方法调用到下一个方法的状态。 每当服务器从客户端收到方法调用时,中心类的新实例都会处理消息。 若要通过多个连接和方法调用来维护状态,请使用一些其他方法(例如数据库)或 Hub 类上的静态变量,或者不派生自 Hub的其他类。 如果在内存中保留数据,请使用 Hub 类上的静态变量等方法,则应用域回收时数据将丢失。
如果要从在 Hub 类外部运行的代码将消息发送到客户端,则无法通过实例化 Hub 类实例来执行此操作,但可以通过获取对 Hub 类的 SignalR 上下文对象的引用来执行此操作。
注意:ChatHub每次调用都是一个新的实例,所以不可以有私有属性或变量,不可以保存对像的值,所以如果需要记录一些持久保存的值,则可以采用静态变量,或者中心以外的对象。
客户端如果要调用SignalR的值,需要通过NuGet包管理器,安装SignalR客户端,如下所示:

在客户端实现消息的接收和发送,主要通过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 }
在示例中,需要同时启动服务端和客户端,所以以多项目方式启动,如下所示:

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

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


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

以上就是WPF+ASP.NET SignalR实现在线聊天的全部内容,关于SignalR的应用,不仅仅局限于在线聊天,这只是一个简单的入门示例,希望可以抛砖引玉,一起学习,共同进步。学习编程,从关注【老码识途】开始!!!
我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden
只是想确保我理解了事情。据我目前收集到的信息,Cucumber只是一个“包装器”,或者是一种通过将事物分类为功能和步骤来组织测试的好方法,其中实际的单元测试处于步骤阶段。它允许您根据事物的工作方式组织您的测试。对吗? 最佳答案 有点。它是一种组织测试的方式,但不仅如此。它的行为就像最初的Rails集成测试一样,但更易于使用。这里最大的好处是您的session在整个Scenario中保持透明。关于Cucumber的另一件事是您(应该)从使用您的代码的浏览器或客户端的角度进行测试。如果您愿意,您可以使用步骤来构建对象和设置状态,但通常您
华为OD机试题本篇题目:明明的随机数题目输入描述输出描述:示例1输入输出说明代码编写思路最近更新的博客华为od2023|什么是华为od,od薪资待遇,od机试题清单华为OD机试真题大全,用Python解华为机试题|机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南华为o
C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.
MIMO技术的优缺点优点通过下面三个增益来总体概括:阵列增益。阵列增益是指由于接收机通过对接收信号的相干合并而活得的平均SNR的提高。在发射机不知道信道信息的情况下,MIMO系统可以获得的阵列增益与接收天线数成正比复用增益。在采用空间复用方案的MIMO系统中,可以获得复用增益,即信道容量成倍增加。信道容量的增加与min(Nt,Nr)成正比分集增益。在采用空间分集方案的MIMO系统中,可以获得分集增益,即可靠性性能的改善。分集增益用独立衰落支路数来描述,即分集指数。在使用了空时编码的MIMO系统中,由于接收天线或发射天线之间的间距较远,可认为它们各自的大尺度衰落是相互独立的,因此分布式MIMO
遍历文件夹我们通常是使用递归进行操作,这种方式比较简单,也比较容易理解。本文为大家介绍另一种不使用递归的方式,由于没有使用递归,只用到了循环和集合,所以效率更高一些!一、使用递归遍历文件夹整体思路1、使用File封装初始目录,2、打印这个目录3、获取这个目录下所有的子文件和子目录的数组。4、遍历这个数组,取出每个File对象4-1、如果File是否是一个文件,打印4-2、否则就是一个目录,递归调用代码实现publicclassSearchFile{publicstaticvoidmain(String[]args){//初始目录Filedir=newFile("d:/Dev");Datebeg
通常,数组被实现为内存块,集合被实现为HashMap,有序集合被实现为跳跃列表。在Ruby中也是如此吗?我正在尝试从性能和内存占用方面评估Ruby中不同容器的使用情况 最佳答案 数组是Ruby核心库的一部分。每个Ruby实现都有自己的数组实现。Ruby语言规范只规定了Ruby数组的行为,并没有规定任何特定的实现策略。它甚至没有指定任何会强制或至少建议特定实现策略的性能约束。然而,大多数Rubyist对数组的性能特征有一些期望,这会迫使不符合它们的实现变得默默无闻,因为实际上没有人会使用它:插入、前置或追加以及删除元素的最坏情况步骤复
require'mechanize'agent=Mechanize.newlogin=agent.get('http://www.schoolnet.ch/DE/HomeDE.htm')agent.clicklogin.link_withtext:/Login/然后我得到Mechanize::UnsupportedSchemeError。 最佳答案 Mechanize不支持javascript但您可以将搜索字段添加到表单并为其分配搜索词并使用mechanize提交表单form=page.forms.firstform.add_fie
在ruby中,你可以这样做:classThingpublicdeff1puts"f1"endprivatedeff2puts"f2"endpublicdeff3puts"f3"endprivatedeff4puts"f4"endend现在f1和f3是公共(public)的,f2和f4是私有(private)的。内部发生了什么,允许您调用一个类方法,然后更改方法定义?我怎样才能实现相同的功能(表面上是创建我自己的java之类的注释)例如...classThingfundeff1puts"hey"endnotfundeff2puts"hey"endendfun和notfun将更改以下函数定
在Rails自动生成的功能测试(test/functional/products_controller_test.rb)中,我看到以下代码:classProductsControllerTest我的问题是:方法调用products()在哪里/如何定义?products(:one)到底是什么意思?看代码,大概意思是“创建一个产品”,但是它是如何工作的呢?注意我是Ruby/Rails的新手,如果这些是微不足道的问题,我深表歉意。 最佳答案 如果您查看test/fixtures文件夹,您会看到一个products.yml文件。这是在您创建