草庐IT

WPF+ASP.NET SignalR实现动态折线图

老码识途 2023-03-28 原文

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

什么是SignalR?

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

SignalR做了什么?

传统HTTP采用的是大家熟知的“拉模式”,即客户端发出的每次请求,服务端都是被动处理。此场景下客户端是老大,很显然只有一方主动,操作与处理起来就没那么完美。

为了能让服务端也能主动,html5的出现让这种变得可能,大家知道html5中有两种主动模式。第一种叫做websockectWebSocketsHtml5提供的新的API,可以在Web网页与服务器端间建立Socket连接,它是基于tcp模式的双工通讯。还有一种叫做SSE,也就是客户端来订阅服务器的一种事件模型。

html5出来之前,如果要做到服务器主动,我们只能采用变相的longpooliframe流勉强实现。

SignalR对上面四种方案进行了高度的封装,也就是说signalR会在这四种技术中根据浏览器和服务器设置采取最优的一种模式。

封装与集成

对于.NET开发者的福音,.NET平台为我们提供了一种简洁高效智能的实时信息交互技术->SignalR,它集成了上述数种技术,并能根据配置自动或手动选择其最佳应用。

SignalR用途

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

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

 

官方网址和源码

 

官方网址:https://dotnet.microsoft.com/zh-cn/apps/aspnet/signalr

微软API文档:https://learn.microsoft.com/zh-cn/aspnet/signalr/overview/getting-started/introduction-to-signalr

GitHub网址:https://github.com/SignalR

示例截图

本示例主要实现一个动态折线图功能,主要分为服务端和客户端两部分,示例如下所示:

服务端项目创建

 

1. 创建一个Web服务端程序(以ASP.NET WebApi为例),默认情况下SignalR已经作为项目框架的一部分而存在,所以不需要安装,直接使用即可。通过项目--依赖性--框架--Microsoft.AspNetCore.App可以查看

2. 创建ChatHub,继承Hub基类,作为后台连接管理的中心

 1 using Microsoft.AspNetCore.SignalR;
 2 
 3 namespace DemoSignalR.Server.Chat
 4 {
 5     public class ChatHub : Hub
 6     {
 7         #region 连接和断开连接
 8 
 9         public override async Task OnConnectedAsync()
10         {
11             var connId = Context.ConnectionId;
12             Console.WriteLine($"{connId} 已连接");
13             await base.OnConnectedAsync();
14         }
15 
16         public void StartNotify(string type)
17         {
18             if (type == "1")
19             {
20 
21             }
22             else if (type == "2")
23             {
24 
25             };
26 
27         }
28 
29         public override async Task OnDisconnectedAsync(Exception ex)
30         {
31             //如果断开连接
32             var connId = Context.ConnectionId;
33             Console.WriteLine($"{connId} 已断开");
34             await base.OnDisconnectedAsync(ex);
35         }
36 
37         #endregion
38     }
39 }

SignalR服务端业务集成

在实际业务中,存在各种需要后台通知的功能,根据不同的业务,可以采用不同的通知触发方式:

1. 在调用接口时触发后台通知

 1 using DemoSignalR.Server.Chat;
 2 using Microsoft.AspNetCore.Mvc;
 3 using Microsoft.AspNetCore.SignalR;
 4 
 5 namespace DemoSignalR.Server.Controllers
 6 {
 7     [ApiController]
 8     [Route("[controller]")]
 9     public class TestWebApiController : ControllerBase
10     {
11 
12 
13         private readonly ILogger<TestWebApiController> _logger;
14 
15         private IHubContext<ChatHub> _context;
16 
17         public TestWebApiController(ILogger<TestWebApiController> logger, IHubContext<ChatHub> context)
18         {
19             _logger = logger;
20             _context = context;
21         }
22 
23         [HttpGet]
24         public void GetTestA(string Name)
25         {
26             string info = $"当前接收到的信息为:{Name},{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}";
27             _context.Clients.All.SendAsync("", info);
28         }
29 
30 
31     }
32 }

