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

简介: 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 功能是让服务器代码将内容推送到连接的客户端立即可用,而不是让服务器等待客户端请求新数据的能力。

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

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

在线聊天整体架构


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

ASP.NET SignalR在线聊天服务端


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

1. 创建ASP.NET Web API项目

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

2. 创建消息通知中心Hub

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

using Microsoft.AspNetCore.SignalR;
namespace SignalRChat.Chat
{
    public class ChatHub:Hub
    {
        private static Dictionary<string,string> dictUsers = new Dictionary<string,string>();
        public override Task OnConnectedAsync()
        {
            Console.WriteLine($"ID:{Context.ConnectionId} 已连接");
            return base.OnConnectedAsync();
        }
        public override Task OnDisconnectedAsync(Exception? exception)
        {
            Console.WriteLine($"ID:{Context.ConnectionId} 已断开");
            return base.OnDisconnectedAsync(exception);
        }
        /// <summary>
        /// 向客户端发送信息
        /// </summary>
        /// <param name="msg"></param>
        /// <returns></returns>
        public Task Send(string msg) {
            return Clients.Caller.SendAsync("SendMessage",msg);
        }
        /// <summary>
        /// 登录功能,将用户ID和ConntectionId关联起来
        /// </summary>
        /// <param name="userId"></param>
        public void Login(string userId) {
            if (!dictUsers.ContainsKey(userId)) {
                dictUsers[userId] = Context.ConnectionId;
            }
            Console.WriteLine($"{userId}登录成功,ConnectionId={Context.ConnectionId}");
            //向所有用户发送当前在线的用户列表
            Clients.All.SendAsync("Users", dictUsers.Keys.ToList());
        }
        /// <summary>
        /// 一对一聊天
        /// </summary>
        /// <param name="userId"></param>
        /// <param name="targetUserId"></param>
        /// <param name="msg"></param>
        public void Chat(string userId, string targetUserId, string msg)
        {
            string newMsg = $"{userId}|{msg}";//组装后的消息体
            //如果当前用户在线
            if (dictUsers.ContainsKey(targetUserId))
            {
                Clients.Client(dictUsers[targetUserId]).SendAsync("ChatInfo",newMsg);
            }
            else {
                //如果当前用户不在线,正常是保存数据库,等上线时加载,暂时不做处理
            }
        }
        /// <summary>
        /// 退出功能,当客户端退出时调用
        /// </summary>
        /// <param name="userId"></param>
        public void Logout(string userId)
        {
            if (dictUsers.ContainsKey(userId))
            {
                dictUsers.Remove(userId);
            }
            Console.WriteLine($"{userId}退出成功,ConnectionId={Context.ConnectionId}");
        }
    }
}

3. 注册服务和路由

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

using SignalRChat.Chat;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
//1.添加SignalR服务
builder.Services.AddSignalR();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}
app.UseRouting();
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
//2.映射路由
app.UseEndpoints(endpoints => {
    endpoints.MapHub<ChatHub>("/chat");
});
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客户端,如下所示:

2. 客户端消息接收发送

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

