zl程序教程

您现在的位置是:首页 >  数据库

当前栏目

MAUI新生2.3-数据绑定和MVVM:MVVM开发模式

2023-04-18 15:25:53 时间

一、为什么需要声明式开发

.NET的MVVM,始于WPF,很古典,它甚至可能是现代前端框架“声明式开发”的鼻祖。声明式开发,之所以出现,是因为命令式开发在UI层和代码层上无法解耦的问题。如下图所示:

 

 

1、命令式开发:后台代码需要调用UI层的控件(label.Text),如果更新UI层,则后台代码也要同步进行更改,耦合性强

2、声明式开发:ViewModel对View层(UI)是无感的,不需要知道哪个View绑定了它,即使更新UI,ViewModel也不需要做任何变化。ViewModel将数据和逻辑抽象出来,实现了UI和数据逻辑的解耦。

3、为什么声明式就比命令式好:因为实际开发过程中,UI需求的变更性是很频繁,而数据逻辑相对稳定。

4、绑定补充:无论是控件与控件的绑定,还是控件与代码对象的绑定,本质上都是对象与对象链接属性之间的绑定。但是,两者实现方式有差异,控件之间的绑定,通过可绑定对象(BindableObject)和可绑定属性(BindableProperty)来实现,而控件与代码对象之间的绑定,通过事件机制来实现,在Toolkit.Mvvm中,称之为可观察对象(ObservableObject)和可观察属性(ObservableProperty)。

 

 

二、MAUI中使用最古典的MVVM

1、ViewModel层(MainPageViewModel.cs)。一般先开发ViewModel层,先设计好数据、逻辑和业务,再去设计UI。

//MainPageViewModel类实现了INotifyPropertyChanged接口
public class MainPageViewModel : INotifyPropertyChanged
{
    //PropertyChanged事件,是View层和ViewModel层链接的桥梁
    //View层通过Binding机制,将更改属性的方法委托给PropertyChanged事件,当触发PropertyChanged事件时,执行View层的这个方法
    //OnPropertyChanged方法,将触发PropertyChanged事件,并传入属性名和属性值等参数。这个方法,在属性的Set函数中调用。
    public event PropertyChangedEventHandler PropertyChanged;
    public void OnPropertyChanged([CallerMemberName] string name = "") => 
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); //创建Result属性,并在属性的Set函数中,执行OnPropertyChanged方法,从而触发PropertyChanged事件 private string _result = "HiWorld!"; public string Result { get => _result; set { if (_result != value) { _result = value; //属性值发生变化时,执行OnPropertyChanged方法 //如果不传参,则传入本属性。可以通过OnPropertyChanged("属性名")方式,传入指定属性 OnPropertyChanged(); } } } //创建ClickMeCommand命令 //命令本质上是ICommand类型的属性,需要在构造函数中初始化,并定义触发命令时的回调函数 public ICommand ClickMeCommand { get; private set; } public MainPageViewModel() { ClickMeCommand = new Command(() => { this.Result = "你好世界!"; }); } }

 

2、View层(MainPage.xaml)。在View层有两个工作,一是将ViewModel对象,设置为BindingContext;二是绑定View的控件属性和ViewModel层的属性或命令。

<ContentPage
    x:Class="MauiApp8.MainPage"
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:vm="clr-namespace:MauiApp8.ViewModels">

    <ContentPage.BindingContext>
        <vm:MainPageViewModel />
    </ContentPage.BindingContext>

    <StackLayout Padding="30">
        <!--绑定ViewModel对象的Result属性-->
        <Label Text="{Binding Result}"/>
        <!--绑定ViewModel对象的ClickMeCommand命令。如果命令要传入参数,可以设置【CommanParameter=""】属性-->
        <Button Text="点击修改为中文" Command="{Binding ClickMeCommand}"/>
    </StackLayout>

</ContentPage>

 

 

 

三、使用更加现代的Toolkit.Mvvm

古典的MVVM虽然实现了UI和数据业务的解耦,但是使用起来比较繁琐,官方也一直没有提供更简洁的实现方式。所以,涌现出了很多优秀的第三方库,比如MvvmLignt、MvvmCross、Prism等。不过现在有一个半官方的MVVM框架,CommunityToolkit.Mvvm,它不仅实现了更加简洁的ViewModel,而且更进一步,通过source generators(源生成器?),带来类似Vue和Blazor的爽快体验。PS:使用前应先安装nuget包CommunityToolkit.Mvvm;

 

1、一般模式 

//ObservableObject实现了INotifyPropertyChanged和INotifyPropertyChanging接口,并提供SetProperty、RelayCommand等成员
public class MainPageViewModel : ObservableObject
{
    //定义一个无参构造函数,方便创建对象时调用,因为下例中使用了有参构造函数
    public MainPageViewModel() { } 