2. 定时器循环通知

 1 using Microsoft.AspNetCore.SignalR;
 2 using System.Timers;
 3 
 4 namespace DemoSignalR.Server.Chat
 5 {
 6     public class TestChatInfo
 7     {
 8         private IHubContext<ChatHub> _context;
 9 
10         private System.Timers.Timer _timer;
11 
12         private readonly static object locker = new object();//锁对象
13 
14         public static TestChatInfo instance;//当前实例
15 
16         private readonly ILogger _logger;
17 
18         private int _index = 0;
19 
20         private TestChatInfo(IHubContext<ChatHub> _context, ILogger _logger)
21         {
22             this._context = _context;
23             this._logger = _logger;
24             //定义定时器
25             _timer = new System.Timers.Timer(100);
26             _timer.AutoReset = true;
27             _timer.Enabled = true;
28             _timer.Elapsed += Timer_Elapsed;
29             _timer.Start();
30         }
31 
32         private void Timer_Elapsed(object? sender, ElapsedEventArgs e)
33         {
34             //业务逻辑判断
35             var obj = new TestValue();
36             obj.Index = this._index;
37             obj.Value = DateTime.Now.Millisecond % 100;
38             _context.Clients.All.SendAsync("RefreshInfos", obj);
39             this._index++;
40         }
41 
42         /// <summary>
43         /// 注册,即初始化单例实例
44         /// </summary>
45         /// <param name="context"></param>
46         /// <param name="reviewTaskService"></param>
47         /// <param name="sysParamService"></param>
48         public static void Register(IHubContext<ChatHub> context, ILogger logger)
49         {
50             lock (locker)
51             {
52                 if (instance == null)
53                 {
54                     lock (locker)
55                     {
56                         instance = new TestChatInfo(context, logger);
57                     }
58                 }
59             }
60         }
61 
62     }
63 
64     public class TestValue
65     {
66         private int index;
67         public int Index { get { return index; } set { index = value; } }
68 
69         private float value;
70         public float Value { get { return value; } set { this.value = value; } }
71     }
72 }

 

SignalR服务端配置

 SignalR服务端配置主要分成三步:

1. 添加SignalR服务

2. 映射SignalR路由

3. 注册单例后台通知服务(如果其他方式,可省略)

 1 using DemoSignalR.Server.Chat;
 2 using Microsoft.AspNetCore.SignalR;
 3 
 4 var builder = WebApplication.CreateBuilder(args);
 5 
 6 // Add services to the container.
 7 
 8 builder.Services.AddControllers();
 9 // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
10 builder.Services.AddEndpointsApiExplorer();
11 builder.Services.AddSwaggerGen();
12 //1.添加SignalR服务
13 builder.Services.AddSignalR();
14 builder.Services.AddLogging(logging => logging.AddConsole());
15 
16 var app = builder.Build();
17 
18 // Configure the HTTP request pipeline.
19 if (app.Environment.IsDevelopment())
20 {
21     app.UseSwagger();
22     app.UseSwaggerUI();
23 }
24 app.UseRouting();
25 app.UseHttpsRedirection();
26 
27 app.UseAuthorization();
28 
29 
30 //在Use中注册单例实例
31 app.Use(async (context, next) =>
32 {
33     var hubContext = context.RequestServices.GetRequiredService<IHubContext<ChatHub>>();
34     //var logger = context.RequestServices.GetRequiredService<ILogger>();
35     TestChatInfo.Register(hubContext, null);//调用静态方法注册
36     if (next != null)
37     {
38         await next.Invoke();
39     }
40 });
41 
42 app.MapControllers();
43 
44 //2.映射路由
45 app.UseEndpoints(endpoints => {
46     endpoints.MapHub<ChatHub>("/chat");
47 });
48 
49 app.Run();

客户端项目创建

1. 创建WPF项目

2. 通过NuGet包管理器安装SignalR客户端

