C# WPF 客户端编程入门
MVVM
全称:Model - View - ViewModel,这种架构广泛应用于各种UI框架,如主流Web前端框架、WPF。
-
Model:模型层,负责处理数据业务逻辑以及和服务器端的交互
-
View:视图层,负责将数据模型转化为UI展示出来,可以简单理解为HTML页面
-
ViewModel:视图模型层,负责View和Model之间的沟通,用来连接Model和View
View层和Model层并没有直接联系,而是通过ViewModel层进行交互。 ViewModel层通过双向数据绑定将View层和Model层连接了起来,使得View层和Model层的同步工作完全是自动的。
数据绑定
数据绑定是MVVM框架的核心特性之一。
数据绑定将View和ViewModel的数据同步连接,使得它们保持同步。当ViewModel中的数据发生变化时,数据绑定会自动更新View中绑定到这些数据的部分,反之亦然。
我理解MVVM就是MVC模式的一个变种,或者说升级。相比于MVC模式的框架,MVVM的框架提供了更多方便的机制和工具,让应用开发变得更加快捷。使用MVVM模式的框架,可以将UI和代码分离,更容易保持代码简洁、清晰、易维护。
WPF中应用MVVM模式
WPF框架相当于只给了个MVVM的基座,有点简陋了。要再装个CommunityToolkit.Mvvm库才称得上能用。
使用CommunityToolkit.Mvvm之后,各个抽象层对应的文件如下:
-
模型层:实体类以及相关的数据库访问对象等。
-
视图层:xaml,Visual Studio提供创建入口,以及设计器。
-
视图模型层:自己写个类,继承ObservableObject,需要监听数据变化的一些属性,加上
[ObservableProperty]
注解,变量命名注意要遵守C#编码规范,因为框架要根据你的变量名来生成代码,不规范可能有坑。
CommunityToolkit.Mvvm 通过Source Generator(compile time metaprogramming)实现了很多方便的写法。
比如一个ViewModel类可以这么写:
public partial class ReadRegistersViewModel: ObservableObject
{
[ObservableProperty]
private string _slaveAddress = "0";
[ObservableProperty]
private string _startRegisterAddress = "0";
[ObservableProperty]
private string _registerCount = "1";
[RelayCommand]
public Task ButtonClicked()
{
return Task.CompletedTask;
}
}
这个ViewModel可以直接在xaml中进行绑定,ViewModel中再不需要其他代码。
xaml中,通过如下步骤进行绑定:
-
引入命名空间
即你需要绑定哪个ViewModel,就引入它所在的命名空间。
xaml文件中,第一行就是整个控件类的配置。xaml最终是要编译为C#类的,编译之前,它还有一个关联的C#类,这个类位于 xxx.xaml.cs文件中。这个关联的C#类被声明为partial,意思就是不完整,需要由多个part合并才能得到一个完整的类。它的内容有一部分是从xaml中生成的,现在还没有,另一部分就是xaml.cs文件中的。
x:Class
:对应的C#类,.xaml.cs文件中的那个类。一般不用动。但如果你把那个类改名了或者改了命名空间,这里也要改,不然就报错。xmlns:local
:声明一个叫local
的命名空间,对应TestApp
这个C#中的命名空间。xmlns:vm
:声明一个叫vm
的命名空间,对应TestApp.ViewModel
这个C#中的命名空间。从visual studio创建控件之后,默认会帮你把写上xmlns:local写上,但是如果你要引用其他命名空间的控件,或者ViewModel在其他命名空间,你得自己在这里加。vm是我自己取的名字,ViewModel的缩写。名字无所谓,不要跟其他的冲突就行。
<UserControl x:Class="TestApp.View.ReadRegistersView" ... xmlns:local="clr-namespace:TestApp" xmlns:vm="clr-namespace:TestApp.ViewModel" ...>
-
设置
DataContext
<UserControl x:Class="TestApp.View.ReadRegistersView" ...> <UserControl.DataContext> <vm:ReadRegistersViewModel /> </UserControl.DataContext> </UserControl>
-
绑定数据
public partial class ReadRegistersViewModel: ObservableObject { [ObservableProperty] private string _slaveAddress = "0"; }
比如这个TextBox,将它的内容与ViewModel中的变量进行绑定:
<TextBox Text="{Binding SlaveAddress}"/>
-
绑定函数
public partial class MainWindowViewModel: ObservableObject { [RelayCommand] public Task Connect() { return Task.Run(() => {}); } }
这里绑定按钮行为,按钮按下去之后执行Connect函数:
<Button Content="connect" Command="{Binding ConnectCommand}" />
ViewModel中的函数叫Connect,xaml中需要加个Command后缀。这是
[RelayCommand]
这行注解的作用,它会自动为Connect这个函数创建一个ICommand对象,对象名为函数名+Command。 -
绑定事件
比如ComboBox的SelectionChanged事件,无法像Button的Command一样直接绑定ViewModel中的ICommand对象,但是官方有提供解决方案。
首先安装
Microsoft.Xaml.Behaviors.Wpf
:Install-Package Microsoft.Xaml.Behaviors.Wpf
xaml中添加命名空间:
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
此时可以按照如下方式,在xaml中为事件绑定ViewModel中的ICommand对象:
<ComboBox ...> <i:Interaction.Triggers> <i:EventTrigger EventName="SelectionChanged"> <i:InvokeCommandAction Command="{Binding SelectionChangedCommand}" /> </i:EventTrigger> </i:Interaction.Triggers> </ComboBox>
ViewModel之间通信
通信使用EventAggregator
,基于发布订阅模式,允许在不同的 ViewModel 之间传播事件。由 CommunityToolkit.Mvvm.Messaging
提供。
可以直接使用 WeakReferenceMessenger.Default
这个全局静态单例。
也可以配置依赖注入,通过构造函数注入到ViewModel中:
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
var services = new ServiceCollection();
services.AddSingleton<IMessenger>(WeakReferenceMessenger.Default);
services.AddTransient<ViewModelA>();
services.AddTransient<ViewModelB>();
// 配置依赖注入容器...
}
}
我这里是直接使用 WeakReferenceMessenger.Default
:
定义消息类
// 泛型参数是响应数据类型
public class RequestMsg : RequestMessage<string>
{
public RequestMsg() : base() { }
}
发送 & 获取响应
var response = _messenger.Send(new RequestMsg());
// 此处data类型应为string,与RequestMsg的泛型参数一致
var data = response.Response;
接收 & 发送响应
public partial class SomeViewModel : ObservableObject
{
private readonly IMessenger _messenger;
public SomeViewModel() : base()
{
_messenger = WeakReferenceMessenger.Default;
_messenger.Register<RequestMsg>(this, WaitForRequest);
}
private void WaitForRequest(object sender, RequestMsg msg)
{
msg.Reply("Reply");
}
}