    //①简单类型属性和命令。注:RelayCommand的异步为AsyncRelayCommand===============================================
    private string result = "HiWorld!";
    public string Result
    {
        get => result;
        //SetProperty方法,比较旧值和新值是否相等,如果不相等,则将新值value赋值给_result,并触发属性更改事件
        set => SetProperty(ref result, value);
    }

    private RelayCommand clickMeCommand;
    public RelayCommand ClickMeCommand =>
        clickMeCommand ??= new RelayCommand(() => Result = "你好世界!");//如果_clickMeCommand不为空,则赋值回调函数



    //②集合类型属性和命令(带参数)=================================================================================
//注,①集合List无事件通知机制,不能用于绑定属性;②RelayCommand<T>的异步为AsyncRelayCommand<T>
private ObservableCollection<string> names = new ObservableCollection<string> { "zs","ls","ww"}; public ObservableCollection<string> Names { get => names; set => SetProperty(ref names, value); } private RelayCommand<string> addNameCommand; public RelayCommand<string> AddNameCommand => addNameCommand ??= new RelayCommand<string>(AddName); private void AddName(string name) { Names.Add(name); } //③复杂类型的某个属性,较少使用================================================================================= //可以实现复杂类型某个属性更改通知,较少使用 //如果是定义User属性,则整个对象替换时,才会有属性更改通知,也就是引用类型是浅绑定,类似于Vue2的引用类型data private readonly User user; public MainPageViewModel(User user) => this.user = user; public string Name { get => user.Name; //SetProperty重载方法,user.Name-旧值,value-新值,user-复杂类型。判断user.Name和新值value是否相等,如果不想等,则将新值赋值给u.Name,并触发属性更改事件 set => SetProperty(user.Name, value, user, (user, value) => user.Name = value); } //④Task类型属性,较少使用===================================================================================== //主要用于加载任务的提示,如任务完成,通知UI更新,还没使用过。 private TaskNotifier<int> requestTask; public Task<int> RequestTask { get => requestTask; set => SetPropertyAndNotifyOnCompletion(ref requestTask, value); }
public void RequestValue()
{
RequestTask = WebService.LoadMyValueAsync();
}
}

 

2、SourceGenerators模式。注:需要将ViewModel类改为部分类,加partial修饰符。

public partial class MainPageViewModel : ObservableObject
{
    //定义可观察属性================================================================================================
    [ObservableProperty] //标注为可观察属性
    [NotifyPropertyChangedFor(nameof(FullName))] //当FirstName属性发生变化时,通知FullName响应
    private string firstName; //定义属性的back字段即可

    [ObservableProperty]
    [NotifyPropertyChangedFor(nameof(FullName))]
    private string lastName;

    //因FirstName和LastName是可观察属性,FullName也具有可观察特性。类似Vue的计算属性-computed
    public string FullName => $"{FirstName}{LastName}";



    //可以监听属性变化。类似于Vue中的Watch============================================================================
    [ObservableProperty]
    private int result;
    //两个监听方法的定义规则:
    //①两个方法可以同时存在,也可以任一个,或者都不定义
    //②按约定命名为“On+属性名+Changing”和“On+属性名+Changed”
    //③不能使用访问修饰符,如private等,必须使用partial修饰符
    //④方法参数为新值
    partial void OnResultChanging(int value)
    {
        Console.WriteLine($"Result将改变为{value}");
    }
    partial void OnResultChanged(int value)
    {
        Console.WriteLine($"Result已改变为{value}");
    }



    //定义命令=======================================================================================================
    //按约定自动生成名称为ChangeNameCommand的命令
    //如果ChangeName不带参,生成的命令属性为RelayCommand;如果带参,则为RelayCommand<T>。仅支持一个参数,不限制类型
    //如果ChangeName为异步方法,则生成的命令属性也是异步的,AsyncRelayCommand、AsyncRelayCommand<T>
    [RelayCommand]
    private void ChangeName(FullName fullName) 
    {
        if (fullName != null)
        {
            FirstName = fullName.FirstName;
            LastName= fullName.LastName;
        }
    }

    

    //控制命令是否可以执行。CanExcute的值为CanCallUser方法的返回值====================================================
    [RelayCommand(CanExecute = nameof(CanCallUser))]
    private async void CallUser(User? user)
    {
        await Application.Current.MainPage.DisplayAlert("title",$"Hi,{user.Name}","cancel");
    }
    //CanCallUser方法可以传入RelayCommand<T>命令的T参数
    private bool CanCallUser(User? user) 
    {
        return user is not null; //UI层将CommandParameter绑定为User对象,可使用资源字典
    }
    //属性可以触发命令的CanExcute的执行,实现在运行时,控制命令是否可以执行
    [ObservableProperty]
    [NotifyCanExecuteChangedFor(nameof(CallUserCommand))]
    private User? selectedUser; //UI层将CommandParameter绑定为SelectedUser属性