namespace SignalRClient
{
    public class ChatViewModel:ObservableObject
    {
        #region 属性及构造函数
        private string targetUserName;
        public string TargetUserName
        {
            get { return targetUserName; }
            set { SetProperty(ref targetUserName , value); }
        }
        private string userName;
        public string UserName
        {
            get { return userName; }
            set
            {
                SetProperty(ref userName, value);
                Welcome = $"欢迎 {value} 来到聊天室";
            }
        }
        private string welcome;
        public string Welcome
        {
            get { return welcome; }
            set { SetProperty(ref welcome , value); }
        }
        private List<string> users;
        public List<string> Users
        {
            get { return users; }
            set {SetProperty(ref users , value); }
        }
        private RichTextBox richTextBox;
        private HubConnection hubConnection;
        public ChatViewModel() {
        }
        #endregion
        #region 命令
        private ICommand loadedCommand;
        public ICommand LoadedCommand
        {
            get
            {
                if (loadedCommand == null)
                {
                    loadedCommand = new RelayCommand<object>(Loaded);
                }
                return loadedCommand;
            }
        }
        private void Loaded(object obj)
        {
            //1.初始化
            InitInfo();
            //2.监听
            Listen();
            //3.连接
            Link();
            //4.登录
            Login();
            //
            if (obj != null) {
                var eventArgs = obj as RoutedEventArgs;
                var window= eventArgs.OriginalSource as ChatWindow;
                this.richTextBox = window.richTextBox;
            }
        }
        private IRelayCommand<string> sendCommand;
        public IRelayCommand<string> SendCommand
        {
            get {
                if (sendCommand == null) {
                    sendCommand = new RelayCommand<string>(Send);
                }
                return sendCommand; }
        }
        private void Send(string msg)
        {
            if (string.IsNullOrEmpty(msg)) {
                MessageBox.Show("发送的消息为空");
                return;
            }
            if (string.IsNullOrEmpty(this.TargetUserName)) {
                MessageBox.Show("发送的目标用户为空");
                return ;
            }
            hubConnection.InvokeAsync("Chat",this.UserName,this.TargetUserName,msg);
            if (this.richTextBox != null)
            {
                Run run = new Run();
                Run run1 = new Run();
                Paragraph paragraph = new Paragraph();
                Paragraph paragraph1 = new Paragraph();
                run.Foreground = Brushes.Blue;
                run.Text = this.UserName;
                run1.Foreground= Brushes.Black;
                run1.Text = msg;
                paragraph.Inlines.Add(run);
                paragraph1.Inlines.Add(run1);
                paragraph.LineHeight = 1;
                paragraph.TextAlignment = TextAlignment.Right;
                paragraph1.LineHeight = 1;
                paragraph1.TextAlignment = TextAlignment.Right;
                this.richTextBox.Document.Blocks.Add(paragraph);
                this.richTextBox.Document.Blocks.Add(paragraph1);
                this.richTextBox.ScrollToEnd();
            }
        }
        #endregion
        /// <summary>
        /// 初始化Connection对象
        /// </summary>
        private void InitInfo() {
            hubConnection = new HubConnectionBuilder().WithUrl("https://localhost:7149/chat").WithAutomaticReconnect().Build();
            hubConnection.KeepAliveInterval =TimeSpan.FromSeconds(5);
        }
        /// <summary>
        /// 监听
        /// </summary>
        private void Listen() {
            hubConnection.On<List<string>>("Users", RefreshUsers);
            hubConnection.On<string>("ChatInfo",ReceiveInfos);
        }
        /// <summary>
        /// 连接
        /// </summary>
        private async void Link() {
            try
            {
               await hubConnection.StartAsync();
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message);
            }
        }
        private void Login()
        {
            hubConnection.InvokeAsync("Login", this.UserName);
        }
        private void ReceiveInfos(string msg)
        {
            if (string.IsNullOrEmpty(msg)) {
                return;
            }
            if (this.richTextBox != null)
            {
                Run run = new Run();
                Run run1 = new Run();
                Paragraph paragraph = new Paragraph();
                Paragraph paragraph1 = new Paragraph();
                run.Foreground = Brushes.Red;
                run.Text = msg.Split("|")[0];
                run1.Foreground = Brushes.Black;
                run1.Text = msg.Split("|")[1];
                paragraph.Inlines.Add(run);
                paragraph1.Inlines.Add(run1);
                paragraph.LineHeight = 1;
                paragraph.TextAlignment = TextAlignment.Left;
                paragraph1.LineHeight = 1;
                paragraph1.TextAlignment = TextAlignment.Left;
                this.richTextBox.Document.Blocks.Add(paragraph);
                this.richTextBox.Document.Blocks.Add(paragraph1);
                this.richTextBox.ScrollToEnd();
            }
        }
        private void RefreshUsers(List<string> users) {
            this.Users = users;
        }
    }
}

运行示例


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

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

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

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

备注


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

