當前位置: 妍妍網 > 碼農

在Avalonia計畫中使用MediatR和MS.DI庫實作事件驅動通訊

2024-03-02碼農

大家好,我是沙漠盡頭的狼!

AvaloniaUI是一個強大的跨平台.NET客戶端開發框架,讓開發者能夠針對Windows、Linux、macOS、Android和iOS等多個平台構建應用程式。在構建復雜的應用程式時,模組化和元件間的通訊變得尤為重要。Prism框架提供了模組化的開發方式,支持外掛程式的熱拔插,而MediatR則是一個實作了中介者(Mediator)模式的事件訂閱釋出框架,非常適合用於模組之間以及模組與主程式之間的通訊。

本文重點是介紹 MediatR ,它 是 .NET 中的開源簡單中介者模式實作。它透過一種行程內訊息傳遞機制(無其他外部依賴),進行請求/響應、命令、查詢、通知和事件的訊息傳遞,並透過泛型來支持訊息的智慧排程。開源庫地址是 https://github.com/jbogard/MediatR。

本文將詳細介紹如何在Avalonia計畫中使用MediatR和Microsoft的依賴註入(MS.DI)庫來實作事件驅動的通訊。

unset unset 0. 基礎知識準備-MediatR的基本用法 unset unset

MediatR 中有兩種訊息傳遞的方式:

  • Request/Response ,用於一個單獨的Handler。

  • Notification ,用於多個Handler。

  • Request/Response

    Request/Response 有點類似於 HTTP 的 Request/Response,發出一個 Request 會得到一個 Response。

    Request 訊息在 MediatR 中,有兩種型別:

  • IRequest<T> 返回一個T型別的值。

  • IRequest 不返回值。

  • 對於每個 request 型別,都有相應的 handler 介面:

  • IRequestHandler<T, U> 實作該介面並返回 Task<U>

  • RequestHandler<T, U> 繼承該類並返回 U

  • IRequestHandler<T> 實作該介面並返回 Task<Unit>

  • AsyncRequestHandler<T> 繼承該類並返回 Task

  • RequestHandler<T> 繼承該類不返回

  • Notification

    Notification 就是通知,呼叫者發出一次,然後可以有多個處理者參與處理。

    unset unset 1. 準備工作 unset unset

    首先,確保你的Avalonia計畫中已經安裝了必要的NuGet包。你將需要 Prism.DryIoc.Avalonia 作為依賴註入容器,以及 MediatR 來處理事件的釋出和訂閱。此外,為了將MediatR整合到DryIoc容器中,你還需要 DryIoc.Microsoft.DependencyInjection 包(這裏感謝網友 提供的技術解答)。

    在計畫的 .csproj 檔或NuGet包管理器中添加以下參照:

    <PackageReferenceInclude="Prism.DryIoc.Avalonia"Version="8.1.97.11072" />
    <PackageReferenceInclude="MediatR"Version="12.2.0" />
    <PackageReferenceInclude="DryIoc.Microsoft.DependencyInjection"Version="8.0.0-preview-01" />

    unset unset 2. 配置容器和註冊服務 unset unset

    在Avalonia計畫中,你需要配置DryIoc容器以使用Microsoft的DI擴充套件,並註冊MediatR服務。這通常在你的主啟動類(如 App.axaml.cs )中完成。

    以下是配置容器和註冊服務的範例程式碼:

    namespaceCodeWF.Tools.Desktop;
    public classApp : PrismApplication
    {
    // 省略了模組註入等和主題無關的程式碼,有興趣源碼在文末可查
    ///<summary>
    /// 1、DryIoc.Microsoft.DependencyInjection低版本可不要這個方法(5.1.0及以下)
    /// 2、高版本必須,否則會丟擲異常:System.MissingMethodException:「Method not found: 'DryIoc.Rules DryIoc.Rules.WithoutFastExpressionCompiler()'.」
    /// 參考issues:https://github.com/dadhi/DryIoc/issues/529
    ///</summary>
    ///<returns></returns>
    protectedoverride Rules CreateContainerRules()
    {
    return Rules.Default.WithConcreteTypeDynamicRegistrations(reuse: Reuse.Transient)
    .With(Made.Of(FactoryMethod.ConstructorWithResolvableArguments))
    .WithFuncAndLazyWithoutRegistration()
    .WithTrackingDisposableTransients()
    //.WithoutFastExpressionCompiler()
    .WithFactorySelector(Rules.SelectLastRegisteredFactory());
    }
    protectedoverride IContainerExtension CreateContainerExtension()
    {
    IContainer container = new Container(CreateContainerRules());
    container.WithDependencyInjectionAdapter();
    returnnew DryIocContainerExtension(container);
    }
    protectedoverridevoidRegisterRequiredTypes(IContainerRegistry containerRegistry)
    {
    base.RegisterRequiredTypes(containerRegistry);
    IServiceCollection services = ConfigureServices();
    IContainer container = ((IContainerExtension<IContainer>)containerRegistry).Instance;
    container.Populate(services);
    }
    privatestatic ServiceCollection ConfigureServices()
    {
    var services = new ServiceCollection();
    // 註入MediatR
    var assemblies = AppDomain.CurrentDomain.GetAssemblies().ToList();
    // 添加模組註入,未顯示呼叫模組型別前,模組程式集是未載入到當前程式域`AppDomain.CurrentDomain`的
    var assembly = typeof(SlugifyStringModule).GetAssembly();
    assemblies.Add(assembly);
    services.AddMediatR(configure =>
    {
    configure.RegisterServicesFromAssemblies(assemblies.ToArray());
    });
    return services;
    }
    }










    在上面的程式碼中,我們重寫了 CreateContainerRules CreateContainerExtension RegisterRequiredTypes 方法以配置DryIoc容器,並註冊了MediatR服務和相關處理常式。

    註意,在註冊MediatR服務時,我們從當前已載入的程式集列表中尋找並註冊處理常式。如果模組是按需載入的,請確保在註冊處理常式之前已載入了相應的模組。

    此外,我們還演示了如何手動添加模組程式集到列表中以便註冊處理常式。這通常在你需要顯式控制哪些模組和處理常式被註冊時很有用。但是,請註意,在大多數情況下,你可能希望使用更自動化的方式來載入和註冊模組及處理常式(例如,透過掃描特定目錄或使用約定等)。這取決於你的具體需求和計畫結構。

    另外,請註意程式碼中的註釋和說明,它們提供了有關每個步驟和配置的額外資訊。在實際計畫中,你可能需要根據計畫的實際情況和需求進行相應的調整和最佳化。例如,你可能需要處理迴圈依賴、配置作用域、使用攔截器或裝飾器等高級功能。這些都可以在DryIoc和MediatR的文件中找到更詳細的說明和範例。

    unset unset 3. MediatR2種傳遞方式 unset unset

    有了前面的基礎知識準備,我們添加類別庫工程 CodeWF.Tools.MediatR.Notifications ,並添加請求定義(主工程及模組的響應處理常式需要實作):

    public classTestRequest : IRequest<string>
    {
    publicstring? Args { getset; }
    }

    添加通知定義:

    public classTestNotification : INotification
    {
    publicstring? Args { getset; }
    }

    請求和通知定義結構一樣,只有一個字串參數,只是實作介面不同。

    unset unset 4. 添加處理常式 unset unset

    範例工程結構如下,因為該開源計畫(文末連結)寫在站長的AvaloniaUI桌面工具工程,本文只關註如下圖3個工程即可:

    程式結構

    在AvaloniaUI主工程(CodeWF.Tools.Desktop)添加請求響應處理常式:

    public classTestHandler : IRequestHandler<TestRequeststring>
    {
    publicasync Task<stringHandle(TestRequest request, CancellationToken cancellationToken)
    {
    returnawait Task.FromResult($"主工程處理常式:Args = {request.Args}, Now = {DateTime.Now}");
    }
    }

    添加通知響應處理常式:

    public class TestNotificationHandler(INotificationService notificationService) : INotificationHandler<TestNotification>
    {
    public Task Handle(TestNotification notification, CancellationToken cancellationToken)
    {
    notificationService.Show("Notification",
    $"主工程Notification處理常式:Args = {notification.Args}, Now = {DateTime.Now}");
    return Task.CompletedTask;
    }
    }

    在模組【CodeWF.Tools.Modules.SlugifyString】中添加請求響應處理常式(因為順序關系,不會觸發,這裏添加只是演示請求為一對一響應):

    public classTestHandler : IRequestHandler<TestRequeststring>
    {
    publicasync Task<stringHandle(TestRequest request, CancellationToken cancellationToken)
    {
    returnawait Task.FromResult($"模組【SlugifyString】Request處理常式:Args = {request.Args}, Now = {DateTime.Now}");
    }
    }

    添加通知響應處理常式(會和主工程通知響應處理常式一樣被觸發):

    public class TestNotificationHandler(INotificationService notificationService) : INotificationHandler<TestNotification>
    {
    public Task Handle(TestNotification notification, CancellationToken cancellationToken)
    {
    notificationService.Show("Notification",
    $"模組【SlugifyString】Notification處理常式:Args = {notification.Args}, Now = {DateTime.Now}");
    return Task.CompletedTask;
    }
    }

    幾個響應處理常式類別定義類似:收到請求時,返回格式化字串;收到通知時,彈出提示表明當前是哪個位置收到的通知,便於演示效果。

    unset unset 5. 請求和通知演示 unset unset

    觸發操作我們寫在模組【CodeWF.Tools.Modules.SlugifyString】中,在模組的ViewModel類裏透過依賴註入獲取請求和通知的發送者例項ISender和IPublisher:

    using Unit = System.Reactive.Unit;
    namespaceCodeWF.Tools.Modules.SlugifyString.ViewModels;
    public classSlugifyViewModel : ViewModelBase
    {
    // 省略別名轉換相關邏輯程式碼,源碼文末檢視
    privatereadonly INotificationService _notificationService;
    privatereadonly IClipboardService? _clipboardService;
    privatereadonly ITranslationService? _translationService;
    publicSlugifyViewModel(INotificationService notificationService, IClipboardService clipboardService,
    ITranslationService translationService, ISender sender, IPublisher publisher
    ) : base(sender, publisher)

    {
    _notificationService = notificationService;
    _clipboardService = clipboardService;
    _translationService = translationService;
    KindChanged = ReactiveCommand.Create<TranslationKind>(OnKindChanged);
    }

    publicasync Task ExecuteMediatRRequestAsync()
    {
    var result = Sender.Send(new TestRequest() { Args = To });
    _notificationService.Show("MediatR"$"收到響應:{result.Result}");
    }
    publicasync Task ExecuteMediatRNotificationAsync()
    {
    await Publisher.Publish(new TestNotification() { Args = To });
    }
    }





    點選 測試MediatR-Request 按鈕觸發呼叫 ISender.Send 發出請求並得到響應,透過點選 測試MediatR-Notification 按鈕觸發呼叫 IPublisher.Publish 發出通知。

    請求效果:

    看上面的請求效果:雖然在主工程和模組工程都註冊了一個響應,但只有主工程被觸發。

    通知效果:

    在主工程和模組工程都註冊了一個通知響應,所以兩個處理常式都彈出了提示。

    unset unset 6. 總結 unset unset

    為什麽使用MediatR,而未使用Prism的事件聚合器?

    站長開發工具做了線上版(https://blazor.dotnet9.com),也做了跨平台桌面版本(AvaloniaUI),兩個版本使用MediatR可以復用大部份事件程式碼。

    參考

    文中寫了主要程式碼,但可能缺失部份細節,源碼連結如下,歡迎留言交流。

    參考文章: MediatR 在 .NET 套用中的實踐 [1]

    本文源碼: Github [2]

    參考資料

    [1]

    MediatR 在 .NET 套用中的實踐: https://yimingzhi.net/2021/12/mediatr-zai-dotnet-ying-yong-zhong-de-shi-jian

    [2]

    Github: https://github.com/dotnet9/CodeWF/tree/main/src/CodeWF.Tools.Desktop