乘風破浪,遇見最佳跨平臺跨終端框架.Net Core/.Net生態 - WPF應用整合依賴注入(DI)、MediatR、CommunityToolkit.Mvvm

前言

之前一直用Stylet,寫過兩篇乘風破浪,遇見Stylet超清爽WPF御用MVVM框架,愛不釋手的.Net Core輕量級MVVM框架乘風破浪,超清爽WPF御用MVVM框架Stylet,啓動到登錄設計的高階實戰,用這確實很爽,在MVVM這塊非常省心,用起來有點在寫UWP的感覺。

但是這個玩意自帶了IOC,我又想引入MediatR來實現面向消息事件的編程(這個做客戶端的同學可能不熟悉哈哈,一般大家都是面向接口編程),它只針對.Net Core原汁原味的DI有擴展支持,所以不得不最終棄用Stylet

離開了Stylet,我就沒有了MVVM的配套支持,怎麼辦呢?想起了之前寫的乘風破浪,遇見MVVM Toolkit官方社區首推MVVM框架,後UWP時代的拯救版MVVM框架,這是有社區維護的一個MVVM框架,用它肯定沒錯了,牛逼的是,微軟改名部這麼快就把它從Microsoft.Toolkit.Mvvm改名爲CommunityToolkit.Mvvm了。

image

所以最終的組合是:

  • .Net 6
  • Microsoft.Extensions.DependencyInjection
  • MediatR.Extensions.Microsoft.DependencyInjection
  • CommunityToolkit.Mvvm
  • PropertyChanged.Fody

引入官方依賴注入(DI)

https://github.com/TaylorShi/HelloNetWPF

準備示例項目

這個倒是不難,這裏我們使用.Net 6/5/3.1來創建WPF應用哈。

image

image

image

引入依賴包

依賴包

https://www.nuget.org/packages/Microsoft.Extensions.DependencyInjection

dotnet add package Microsoft.Extensions.DependencyInjection

image

基本使用

剛創建完的項目模板中,App.Xaml.cs中很簡單,啥都沒有。

/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
}

我們準備一個熟悉的函數ConfigureServices來得到IServiceProvider

private static IServiceProvider ConfigureServices()
{
    var services = new ServiceCollection();

    return services.BuildServiceProvider();
}

這裏我們利用它的構造函數來調用這個函數

public IServiceProvider Services { get; }

public App()
{
    InitializeComponent();
    Services = ConfigureServices();
}

這裏定義一個Services對象來託管住IServiceProvider,方便後面做個全局使用。

基於DI實現頁面窗體展示

註冊窗體到容器中

既然有了DI,那我們就要用起來,把窗體丟進去,從容器中拿出來用。

private static IServiceProvider ConfigureServices()
{
    var services = new ServiceCollection();
    services.AddSingleton<MainWindow>();
    return services.BuildServiceProvider();
}

這裏把MainWindow頁面做個單實例注入。

使用容器中窗體

我們在定一個啓動函數OnStartup,把它取出來,並且展示出來。

private void OnStartup(object sender, StartupEventArgs e)
{
    var mainWindow = Services.GetService<MainWindow>();
    mainWindow?.Show();
}

改造啓動入口

上哪去調用這個啓動函數OnStartup呢?我們去App.xaml.cs對應的App.xaml改動下。

改動前,它是使用StartupUri直接導向MainWindow.xaml的,這裏我們把它換成Startup函數指定。

<Application x:Class="demoForWpfApp60.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:demoForWpfApp60"
             StartupUri="MainWindow.xaml">
    <Application.Resources>

    </Application.Resources>
</Application>
<Application x:Class="demoForWpfApp60.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:demoForWpfApp60"
             Startup="OnStartup">
    <Application.Resources>

    </Application.Resources>
</Application>

這樣就使用了容器來管理界面了。

image

.Net 5或者.Net Core 3.1也可以

雖然在.Net 5.Net Core 3.1會遇到一些不友好的警告提示,但是還是能成功運行起來。

Microsoft.Extensions.DependencyInjection.Abstractions 7.0.0 doesn't support net5.0-windows and has not been tested with it. Consider upgrading your TargetFramework to net6.0 or later. You may also set <SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings> in the project file to ignore this warning and attempt to run in this unsupported configuration at your own risk.
Microsoft.Extensions.DependencyInjection.Abstractions 7.0.0 doesn't support netcoreapp3.1 and has not been tested with it. Consider upgrading your TargetFramework to net6.0 or later. You may also set <SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings> in the project file to ignore this warning and attempt to run in this unsupported configuration at your own risk.

image

引入Mvvm支持

引入CommunityToolkit.Mvvm包

依賴包

CommunityToolkit.Mvvm最低也要.Net 6了,其的前身就是Microsoft.Toolkit.Mvvm,如果是.Net 5或者.NET Standard 2.0可以考慮優先使用Microsoft.Toolkit.Mvvm

image

dotnet add package CommunityToolkit.Mvvm

image

dotnet add package Microsoft.Toolkit.Mvvm

添加ViewModel

