7

在Winform系统开发中,使用MediatR来实现类似事件总线的消息处理

 7 months ago
source link: https://www.cnblogs.com/wuhuacong/p/17984987
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

MediatR是一款进程内的消息订阅、发布框架,可实现请求/响应、命令、查询、通知和事件的消息传递,解耦了消息处理器和消息之间耦合。提供了Send方法用于发布到单个处理程序、Publish方法发布到多个处理程序,使用起来非常方便。目前支持 .NET Framework 、.NET Stardand、.NETCore等版本,可跨平台使用。本篇随笔介绍在Winform系统开发中,使用MediatR来实现类似事件总线的消息处理。

1、安装使用MediatR

MediatR的GitHub项目地址:https://github.com/jbogard/MediatR

MediatR的各种场景使用代码:https://github.com/jbogard/MediatR/wiki

如果我们在VS开发项目,我们在Nugget上找到对应模块,直接添加到项目引用即可,如下所示。

8867-20240124160840246-1836079020.png

MediatR使用 Microsoft.Extensions.DependencyInjection.Abstractions 来 注入服务处理,我们使用MediatR的时候,首先需要构造ServiceCollection,然后添加配置到其中。

// IServiceCollection负责注册
IServiceCollection services = new ServiceCollection();

//注册MediatR服务,用于测试MediatR的服务
services.AddMediatR(cfg => {
    cfg.RegisterServicesFromAssembly(typeof(Portal).Assembly);
});

使用注入服务的时候,我们需要获得其中的ServiceProvider,如下通过BuildServiceProvider 获得该对象。

 IServiceProvider provider = services.BuildServiceProvider();

然后我们创建一个静态类来存储这个对象。

//存储全局IServiceProvider的接口实例, 便于后续获得接口实例
ServiceLocator.ConfigService(provider);

其中静态类 ServiceLocator 的代码如下所示。

    /// <summary>
    /// 全局存储IServiceProvider
    /// </summary>
    public class ServiceLocator
    {
        /// <summary>
        /// IOC中的IServiceProvider对象接口
        /// </summary>
        public static IServiceProvider SerivcePovider { get; private set; }

        /// <summary>
        /// 赋值IServiceProvider到静态变量中
        /// </summary>
        /// <param name="provider">IServiceProvider对象接口</param>
        public static void ConfigService(IServiceProvider provider)
        {
            SerivcePovider = provider;
        }

        /// <summary>
        /// 获取指定服务接口实例
        /// </summary>
        /// <returns></returns>
        public static T GetService<T>()
        {
            return SerivcePovider.GetService<T>();
        }
    }