3. 创建SignalR状态管理,主要管理SignalR的连接,关闭,重连等操作。

 1 using Microsoft.AspNetCore.SignalR.Client;
 2 using System;
 3 using System.Collections.Generic;
 4 using System.Configuration;
 5 using System.Linq;
 6 using System.Text;
 7 using System.Threading.Tasks;
 8 
 9 namespace DemoSignalR.Client
10 {
11     internal class SignalRClient
12     {
13         private readonly HubConnection hubConnection;
14 
15         public HubConnection HubConnection
16         {
17             get { return hubConnection; }
18         }
19 
20         public SignalRClient()
21         {
22             var server = ConfigurationManager.AppSettings["Server"].ToString();
23             hubConnection = new HubConnectionBuilder().WithUrl($"{server}/chat").WithAutomaticReconnect().Build();
24             hubConnection.KeepAliveInterval = TimeSpan.FromSeconds(5);
25         }
26 
27         public virtual void Listen()
28         {
29 
30         }
31 
32         public async void Start()
33         {
34             try
35             {
36                 await hubConnection.StartAsync();
37 
38             }
39             catch (Exception e)
40             {
41 
42             }
43         }
44     }
45 }

客户端业务逻辑处理

1. 根据不同的业务逻辑分别监听不同的通知事件。

2. 示例详见源码

 1 using Microsoft.AspNetCore.SignalR.Client;
 2 using System;
 3 using System.Collections.Generic;
 4 using System.Linq;
 5 using System.Text;
 6 using System.Threading.Tasks;
 7 
 8 namespace DemoSignalR.Client
 9 {
10     internal class TestSignalRClient : SignalRClient
11     {
12         public Action<object> RefreshInfos;
13 
14         public Action<string> Reconnected;
15 
16         public TestSignalRClient() : base()
17         {
18         }
19 
20         public override void Listen()
21         {
22             HubConnection.On<object>("RefreshInfos", (obj) =>
23             {
24                 //
25                 if (obj != null)
26                 {
27                     Console.WriteLine("收到数据");
28                     //发送消息
29                     if (RefreshInfos != null)
30                     {
31                         RefreshInfos(obj);
32                     }
33                 }
34             });
35             HubConnection.Reconnected += HubConnection_Reconnected;
36         }
37 
38         private Task HubConnection_Reconnected(string arg)
39         {
40             return Task.Run(() =>
41             {
42                 if (Reconnected != null)
43                 {
44                     Reconnected(arg);
45                 }
46             });
47         }
48 
49         public virtual void StartNotify(string condition)
50         {
51             HubConnection.SendAsync("StartNotify", condition);
52             Console.WriteLine($"开始通过知{condition}");
53         }
54     }
55 }

 

SignalR需要注意事项

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

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

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

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

关于源码

本示例中相关源码,已上传至gitee(码云),链接如下:
https://gitee.com/ahsiang/demo-signal-r

 

 

备注

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