創建一個簡單的ViewModel示例: MainViewModel,它繼承了ObservableObject,這是CommunityToolkit.Mvvm中一個通過實現INotifyPropertyChangedINotifyPropertyChanging接口可觀察的對象的基類。它可以用作需要支持屬性更改通知的各種對象的起點。

internal class MainViewModel : ObservableObject
{

}
namespace CommunityToolkit.Mvvm.ComponentModel;

/// <summary>
/// A base class for objects of which the properties must be observable.
/// </summary>
public abstract class ObservableObject : INotifyPropertyChanged, INotifyPropertyChanging
{
}

然後在容器中註冊它,這裏我們把它註冊爲瞬時模式

private static IServiceProvider ConfigureServices()
{
    var services = new ServiceCollection();
    services.AddTransient<MainViewModel>();
    services.AddSingleton<MainWindow>();
    return services.BuildServiceProvider();
}

同時,我們構建一個靜態變量來託管當前的App實例。

public partial class App : Application
{
    public new static App Current => (App)Application.Current;
}

然後在View界面使用它作爲數據上下文

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        this.DataContext = App.Current.Services.GetService<MainViewModel>();
    }
}

這樣就可以了,當前MainViewModelMainView就對應起來了。

添加簡單綁定

接下來我們在MainViewModel添加簡單屬性和事件及命令。

internal class MainViewModel : ObservableObject
{
    private int _count;
    public int Count
    {
        get => _count;
        set => SetProperty(ref _count, value);
    }

    public ICommand AddCountCommand => new RelayCommand(AddCount);

    public void AddCount()
    {
        Count++;
    }
}

在界面上我們構建一個按鈕吧

<Window x:Class="demoForWpfApp60.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:demoForWpfApp60"
        mc:Ignorable="d"
        Title="demoForWpfApp60" Height="450" Width="800">
    <Grid>
        <Button Content="{Binding Count}" Command="{Binding AddCountCommand}"/>
    </Grid>
</Window>

就這麼簡單,就可以運行起來了。

image

支持異步的命令綁定

public IAsyncRelayCommand RemoveCountCommand => new AsyncRelayCommand(RemoveCount);

public async Task RemoveCount()
{
    await Task.Run(() =>
    {
        Count--;
    });
}
<Window x:Class="demoForWpfApp60.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:demoForWpfApp60"
        mc:Ignorable="d"
        Title="demoForWpfApp60" Height="450" Width="800">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*"></RowDefinition>
            <RowDefinition Height="*"></RowDefinition>
        </Grid.RowDefinitions>
        <Button 
            Grid.Row="0"
            Content="{Binding Count}"
            Command="{Binding AddCountCommand}"
            />
        <Button 
            Grid.Row="1" 
            Content="{Binding Count}"
            Command="{Binding RemoveCountCommand}"
            />
    </Grid>
</Window>

image

更優雅的通知界面

之前我們寫一個可觀察且通知界面的屬性是這樣寫的。

private int _count;
public int Count
{
    get => _count;
    set => SetProperty(ref _count, value);
}

能不能更優雅一點呢,可以,引入PropertyChanged.Fody包,它會自動在編譯時給已知屬性注入IL代碼,以達到PropertyChanged通知的效果。

https://www.nuget.org/packages/PropertyChanged.Fody

dotnet add package PropertyChanged.Fody

直接寫成

public int Count { get; set; }

它會幫我們在編譯的時候,自動生成前面那種代碼,省去了重複代碼量。

引入MediatR支持

添加MediaR包

有了官方DI,添加對MediatR的支持就簡單了。

依賴包

https://www.nuget.org/packages/MediatR.Extensions.Microsoft.DependencyInjection

dotnet add package MediatR.Extensions.Microsoft.DependencyInjection

基本使用

通過程序集掃描來注入MediatR相關服務。

private static IServiceProvider ConfigureServices()
{
    var services = new ServiceCollection();
    services.AddMediatR(typeof(MainViewModel).Assembly);
    return services.BuildServiceProvider();
}

創建命令和命令處理

internal class RemoveCountCommand: IRequest<int>
{
    public int Count { get; private set; }

    public RemoveCountCommand(int count) { this.Count = count; }
}

internal class RemoveCountCommandHandler : IRequestHandler<RemoveCountCommand, int>
{
    public async Task<int> Handle(RemoveCountCommand request, CancellationToken cancellationToken)
    {
        return await Task.FromResult(request.Count - 1);
    }
}

使用MediatR發送事件。

internal class MainViewModel : ObservableObject
{
    private int _count;
    public int Count
    {
        get => _count;
        set => SetProperty(ref _count, value);
    }

    private readonly IMediator _mediator;
    public MainViewModel(IMediator mediator)
    {
        _mediator = mediator;
    }

    public IAsyncRelayCommand RemoveCountCommand => new AsyncRelayCommand(RemoveCount);

    public async Task RemoveCount()
    {
        Count = await _mediator.Send(new RemoveCountCommand(Count));
    }
}

這樣運行也是一樣的,變成了中介者模式,面向消息事件編程了。

參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章