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>