官方文档:https://docs.microsoft.com/zh-cn/windows/communitytoolkit/mvvm/introduction
源码:https://github.com/CommunityToolkit/WindowsCommunityToolkit
Demo:https://github.com/CommunityToolkit/MVVM-Samples
由于MVVMLight已经停止维护,微软出于后续使用的考虑,自主开发、维护了MVVMToolkit框架。
MVVMToolkit相对于MVVMLight具有更快的效率和更强大功能,但是其运行框架要求.NET Standard2.0以上版本,也就是说Framework基本上用不了。
库安装
在MVVMLight框架中,当成功安装库后,会自动在程序集中新建MainViewModel(继承了ViewModelBase
)以及ViewModelLocaltor
类型,并且ViewModelLocaltor
类中还进行了对IOC容器的设置以及向IOC中注册类型。此外,还会在App.xaml中将ViewModelLcaltor
对象引入。而在MVVMToolkit框架中,成功安装库后,并不会自动完成这些事情。
基本组件
MvvmToolkit框架的基本组件如下
数据:ObservableObject
行为:RelayCommand
、AsyncRelayCommand
消息:WeakReferenceMessenger
、ObservableRecipient
、IRecipient
DataContext赋值
MvvmToolkit框架下,可以通过在视图的构造函数中,给视图的DataContext
赋值。(其实也就是用WPF的原始方式来给DataContext
赋值了)
当然,也可以通过IOC容器来获取后赋值给DataContext
,至于IOC的用法可以参考下文。
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new MainViewModel();
}
}
命令
一、同步命令
查看MVVMLight框架中的RelayCommand
类型,可以看到RelayCommand
实现了ICommand
接口,做了一些封装,使得我们可以在框架中比较方便的去实现一些简单命令。具体用法如下(这里在MainViewModel中定义命令)
RelayCommand(Action execute)
:RelayCommand
类的构造函数。
- execute为无参数的命令执行方法。
RelayCommand<T>(Action<T> execute)
:RelayCommand
类的构造函数。
- execute为有参数的命令执行方法。
public RelayCommand(Action execute, Func<bool> canExecute)
:RelayCommand
类的构造函数。
- execute为无参数的命令执行方法。
- canExecute为命令绑定对象的可用性判断方法。
public RelayCommand<T>(Action<T> execute, Func<bool> canExecute)
:RelayCommand
类的构造函数。
- execute为有参数的命令执行方法。
- canExecute为命令绑定对象的可用性判断方法。
public class MainViewModel : ViewModelBase
{
......
public ICommand BtnCommand
{
//get => new RelayCommand(() =>
//{
// //这里做无参命令的业务逻辑
//});
//get => new RelayCommand<object>(obj =>
//{
// //这里做有参命令的业务逻辑
//});
get => new RelayCommand<object>(obj =>
{
//这里定义命令的执行内容
},obj =>
{
//这里可以定义命令绑定对象可执行检查的业务逻辑
return true;
});
}
}
RelayCommand
中还定义了RaiseCanExecuteChanged()
方法,其实就是触发一下CanExecuteChanged
事件,需要的时候可以调用一下来更新命令绑定对象的可用状态。
需要注意的是,在创建RelayCommand
对象时候,如果使用了泛型,泛型不能使用值类型(如int
、double
等),可能是在RelayCommand
的处理过程中使用了反射,而值类型无法反射所以会报错,可以使用object
来接收后在进行拆箱转换。
任意事件触发命令
参考第九章 MVVM 中的任意“事件的绑定”章节
二、异步命令
AsyncRelayComman
是MVVMTooklit框架提供的用于执行异步命令的命令类型。
常用成员
AsyncRelayCommand(Func<Task> execute)
:创建异步命令对象,需要传入一个返回Task
类型的Func
委托。
ExecutionTask
:属性成员,获取执行命令后返回的Task
对象。
Status
:获取Task
对象当前的执行状态。
具体用法如下
创建命令
- 注意,异步命令对象的创建不能在属性的
get
逻辑中去创建,否则无法触发。
public class MainViewModel
{
public ICommand BtnCommand { get;}
public MainViewModel()
{
BtnCommand = new AsyncRelayCommand(DoCommand);
}
private async Task<string> DoCommand()
{
//这里很疑惑,命名返回类型是Task<string>,为什么定义成异步函数之后可以直接返回字符串
await Task.Delay(1000);
return "DoCommand Result";
}
}
创建转换器
public class TaskResultConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is Task task)
{
//GetResultOrDefault这个方法需要安装Microsoft.Toolkit库,用于在未指定泛型的情况下获取Task对象的结果。
return task.GetResultOrDefault();
}
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
Xaml中进行使用
<Window ......>
<Window.DataContext>
<local:MainVeiwModel/>
</Window.DataContext>
<Window.Resources>
<local:TaskResultConverter x:Key="converter"/>
</Window.Resources>
<Grid>
<StackPanel>
<TextBlock Text="{Binding BtnCommand.ExecutionTask.Status}"/>
<TextBlock Text="{Binding BtnCommand.ExecutionTask, Converter={StaticResource converter}}"/>
<Button Content="AsyncCommand" Command="{Binding BtnCommand}"/>
</StackPanel>
</Grid>
</Window>
消息系统
一、WeakReferenceMessenger
WeakReferenceMessenger
是MVVMToolkit框架提供的用于进行消息发送和接收从而实现跨模块访问数据的消息管理类型,用法与MVVMLight中的Messenger
类似。
1、常用成员
WeakReferenceMessenger.Default.Register<TMessage>(object recipient, MessageHandler<object, TMessage> handler)
:注册消息接收对象以及消息的处理函数。
- recipient:消息的接收对象。
- handler:一个委托类型,用于设置接收到消息时的执行函数。该委托函数需要接收两个参数,第一个参数为接收该消息的对象,第二个参数为接收到的消息。
- 注意,此函数的泛型必须为引用类型。
WeakReferenceMessenger.Default.Send<TMessage>(TMessage message)
:发送消息。
2、简单使用
如果只是需要做简单的消息接发(例如简单的字符串发送和接收),根据下列使用即可。
在主窗口后台代码中进行消息处理函数的注册
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
WeakReferenceMessenger.Default.Register<string>(this, DoMessageRecieved);
}
private void DoMessageRecieved(object obj, string msg)
{
//接收到消息后进行业务处理
}
}
在ViewModel的中定义命令并发送消息
public class MainVeiwModel
{
public ICommand BtnMessage
{
get => new RelayCommand(() =>
{
WeakReferenceMessenger.Default.Send<string>("SendMessage");
});
}
}
在xaml中使用
<Window ......>
<Window.DataContext>
<local:MainVeiwModel/>
</Window.DataContext>
<Grid>
<StackPanel>
<Button Content="BtnMessage" Command="{Binding BtnMessage}"/>
</StackPanel>
</Grid>
</Window>
3、复杂数据及回调
当消息的接收和发送中含有较为复杂的消息,或需要做一些回调处理的时候,就需要对发送的消息对象进行封装后再发送了。
消息类型的封装
这里虽然继承了ValueChangedMessage<T>
,但使用后发现,继承这个类仅仅是为了对发送的消息类型进行约束,即使不继承也是可以正常使用的。
public class MessageObject : ValueChangedMessage<string>
{
//可以进行回调函数的设定
public Action DoCallback { get; set; }
public string Message { get; set; }
public MessageObject(string msg):base(msg)
{
//可以做消息的业务处理
Message = msg;
}
public void Execute()
{
DoCallback();
}
}
ViewModel层中定义消息发送命令
public class MainVeiwModel
{
public ICommand BtnMessage
{
get => new RelayCommand(() =>
{
WeakReferenceMessenger.Default.Send<MessageObject>(new MessageObject("发送消息")
{
DoCallback = () =>
{
Debug.WriteLine("做一下消息回调");
}
});
});
}
}
View层中进行注册
在View层中进行消息接收对象以及消息处理函数的注册。
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
WeakReferenceMessenger.Default.Register<MessageObject>(this, DoMessageRecieved);
}
private void DoMessageRecieved(object obj, MessageObject messageObject)
{
//接收到消息后进行业务处理
messageObject.Execute();
}
}
xaml中使用
<Window ......>
<Window.DataContext>
<local:MainVeiwModel/>
</Window.DataContext>
<Grid>
<StackPanel>
<Button Content="BtnMessage" Command="{Binding BtnMessage}"/>
</StackPanel>
</Grid>
</Window>
4、精准发送
由于WeakReferenceMessenger
消息的发送是以广播形式的,因此只要接收对象注册时指定的接受类型与发送消息的类型一致时就会收到消息,如果在开发中需要对指定的接收对象发送消息,可以通过在注册时指定token,然后在发送时候也指定token就可以实现了。(跟MVVMLight的指定发送是一样的,只是实现细节略有不同)
WeakReferenceMessenger.Default.Register<TMessage, TToken>(object recipient, TToken token, MessageHandler<object, TMessage> handler)
:使用指定的token来注册接收消息对象及消息处理函数。
WeakReferenceMessenger.Default.Send<TMessage, TToken>(TMessage message, TToken token)
:向指定token的接收对象发送消息。
注册
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
WeakReferenceMessenger.Default.Register<string, string>(this, "tokenName", DoMessageRecieved);
}
private void DoMessageRecieved(object obj, string msg)
{
//消息处理函数
}
}
发送
public class MainVeiwModel
{
public ICommand BtnMessage
{
get => new RelayCommand(() =>
{
WeakReferenceMessenger.Default.Send<string, string>("SendMessage", "tokenName");
});
}
}
二、非注册形式
在MVVMToolkit中,可以不通过消息接收对象及处理函数的注册来实现消息的接收与发送,此时就需要用到ObservableRecipient
类和IRecipient
接口了。
1、简单实现
ViewModel层处理
不注册而实现消息的接收,需要实现两个类型:ObservableRecipient
(ObservableObject
的子类)和IRecipient<T>
。前者用于打开消息窗口,后者用于注册接收的消息类型和实现消息处理函数。
IsActive
:ObservableRecipient
的属性成员,决定是否打开消息接收。
Receive(TMessage message)
:IRecipient<T>
的方法成员,用于接收广播的消息。
public class MainVeiwModel: ObservableRecipient,IRecipient<string>
{
public MainVeiwModel()
{
//消息开关,打开后才能让Receive函数接收到消息
IsActive = true;
}
public void Receive(string message)
{
//这里就是用来接收消息的
}
//这里是消息的发送
public ICommand BtnMessage
{
get => new RelayCommand(() =>
{
WeakReferenceMessenger.Default.Send<string>("SendMessage");
});
}
}
xaml中绑定命令
<Window ......>
<Window.DataContext>
<local:MainVeiwModel/>
</Window.DataContext>
<Grid>
<StackPanel>
<Button Content="BtnMessage" Command="{Binding BtnMessage}"/>
</StackPanel>
</Grid>
</Window>
2、精准发送
在非注册形式下,如果想要向指定的接受对象发送消息,可以在实现IRecipient<T>
,泛型T使用特定的类型,然后在进行消息发送时,也将特定类型作为消息发送即可。
三、PropertyChangedMessage
PropertyChangedMessage<T>
专门用于Messenger
消息系统中,当有做属性变化广播处理的属性发生变化时,发送消息。泛型T用于指定接收哪种类型的属性变化所发送的消息。
注意,PropertyChangedMessage<T>
消息对象比较特殊,注册后,不需要主动去发送消息,当继承了ViewModelBase
的子类中的有做属性变化广播处理的属性发生变化时,会自动发送消息。
常用的属性成员
NewValue
:PropertyChangedMessage
对象的属性成员,用于获取变化属性的新值。
OldValue
:PropertyChangedMessage
对象的属性成员,用于获取变化属性的旧值。
示例
-
定义命令及属性变化广播
由于MVVMLight的函数设定,这里在ViewModel层中做属性变化的消息广播public class MainViewModel : ViewModelBase { private string _value = "初始值"; public string Value { get { return _value; } set { Set(ref _value, value, broadcast: true); } } public ICommand BtnCommand { get => new RelayCommand(() => { Value = "属性发生变化了"; }); } }
-
消息处理函数的定义及注册
public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); //使用Messenger注册收到消息后的执行函数 Messenger.Default.Register<PropertyChangedMessage<string>>(this, MsgAction); } private void MsgAction(PropertyChangedMessage<string> pcm) { MessageBox.Show("旧值:" + pcm.NewValue + ",新值:" + pcm.OldValue); } }
-
命令的绑定
<Window ...... DataContext="{Binding Source={StaticResource Locator}, Path=Main}"> <Grid> <Button Content="Messenger测试" Command="{Binding BtnCommand}"/> </Grid> </Window>
IOC
MVVMToolkit框架本身是没有提供内置的API来进行IOC操作的。如果想要在MVVMToolkit中实现IOC,官网推荐的是通过Microsoft.Extensions.DependencyInjection
包来实现(当然,也可以跟MVVMLight章节中一样使用SimpleIoc
)。
一、ServiceCollection
ServiceCollection
为Microsoft.Extensions.DependencyInjection
命名空间下的类,使用前需要通过Nuget进行库的安装。
通过ServiceCollection
实现了IServiceProvider
接口,在使用ServiceCollection
实现IOC的过程中主要用到以下成员:
AddSingleton<TService>()/AddSingleton<TIService,TImplService>()
:IServiceProvider
对象的扩展方法,向IOC容器对象中注册TService
类型,为单例模式,即每次从IOC中获取TService
类型对象都是同一个对象。AddTransient<TService>()/AddTransient<TIService,TImplService>()
:IServiceProvider
对象的扩展方法,向IOC容器对象中注册TService
类型,为瞬时模式,即每次从IOC中获取TService
类型对象都是一个新的对象。AddScoped<TService>()/AddScoped<TIService,TImplService>()
:scope也叫做作用域模式,ioc会在在一个作用域内创建唯一一个实例。比如,在后端api程序中,处理一次htpp请求的过程相当于一个Scoped,这个方法也是在实际工作中较为常用的。ServiceProvider BuildServiceProvider()
:IServiceProvider
对象的扩展方法,获取IOC容器对象。T? GetService<T>()
:IServiceProvider
对象的扩展方法,从容器中获取指定的服务对象。
二、IOC的实现
安装库
1、配置服务
在App.cs中进行服务配置
public sealed partial class App : Application
{
public App()
{
Services = ConfigureServices();
this.InitializeComponent();
}
public new static App Current => (App)Application.Current;
public IServiceProvider Services { get; }
private static IServiceProvider ConfigureServices()
{
var services = new ServiceCollection();
services.AddSingleton<IFilesService, FilesService>();
services.AddTransient<MainViewModel>();
return services.BuildServiceProvider();
}
}
该 Services
属性在启动时初始化,此外,由于在App
类中定义了 Current
属性,因此应用程序中的其他视图轻松访问到 Services
,如下所示:
IFilesService filesService = App.Current.Services.GetService<IFilesService>();
2、构造函数注入
与其他IOC容器一样,只要在IOC容器中进行过注册,那么IOC容器将会管理他们之间的依赖关系。例如上文中IOC容器中注册了MainViewModel和IFilesService。如果MainViewModel的构造函数中需要传入IFilesService对象作为参数,那么只需要直接从IOC容器中获取MainViewModel对象即可,其依赖关系IOC会帮助我们管理的。
public class MainViewModel
{
public MainViewModel(IFilesService filesService)
{
//......
}
}
DataContext的赋值示例
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = App.Current.Services.GetService<MainViewModel>();
}
}