相关文章
|
2月前
|
人工智能 开发框架 .NET
.NET技术的强大功能:.NET技术的基础特性、在现代开发中的应用、以及它如何助力未来的软件开发。
.NET技术是软件开发领域的核心支柱,以其强大功能、灵活性及安全性广受认可。本文分三部分解析:基础特性如多语言支持、统一运行时环境;现代应用如企业级与Web开发、移动应用、云服务及游戏开发;以及未来趋势如性能优化、容器化、AI集成等,展望.NET在不断变化的技术环境中持续发展与创新。
80 4
|
25天前
|
存储 开发框架 前端开发
ASP.NET MVC 迅速集成 SignalR
ASP.NET MVC 迅速集成 SignalR
38 0
|
2月前
|
开发框架 JavaScript 前端开发
提升生产力:8个.NET开源且功能强大的快速开发框架
提升生产力:8个.NET开源且功能强大的快速开发框架
|
2月前
|
Unix Linux C#
增强用户体验:2个功能强大的.NET控制台应用帮助库
增强用户体验:2个功能强大的.NET控制台应用帮助库
|
2月前
|
C# 机器学习/深度学习 搜索推荐
WPF与机器学习的完美邂逅:手把手教你打造一个具有智能推荐功能的现代桌面应用——从理论到实践的全方位指南,让你的应用瞬间变得高大上且智能无比
【8月更文挑战第31天】本文详细介绍如何在Windows Presentation Foundation(WPF)应用中集成机器学习功能,以开发具备智能化特性的桌面应用。通过使用Microsoft的ML.NET框架,本文演示了从安装NuGet包、准备数据集、训练推荐系统模型到最终将模型集成到WPF应用中的全过程。具体示例代码展示了如何基于用户行为数据训练模型,并实现实时推荐功能。这为WPF开发者提供了宝贵的实践指导。
29 0
|
2月前
|
开发者 C# UED
WPF与多媒体:解锁音频视频播放新姿势——从界面设计到代码实践,全方位教你如何在WPF应用中集成流畅的多媒体功能
【8月更文挑战第31天】本文以随笔形式介绍了如何在WPF应用中集成音频和视频播放功能。通过使用MediaElement控件,开发者能轻松创建多媒体应用程序。文章详细展示了从创建WPF项目到设计UI及实现媒体控制逻辑的过程,并提供了完整的示例代码。此外,还介绍了如何添加进度条等额外功能以增强用户体验。希望本文能为WPF开发者提供实用的技术指导与灵感。
72 0
|
2月前
|
存储 开发者 C#
WPF与邮件发送:教你如何在Windows Presentation Foundation应用中无缝集成电子邮件功能——从界面设计到代码实现,全面解析邮件发送的每一个细节密武器!
【8月更文挑战第31天】本文探讨了如何在Windows Presentation Foundation(WPF)应用中集成电子邮件发送功能,详细介绍了从创建WPF项目到设计用户界面的全过程,并通过具体示例代码展示了如何使用`System.Net.Mail`命名空间中的`SmtpClient`和`MailMessage`类来实现邮件发送逻辑。文章还强调了安全性和错误处理的重要性,提供了实用的异常捕获代码片段,旨在帮助WPF开发者更好地掌握邮件发送技术,提升应用程序的功能性与用户体验。
34 0
|
2月前
|
API C# Shell
WPF与Windows Shell完美融合:深入解析文件系统操作技巧——从基本文件管理到高级Shell功能调用,全面掌握WPF中的文件处理艺术
【8月更文挑战第31天】Windows Presentation Foundation (WPF) 是 .NET Framework 的关键组件,用于构建 Windows 桌面应用程序。WPF 提供了丰富的功能来创建美观且功能强大的用户界面。本文通过问题解答的形式,探讨了如何在 WPF 应用中集成 Windows Shell 功能,并通过具体示例代码展示了文件系统的操作方法,包括列出目录下的所有文件、创建和删除文件、移动和复制文件以及打开文件夹或文件等。
44 0
|
2月前
|
C# Windows 监控
WPF应用跨界成长秘籍:深度揭秘如何与Windows服务完美交互,扩展功能无界限!
【8月更文挑战第31天】WPF(Windows Presentation Foundation)是 .NET 框架下的图形界面技术,具有丰富的界面设计和灵活的客户端功能。在某些场景下,WPF 应用需与 Windows 服务交互以实现后台任务处理、系统监控等功能。本文探讨了两者交互的方法,并通过示例代码展示了如何扩展 WPF 应用的功能。首先介绍了 Windows 服务的基础知识,然后阐述了创建 Windows 服务、设计通信接口及 WPF 客户端调用服务的具体步骤。通过合理的交互设计,WPF 应用可获得更强的后台处理能力和系统级操作权限,提升应用的整体性能。
64 0
|
2月前
|
数据库 C# 开发者
WPF开发者必读:揭秘ADO.NET与Entity Framework数据库交互秘籍,轻松实现企业级应用!
【8月更文挑战第31天】在现代软件开发中,WPF 与数据库的交互对于构建企业级应用至关重要。本文介绍了如何利用 ADO.NET 和 Entity Framework 在 WPF 应用中访问和操作数据库。ADO.NET 是 .NET Framework 中用于访问各类数据库(如 SQL Server、MySQL 等)的类库;Entity Framework 则是一种 ORM 框架,支持面向对象的数据操作。文章通过示例展示了如何在 WPF 应用中集成这两种技术,提高开发效率。
40 0