大家好,我是沙漠盡頭的狼!
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 { get; set; }
}
添加通知定義:
public classTestNotification : INotification
{
publicstring? Args { get; set; }
}
請求和通知定義結構一樣,只有一個字串參數,只是實作介面不同。
unset unset 4. 添加處理常式 unset unset
範例工程結構如下,因為該開源計畫(文末連結)寫在站長的AvaloniaUI桌面工具工程,本文只關註如下圖3個工程即可:
在AvaloniaUI主工程(CodeWF.Tools.Desktop)添加請求響應處理常式:
public classTestHandler : IRequestHandler<TestRequest, string>
{
publicasync Task<string> Handle(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<TestRequest, string>
{
publicasync Task<string> Handle(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
Github:
https://github.com/dotnet9/CodeWF/tree/main/src/CodeWF.Tools.Desktop