有关WPF+ASP.NET SignalR实现动态折线图的更多相关文章

  1. ruby - 如何根据特征实现 FactoryGirl 的条件行为 - 2

    我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden

  2. 华为OD机试用Python实现 -【明明的随机数】 2023Q1A - 2

    华为OD机试题本篇题目:明明的随机数题目输入描述输出描述:示例1输入输出说明代码编写思路最近更新的博客华为od2023|什么是华为od,od薪资待遇,od机试题清单华为OD机试真题大全,用Python解华为机试题|机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南华为o

  3. 基于C#实现简易绘图工具【100010177】 - 2

    C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.

  4. MIMO-OFDM无线通信技术及MATLAB实现(1)无线信道:传播和衰落 - 2

     MIMO技术的优缺点优点通过下面三个增益来总体概括:阵列增益。阵列增益是指由于接收机通过对接收信号的相干合并而活得的平均SNR的提高。在发射机不知道信道信息的情况下,MIMO系统可以获得的阵列增益与接收天线数成正比复用增益。在采用空间复用方案的MIMO系统中,可以获得复用增益,即信道容量成倍增加。信道容量的增加与min(Nt,Nr)成正比分集增益。在采用空间分集方案的MIMO系统中,可以获得分集增益,即可靠性性能的改善。分集增益用独立衰落支路数来描述,即分集指数。在使用了空时编码的MIMO系统中,由于接收天线或发射天线之间的间距较远,可认为它们各自的大尺度衰落是相互独立的,因此分布式MIMO

  5. 【Java入门】使用Java实现文件夹的遍历 - 2

    遍历文件夹我们通常是使用递归进行操作,这种方式比较简单,也比较容易理解。本文为大家介绍另一种不使用递归的方式,由于没有使用递归,只用到了循环和集合,所以效率更高一些!一、使用递归遍历文件夹整体思路1、使用File封装初始目录,2、打印这个目录3、获取这个目录下所有的子文件和子目录的数组。4、遍历这个数组,取出每个File对象4-1、如果File是否是一个文件,打印4-2、否则就是一个目录,递归调用代码实现publicclassSearchFile{publicstaticvoidmain(String[]args){//初始目录Filedir=newFile("d:/Dev");Datebeg

  6. ruby - Arrays Sets 和 SortedSets 在 Ruby 中是如何实现的 - 2

    通常,数组被实现为内存块,集合被实现为HashMap,有序集合被实现为跳跃列表。在Ruby中也是如此吗?我正在尝试从性能和内存占用方面评估Ruby中不同容器的使用情况 最佳答案 数组是Ruby核心库的一部分。每个Ruby实现都有自己的数组实现。Ruby语言规范只规定了Ruby数组的行为,并没有规定任何特定的实现策略。它甚至没有指定任何会强制或至少建议特定实现策略的性能约束。然而,大多数Rubyist对数组的性能特征有一些期望,这会迫使不符合它们的实现变得默默无闻,因为实际上没有人会使用它:插入、前置或追加以及删除元素的最坏情况步骤复

  7. ruby - 在 Ruby 中动态创建数组 - 2

    有没有办法在Ruby中动态创建数组?例如,假设我想遍历用户输入的书籍数组:books=gets.chomp用户输入:"TheGreatGatsby,CrimeandPunishment,Dracula,Fahrenheit451,PrideandPrejudice,SenseandSensibility,Slaughterhouse-Five,TheAdventuresofHuckleberryFinn"我把它变成一个数组:books_array=books.split(",")现在,对于用户输入的每一本书,我想用Ruby创建一个数组。伪代码来做到这一点:x=0books_array.

  8. ruby - 在 ASP 页面上 Mechanize 中断 - 2

    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

  9. ruby - 是否可以将 IRB 提示配置为动态更改? - 2

    我想在IRB中浏览文件系统并让提示更改以反射(reflect)当前工作目录,但我不知道如何在每个命令后进行提示更新。最终,我想在日常工作中更多地使用IRB,让bash溜走。我在我的.irbrc中试过这个:require'fileutils'includeFileUtilsIRB.conf[:PROMPT][:CUSTOM]={:PROMPT_N=>"\e[1m:\e[m",:PROMPT_I=>"\e[1m#{pwd}>\e[m",:PROMPT_S=>"FOO",:PROMPT_C=>"\e[1m#{pwd}>\e[m",:RETURN=>""}IRB.conf[:PROMPT_MO

  10. ruby - "public/protected/private"方法是如何实现的,我该如何模拟它? - 2

    在ruby中,你可以这样做:classThingpublicdeff1puts"f1"endprivatedeff2puts"f2"endpublicdeff3puts"f3"endprivatedeff4puts"f4"endend现在f1和f3是公共(public)的,f2和f4是私有(private)的。内部发生了什么,允许您调用一个类方法,然后更改方法定义?我怎样才能实现相同的功能(表面上是创建我自己的java之类的注释)例如...classThingfundeff1puts"hey"endnotfundeff2puts"hey"endendfun和notfun将更改以下函数定

随机推荐