C# WPF 客户端编程入门

MVVM

全称:Model - View - ViewModel,这种架构广泛应用于各种UI框架,如主流Web前端框架、WPF。

  • Model:模型层,负责处理数据业务逻辑以及和服务器端的交互

  • View:视图层,负责将数据模型转化为UI展示出来,可以简单理解为HTML页面

  • ViewModel:视图模型层,负责View和Model之间的沟通,用来连接Model和View

MVVM 模式

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中,通过如下步骤进行绑定:

  1. 引入命名空间

    即你需要绑定哪个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"
                 ...>
    
  2. 设置 DataContext

    <UserControl x:Class="TestApp.View.ReadRegistersView" ...>
    
        <UserControl.DataContext>
            <vm:ReadRegistersViewModel />
        </UserControl.DataContext>
    
    </UserControl>
    
  3. 绑定数据

    public partial class ReadRegistersViewModel: ObservableObject
    {
        [ObservableProperty]
        private string _slaveAddress = "0";
    }
    

    比如这个TextBox,将它的内容与ViewModel中的变量进行绑定:

    <TextBox Text="{Binding SlaveAddress}"/>
    
  4. 绑定函数

    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。

  5. 绑定事件

    比如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");
    }
}
posted @ 2024-10-28 20:58  烟酒忆长安  阅读(21)  评论(0编辑  收藏  举报