后面我们就可以通过该静态类的 GetService<T>() 方法获取对应的注入接口IMediator,我们需要利用该接口来发送Send请求/应答命令或者发布Publish消息的处理。例如我们在窗体对象中定义该接口,用于实际的相关命令、消息的处理。

    public partial class TestMediatR : BaseForm
    {
        private readonly IMediator _mediator;

        public TestMediatR()
        {
            InitializeComponent();

            _mediator = ServiceLocator.GetService<IMediator>();
        }

2、MediatR命令或者消息的处理

MediatR是一个跨平台通过一种进程内消息传递机制,进行请求/响应、命令、查询、通知和事件的消息传递,并通过C#泛型来支持消息的智能调度,其目的是消息发送和消息处理的解耦。它支持以单播和多播形式使用同步或异步的模式来发布消息,创建和侦听事件。它主要的几个对象:

  IMediator:主要提供Send与Publish方法,需要执行的命令都是通过这两个方法实现

  IRequest、IRequest<T>命令查询 | 处理类所继承的接口,一个有返回类型,一个无返回类型,一个查询对应一个处理类,程序集只认第一个扫描到的类。

  IRequestHandler<in TRequest,TResponse>(实现Handle方法) :命令处理接口。命令查询 | 处理类继承它,也可以继承AsyncRequestHandler(实现抽象Handle方法)、RequestHandler(实现抽象Handle方法)接口

  INotification命令查询 | 处理类所继承的接口这个没有返回,与IRequest不通的是可以对于多个处理类。

  INotificationHandler<in TNotification>:与IRequestHandler一样的只不过这是INotification的处理接口。

Request/Response模式对象定义

    /// <summary>
    /// 请求类
    /// </summary>
    public class RetrieveInfoCommandRequest : IRequest<RetrieveInfoCommandResponse>
    {
        public string Text { get; set; }
    }
    /// <summary>
    /// 回应消息
    /// </summary>
    public class RetrieveInfoCommandResponse
    {
        public string OutputMessage { get; set; }
    }

    /// <summary>
    /// 请求应答处理类
    /// </summary>
    public class RetrieveInfoCommandHandler : IRequestHandler<RetrieveInfoCommandRequest, RetrieveInfoCommandResponse>
    {
        public async Task<RetrieveInfoCommandResponse> Handle(RetrieveInfoCommandRequest request, CancellationToken cancellationToken)
        {
            var response = new RetrieveInfoCommandResponse();
            response.OutputMessage = $"This is an example of MediatR using {request.Text}";
            return response;
        }
    }

例如我们根据这个请求、应答的消息协议,以及定义的处理Handler类(唯一一个),我们可以设计一个Winform界面来测试消息的处理。

8867-20240124162539246-2062353920.png

 界面的代码如下所示。

/// <summary>
/// 测试MediatR的窗体例子
/// </summary>
public partial class TestMediatR : BaseForm
{
    private readonly IMediator _mediator;

    public TestMediatR()
    {
        InitializeComponent();

        _mediator = ServiceLocator.GetService<IMediator>();
    }

    /// <summary>
    /// 使用请求、应答的消息进行测试,获得返回结果后输出显示
    /// </summary>
    private async void btnSend_Click(object sender, EventArgs e)
    {
        //应答处理
        var outputMessage = await _mediator.Send(new RetrieveInfoCommandRequest
        {
            Text = this.txtSend.Text
        });
        Console.WriteLine(outputMessage.OutputMessage);
        this.txtReceived.AppendText(outputMessage.OutputMessage + Environment.NewLine);
    }

上面的命令消息方式,有返回值,如果不需要返回值,也可以采用这种一一应答的方式,那么定义的时候,继承IRequest接口即可。

    public class OneWay : IRequest { }
    public class OneWayHandler : IRequestHandler<OneWay>
    {
        public Task Handle(OneWay request, CancellationToken cancellationToken)
        {
            // do work
            return Task.CompletedTask;
        }
    }

Notification 消息通知模式

如果我们需要类似事件多播的处理,也就是常规的消息通知处理,我们采用INotification方式。

Notification模式将消息发布给多个处理程序,消息的处理没有返回值。

/// <summary>
/// 通知类
/// </summary>
public class MyNotification : INotification
{
    public string Message { get; }

    public MyNotification(string message)
    {
        Message = message;
    }
}

/// <summary>
/// Notification处理程序-模块1
/// </summary>
public class MyNotifyHandler : INotificationHandler<MyNotification>
{
    public Task Handle(MyNotification notification, CancellationToken cancellationToken)
    {
        var message = "模块1-收到消息:" + notification.Message;
        //MessageDxUtil.ShowTips(message);

        //提示消息
        var alert = new AlertControl();
        alert.FormLocation =  AlertFormLocation.TopRight;
        alert.AutoFormDelay = 3000;
        alert.Show(Portal.gc.MainDialog, message, message);


        // 处理通知
        Console.WriteLine($"Notification处理程序-模块1-收到消息: {notification.Message}");
        return Task.CompletedTask;
    }
}
/// <summary>
/// Notification处理程序-模块2
/// </summary>
public class MySecondNotifyHandler : INotificationHandler<MyNotification>
{
    public Task Handle(MyNotification notification, CancellationToken cancellationToken)
    {
        var message = "模块2-收到消息:" + notification.Message;
        //MessageDxUtil.ShowTips(message);

        //提示消息
        var alert = new AlertControl();
        alert.FormLocation = AlertFormLocation.TopRight;
        alert.AutoFormDelay = 3000;
        alert.Show(Portal.gc.MainDialog, message, message);

        // 处理通知
        Console.WriteLine($"Notification处理程序-模块2-收到消息: {notification.Message}");
        return Task.CompletedTask;
    }
}

我们在界面上发布消息的代码如下所示。

private async void btnNotify_Click(object sender, EventArgs e)
{
    //发布消息
    await _mediator.Publish(new MyNotification(this.txtSend.Text));
}

可以看到在控制台和UI上我们的都有测试消息的输出。

8867-20240124163531662-641317747.png
8867-20240124163637509-767855947.png

默认情况下,MediatR的消息发布是一个一个执行的,即便是返回Task的情况,也是使用await等待上一个执行完成后才进行下一个的调用。如果需要使用并行的方法进行调用,可以进行定制,具体可参考官方示例:MediatR.Examples.PublishStrategies

对于MediatR来说,无论是发送IRequest类型消息,还是发布INotification类型消息,都是异步的。这里需要特别留意,即使你使用的是同步的消息处理程序,对于消息发布来说,都是异步的,与你的处理程序是同步或异步无关。

详细的介绍,可以参考官方的案例介绍:https://github.com/jbogard/MediatR/wiki

3、回顾WPF的MVVM的消息处理

对于WPF,其实也是类似采用该组件实现事件、消息的处理的,不过如果我们采用MVVM的框架设计模式,可以采用MVVM(微软的 CommunityToolkit.Mvvm的组件包)的内置的消息处理模式,我在随笔《使用WPF开发自定义用户控件,以及实现相关自定义事件的处理》有相关的介绍。

CommunityToolkit.Mvvm  (又名 MVVM 工具包,以前名为 Microsoft.Toolkit.Mvvm) 是一个现代、快速且模块化的 MVVM 库。官网介绍地址:https://learn.microsoft.com/zh-cn/dotnet/communitytoolkit/mvvm/ 

利用MVVM推送一条消息,如下代码所示。

//发送MVVM消息信息通知方式(一)
WeakReferenceMessenger.Default.Send(new ClickEventMessage(eventData));

而其中 ClickEventMessage 是我们根据要求定义的一个消息对象类,如下代码所示。

8867-20240112105149941-57079291.png

完整的Command命令如下所示。

/// <summary>
/// 双击触发MVVM消息通知
/// </summary>
/// <param name="typeName">处理类型:Number、Animal、WuHan</param>
/// <returns></returns>
[RelayCommand]
private async Task DoubleClick(string typeName)
{
    var clickType = ClickEventType.Number;
    var clickValue = this.Number;

    ..............//处理不同typeName值逻辑//事件数据
    var eventData = new ClickEventData(clickType, clickValue);

    //发送MVVM消息信息通知方式(一)
    WeakReferenceMessenger.Default.Send(new ClickEventMessage(eventData));
}

通过这样的消息发送,就需要有个地方来接收这个信息的,我们在需要处理事件的父窗口中拦截处理消息即可。

//处理MVVM的消息通知
WeakReferenceMessenger.Default.Register<ClickEventMessage>(this, (r, m) =>
{
    var data = m.Value;
    var list = ControlHelper.FindVisualChildren<LotteryItemControl>(this.listControl);
    foreach (var lottery in list)
    {
        lottery.SetSelected(data);
    }
});

从而实现了WPF消息的发送和应答处理。

另外,我在随笔《使用 FastEndpoints 来垂直切割Web API的控制器方法》介绍的FastEndpoints 处理机制,也是类似这样的模式,有兴趣可以了解一下FastEndpoints 的处理。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK