MAUI新生2.3-数据绑定和MVVM:MVVM开发模式
一、为什么需要声明式开发
.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>