    //命令特性的另外两个参数,应用于异步命令,很少用,知道一下=======================================================   
    //①控制异步的执行
    [RelayCommand(IncludeCancelCommand = true)]
    private async Task DoWorkAsync(CancellationToken token) { }

    //控制异常的处理方式,默认值为false,如有异常,将导致应用崩溃。为true时,不会导致应用崩溃
    [RelayCommand(FlowExceptionsToTaskScheduler = true)]
    private async Task GreetUserAsync(CancellationToken token){}
}

 

3、实现可观察对象,除了继承ObservableObject之外,还提供的特性方式,主要用于解决多继承的问题。

[ObservableObject]
public partial class MainPageViewModel
{
}
//除了ObservableObject特殊之外,还提供了[INotifyPropertyChanged]、[ObservableRecipient]特性
//三者关系:ObservableObject实现了INotifyPropertyChanged,ObservableRecipient派生自ObservableObject

 

4、Toolkit.Mvvm除了带来更加简洁的属性和命令,还增加了属性验证和消息通知功能,将在下两个章节中介绍。

 

 

五、View和ViewModel的关联方式

1、方式一:创建ViewModel对象:在View中,通过设置BindingContext为ViewModel对象,即可进行绑定。如下所示:

<ContentPage
    ......
    xmlns:vm="clr-namespace:MauiApp8.ViewModels">

    <ContentPage.BindingContext>
        <vm:MainPageViewModel />
    </ContentPage.BindingContext>

    <!--子元素继承ContentPage的BindingContext-->
    <StackLayout Padding="30">
        <Entry Text="{Binding FirstName}" /> 
        <Entry Text="{Binding LastName}" />
        <Label Text="{Binding FullName}" />
    </StackLayout>

</ContentPage>

 

2、方式二:简单的依赖注入

//第一步:在MauiProgram.cs中,注册MainPageViewModel服务
//本质就是应用启动时,由框架帮我们创建MainPageViewModel对象
builder.Services.AddSingleton<MainPageViewModel>();

//第二步:在MainPage.xaml.cs后台代码中,注入服务,设置BindingContext
public partial class MainPage : ContentPage
{
    public MainPage(MainPageViewModel viewModel)
    {
        InitializeComponent();
        this.BindingContext = viewModel;
    }
}

 

3、方式三(推荐):更加优雅的依赖注入,通过自定义容器和服务定位器实现

(1)第一步:自定义IOC容器和服务定位器类,统一在这个类中,注册ViewModel服务和获取服务 

public class ServiceLocator
{
    //服务定位器字段,使用这个字段来获取服务
    private IServiceProvider serviceProvider;
    //******以下定义属性,通过serviceProvider返回需要的服务(对象)
    public MainPageViewModel MainPageViewModel => serviceProvider.GetService<MainPageViewModel>();

    //构造函数,创建ServiceLocator对象时,①创建容器;②注册ViewModel服务;③创建服务定位器。
    public ServiceLocator()
    {
        var serviceCollection = new ServiceCollection();
        //******以下注册服务
        serviceCollection.AddSingleton<MainPageViewModel>();

        //【注意顺序】,服务定义器在注册完所有服务后,再创建
        serviceProvider = serviceCollection.BuildServiceProvider();
    }
}

 

 

 (2)第二步:在根页面App.xaml的资源字典中,创建ServiceLocator对象 ,一次性注册所有需要的服务

<Application
    x:Class="MauiApp8.App"
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:local="clr-namespace:MauiApp8">
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="Resources/Styles/Colors.xaml" />
                <ResourceDictionary Source="Resources/Styles/Styles.xaml" />
                <ResourceDictionary>
                    <local:ServiceLocator x:Key="ServiceLocator"/>
                </ResourceDictionary>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>
</Application>

 

 

(3)第三步:在View层,MainPage.xaml页面中,设置BindingContext,绑定ServiceLocator对象的MainPageViewModel属性

<ContentPage
    x:Class="MauiApp10.MainPage"
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    BindingContext="{Binding MainPageViewModel, Source={StaticResource ServiceLocator}}">

    <VerticalStackLayout>
        <Entry Text="{Binding FirstName}" />
        <Entry Text="{Binding LastName}" />
        <Label Text="{Binding FullName}" />
    </VerticalStackLayout>

</ContentPage>