乘风破浪,遇见最佳跨平台跨终端框架.Net Core/.Net生态 - WPF应用整合依赖注入(DI)、MediatR、CommunityToolkit.Mvvm、Behaviors

前言

之前一直用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 / 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; }

它会帮我们在编译的时候,自动生成前面那种代码,省去了重复代码量。

更简化的写法

其实CommunityToolkit.Mvvm支持更加简化的写法,上面的通知,按如下写,它会自动生成剩余代码,效果是一样的。

这里有个语法约定,一个是要加[ObservableProperty],第二个是只需要写小写的属性就行,大写的会自动生成。

/// <summary>
/// 数量
/// </summary>
[ObservableProperty]
public int count;

如果换成事件也是如此,如下代码就等同写了CreateAppleCommand

/// <summary>
/// 创建苹果
/// </summary>
[RelayCommand]
public async Task CreateApple()
{

}

引入Behavior支持

引入Microsoft.Xaml.Behaviors.Wpf包

依赖包

Microsoft.Xaml.Behaviors.Wpf支持从.Net Framework 4.5到.Net Core 3.1、.Net 5都可以。

image

dotnet add package Microsoft.Xaml.Behaviors.Wpf

并非所有控件都能支持Command,那么对非标准的,我们可以使用Behaviors机制来实现。

简单示例Commbox

<Page
	xmlns:Behaviors="http://schemas.microsoft.com/xaml/behaviors"
	>
	<ComboBox
	Margin="0,8,0,0"
	x:Name="KeywordComboBox"
	ItemsSource="{Binding FilterContext.FilterKeywords}"
	DisplayMemberPath="Name"
	>
	<Behaviors:Interaction.Triggers>
		<Behaviors:EventTrigger EventName="SelectionChanged">
			<Behaviors:InvokeCommandAction Command="{Binding SelectFilterKeywordCommand}" CommandParameter="{Binding SelectedItem, ElementName=KeyWordComboBox}" />
		</Behaviors:EventTrigger>
	</Behaviors:Interaction.Triggers>
	</ComboBox>
</Page>

其他用法

<Grid Grid.Column="0" Margin="0,0,10,0">
	<Rectangle Width="40" Height="40" Fill="DeepPink">
		<Behaviors:Interaction.Behaviors>
			<Behaviors:MouseDragElementBehavior/>
		</Behaviors:Interaction.Behaviors>
	</Rectangle>
</Grid>
<Button x:Name="button" Content="Click Me" HorizontalAlignment="Stretch" Grid.Row="1" Margin="0,10,0,10">
	<Behaviors:Interaction.Triggers>
		<Behaviors:EventTrigger EventName="Click" SourceObject="{Binding ElementName=button}">
			<Behaviors:LaunchUriOrFileAction Path="https://www.visualstudio.com" />
		</Behaviors:EventTrigger>
	</Behaviors:Interaction.Triggers>
</Button>
<Button x:Name="RemoveButton" Content="Remove Rectangle" HorizontalAlignment="Stretch" Grid.Row="1" VerticalAlignment="Stretch" Margin="0,10,0,10" d:LayoutOverrides="Width, Height" FontSize="20" Foreground="LightGray" Background="DarkGray" BorderBrush="LightYellow">
	<Behaviors:Interaction.Triggers>
		<Behaviors:EventTrigger EventName="Click">
			<Behaviors:RemoveElementAction TargetName="Rectangle" />
		</Behaviors:EventTrigger>
	</Behaviors:Interaction.Triggers>
</Button>
<Button x:Name="button" Content="Click Me" HorizontalAlignment="Stretch" Grid.Row="1" VerticalAlignment="Stretch" Margin="0,10,0,10" d:LayoutOverrides="Width, Height" FontSize="20" Foreground="LightGray" Background="DarkGray" BorderBrush="LightYellow">
	<Behaviors:Interaction.Triggers>
		<Behaviors:EventTrigger EventName="Click" SourceObject="{Binding ElementName=button}">
			<Behaviors:CallMethodAction TargetObject="{Binding}" MethodName="IncrementCount"/>
		</Behaviors:EventTrigger>
	</Behaviors:Interaction.Triggers>
</Button>

自定义控件

依赖属性(DependencyProperty)

界面

<TextBlock
    x:Name="TextGroupName"
    Margin="0,0,0,0"
    Foreground="{DynamicResource TextFillColorSecondaryBrush}"
    />

定义

/// <summary>
/// 分组名称依赖属性
/// </summary>
public static readonly DependencyProperty GroupNameProperty = DependencyProperty.Register
(
    nameof(GroupName),
    typeof(string),
    typeof(FilterConditionControl),
    new PropertyMetadata(string.Empty, new PropertyChangedCallback((d, e) =>
    {
        if (d is not FilterConditionControl control)
        {
            return;
        }
        control.UpdateGroupName();
    }))
);

/// <summary>
/// 分组名称
/// </summary>
public string GroupName
{
    get => (string)GetValue(GroupNameProperty);
    set => SetValue(GroupNameProperty, value);
}

/// <summary>
/// 更新分组名称
/// </summary>
public void UpdateGroupName()
{
    TextGroupName.Text = GroupName;
}

调用

<controls:FilterConditionControl
    Margin="0,12,0,0"
    GroupName="历史条件"
    />

引入MediatR支持

添加MediatR包(最新)

依赖包,适用于>= v12.0.0

https://www.nuget.org/packages/MediatR

dotnet add package MediatR

添加MediatR包(过时)

有了官方DI,添加对MediatR的支持就简单了。

依赖包,适用于< v12.0.0

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

dotnet add package MediatR.Extensions.Microsoft.DependencyInjection

注册MediatR(最新)

通过程序集扫描来注入MediatR相关服务。

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

注册MediatR(过时)

通过程序集扫描来注入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));
    }
}

这样运行也是一样的,变成了中介者模式,面向消息事件编程了。

Wpf和Winfroms共存

修改项目配置

在.Net Core的WPF中如果想引用System.Windows.Forms空间该怎么办呢?编辑项目文件。

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net6.0-windows</TargetFramework>
    <UseWPF>true</UseWPF>
    <AssemblyName>AgvHub.Toolbox</AssemblyName>
    <ApplicationIcon>AgvHub.Toolbox.ico</ApplicationIcon>
    <RuntimeIdentifiers>win-x86;win-x64</RuntimeIdentifiers>
    <UseWindowsForms>true</UseWindowsForms>
  </PropertyGroup

加入<UseWindowsForms>true</UseWindowsForms>这一项,这时候就可以很自然的引用Winfroms下的命名空间了。

比如这时候,你就可以使用System.Windows.Forms.FolderBrowserDialog了。

参考

posted @ 2022-12-04 17:57  TaylorShi  阅读(583)  评论(0编辑  收藏  举报