C#之MVVM篇快速入门

MVVM(Model-View-ViewModel)模型-视图-视图模型

  • 参考网址: https://www.cnblogs.com/hsiang/p/15579839.htmlhttps://www.cnblogs.com/mingupupu/p/18218027
- 【模型】指的是后端传递的数据
- 【视图】指的是前端页面
- 【视图模型】mvvm模式的核心,它是连接view和model的桥梁
	- 将【模型】转化成【视图】,即将后端传递的数据转化成所看到的页面。实现的方式是:数据绑定。
	- 将【视图】转化成【模型】,即将所看到的页面转化成后端的数据。实现的方式是:DOM 事件监听。
	- 这两个方向都实现的,我们称之为数据的双向绑定。
  • 新建WPF应用(.FW)程序
  • 安装MvvmLight插件
- 项目名称右键-->管理NuGet程序包-->搜索MvvmLight-->安装 # v5.4.1.1
  • 注意事项,此插件安装以后,需要删除两处代码
// app.xaml
......
    <ResourceDictionary>
      <!--注释掉这句(插件自动生成)-->
      <!--<vm:ViewModelLocator x:Key="Locator" d:IsDataSource="True" xmlns:vm="clr-namespace:WpfAppMVVMFWDemo1.ViewModel" />-->
    </ResourceDictionary>
......

- 删除ViewModel目录(插件自动生成)底下的"ViewModelLocator.cs"文件
  • 创建Model目录,新建Student类
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace WpfAppMVVMFWDemo1.Model
{
    public class Student
    {
   
        // 唯一标识
        public int Id { get; set; }


        // 学生姓名
        public string Name { get; set; }


        // 年龄
        public int Age { get; set; }


        // 班级
        public string Classes { get; set; }
    }

}

  • 创建DAL目录,新建LocalDb类
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using WpfAppMVVMFWDemo1.Model;

namespace WpfAppMVVMFWDemo1.DAL
{
    public class LocalDb
    {
        private List<Student> students;

        public LocalDb()
        {
            init();
        }


        // 初始化数据
        private void init()
        {
            students = new List<Student>();
            for (int i = 0; i < 30; i++)
            {
                students.Add(new Student()
                {
                    Id = i,
                    Name = string.Format("学生{0}", i),
                    Age = new Random(i).Next(0, 100),
                    Classes = i % 2 == 0 ? "一班" : "二班"
                });
            }
        }


        // 查询数据
        public List<Student> Query()
        {
            return students;
        }


        // 按名字查询
        public List<Student> QueryByName(string name)
        {
            return students.Where((t) => t.Name.Contains(name)).ToList();//FindAll((t) => t.Name.Contains(name));
        }

        public Student QueryById(int Id)
        {
            var student = students.FirstOrDefault((t) => t.Id == Id);
            if (student != null)
            {
                return new Student()
                {
                    Id = student.Id,
                    Name = student.Name,
                    Age = student.Age,
                    Classes = student.Classes
                };
            }
            return null;
        }



        // 新增学生
        public void AddStudent(Student student)
        {
            if (student != null)
            {
                students.Add(student);
            }
        }


        // 删除学生
        public void DelStudent(int Id)
        {
            var student = students.FirstOrDefault((t) => t.Id == Id); //students.Find((t) => t.Id == Id);
            if (student != null)
            {
                students.Remove(student);
            }

        }
    }


}

  • 主窗体MainWindow内容如下
// xaml
<Window x:Class="WpfAppMVVMFWDemo1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfAppMVVMFWDemo1"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="80" />
            <!--"*"表示会占据剩余所有可用空间-->
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <StackPanel Orientation="Horizontal" Margin="5" VerticalAlignment="Center">
            <TextBlock Text="姓名" Margin="10" />
            <TextBox x:Name="sname" Text="{Binding Search}" Width="120" Margin="10" Padding="5" />
            <Button x:Name="btnQuery" Command="{Binding QueryCommand}" Content="查询" Margin="10" Padding="5" Width="80" />
            <Button x:Name="btnReset" Content="重置" Margin="10" Padding="5" Width="80" Command="{Binding ResetCommand}" />
            <Button x:Name="btnAdd" Content="创建" Margin="10" Padding="5" Width="80"  Command="{Binding AddCommand}" />
        </StackPanel>

        <DataGrid Grid.Row="1" x:Name="dgInfo" ItemsSource="{Binding GridModelList}" AutoGenerateColumns="False" CanUserAddRows="False" CanUserSortColumns="False" Margin="10" >
            <DataGrid.Columns>
                
                <DataGridTextColumn Header="Id" Width="100"  Binding="{Binding Id}" />
                <DataGridTextColumn Header="姓名" Width="100"  Binding="{Binding Name}" />
                <DataGridTextColumn Header="年龄" Width="100"  Binding="{Binding Age}" />
                <DataGridTextColumn Header="班级" Width="100"  Binding="{Binding Classes}" />

                <DataGridTemplateColumn>
                    <DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center">
                                <Button x:Name="edit" Content="编辑" Width="60" Margin="3" Height="25" CommandParameter="{Binding Id}" Command="{Binding DataContext.EditCommand, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=DataGrid}}" />
                                <Button x:Name="delete" Content="删除" Width="60" Margin="3" Height="25"  CommandParameter="{Binding Id}" Command="{Binding DataContext.DeleteCommand, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=DataGrid}}" />
                            </StackPanel>
                        </DataTemplate>
                    </DataGridTemplateColumn.CellTemplate>
                </DataGridTemplateColumn>
                
            </DataGrid.Columns>
            
        </DataGrid>

    </Grid>
</Window>

// cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using WpfAppMVVMFWDemo1.ViewModel;

namespace WpfAppMVVMFWDemo1
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            MainViewModel viewModel = new MainViewModel();
            viewModel.Query();
            this.DataContext = viewModel;
        }
    }
}

  • 创建Views目录,新建窗体StudentWindow
// xaml

<Window x:Class="WpfAppMVVMFWDemo1.Views.StudentWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfAppMVVMFWDemo1.Views"
        mc:Ignorable="d"
        Title="StudentWindow" Height="450" Width="800">


    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="60"></RowDefinition>
            <RowDefinition></RowDefinition>
            <RowDefinition Height="60"></RowDefinition>
        </Grid.RowDefinitions>
        <TextBlock FontSize="30" Margin="10">修改学生信息</TextBlock>
        <StackPanel Grid.Row="1" Orientation="Vertical">
            <TextBlock FontSize="20" Margin="10" Padding="5">姓名</TextBlock>
            <TextBox x:Name="txtName" FontSize="20"  Padding="5" Text="{Binding Model.Name}"></TextBox>
            <TextBlock FontSize="20" Margin="10"  Padding="5">年龄</TextBlock>
            <TextBox x:Name="txtAge" FontSize="20"  Padding="5" Text="{Binding Model.Age}"></TextBox>
            <TextBlock FontSize="20" Margin="10"  Padding="5">班级</TextBlock>
            <TextBox x:Name="txtClasses" FontSize="20"  Padding="5" Text="{Binding Model.Classes}"></TextBox>
        </StackPanel>
        <StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right">
            <Button x:Name="btnSave" Content="保存" Margin="10" FontSize="20" Width="100" Click="btnSave_Click" ></Button>
            <Button x:Name="btnCancel" Content="取消" Margin="10" FontSize="20" Width="100" Click="btnCancel_Click" ></Button>
        </StackPanel>
    </Grid>



</Window>

// cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using WpfAppMVVMFWDemo1.Model;


namespace WpfAppMVVMFWDemo1.Views
{
    /// <summary>
    /// StudentWindow.xaml 的交互逻辑
    /// </summary>
    public partial class StudentWindow : Window
    {
        public StudentWindow(Student student)
        {
            InitializeComponent();
            this.DataContext = new { Model=student };
        }

        private void btnSave_Click(object sender, RoutedEventArgs e)
        {
            this.DialogResult = true;
        }

        private void btnCancel_Click(object sender, RoutedEventArgs e)
        {
            this.DialogResult = false;
        }

    }
}

  • ViewModel.MainViewModel.cs
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows;
using WpfAppMVVMFWDemo1.DAL;
using WpfAppMVVMFWDemo1.Model;
using WpfAppMVVMFWDemo1.Views;

namespace WpfAppMVVMFWDemo1.ViewModel
{
    /// <summary>
    /// This class contains properties that the main View can data bind to.
    /// <para>
    /// Use the <strong>mvvminpc</strong> snippet to add bindable properties to this ViewModel.
    /// </para>
    /// <para>
    /// You can also use Blend to data bind with the tool's support.
    /// </para>
    /// <para>
    /// See http://www.galasoft.ch/mvvm
    /// </para>
    /// </summary>
    public class MainViewModel : ViewModelBase
    {

        private LocalDb localDb;
        private ObservableCollection<Student> gridModelList;
        public ObservableCollection<Student> GridModelList
        {
            get { return gridModelList; }
            set
            {
                gridModelList = value;
                RaisePropertyChanged();
            }
        }

        private string search;

        public string Search
        {
            get { return search; }
            set
            {
                search = value;
                RaisePropertyChanged();
            }
        }




        public MainViewModel()
        {
            localDb = new LocalDb();
            QueryCommand = new RelayCommand(this.Query);
            ResetCommand = new RelayCommand(this.Reset);
            EditCommand = new RelayCommand<int>(this.Edit);
            DeleteCommand = new RelayCommand<int>(this.Delete);
            AddCommand = new RelayCommand(this.Add);
        }

        public RelayCommand QueryCommand { get; set; }
        public RelayCommand ResetCommand { get; set; }
        public RelayCommand<int> EditCommand { get; set; }
        public RelayCommand<int> DeleteCommand { get; set; }
        public RelayCommand AddCommand { get; set; }

        public void Query()
        {
            List<Student> students;
            if (string.IsNullOrEmpty(search))
            {
                students = localDb.Query();
            }
            else
            {
                students = localDb.QueryByName(search);
            }

            GridModelList = new ObservableCollection<Student>();
            if (students != null)
            {
                students.ForEach((t) =>
                {
                    GridModelList.Add(t);
                });
            }
        }

        public void Reset()
        {
            this.Search = string.Empty;
            this.Query();
        }

        public void Edit(int Id)
        {
            var model = localDb.QueryById(Id);
            if (model != null)
            {
                StudentWindow view = new StudentWindow(model);
                var r = view.ShowDialog();
                if (r.Value)
                {
                    var newModel = GridModelList.FirstOrDefault(t => t.Id == model.Id);
                    if (newModel != null)
                    {
                        newModel.Name = model.Name;
                        newModel.Age = model.Age;
                        newModel.Classes = model.Classes;
                    }
                    this.Query();
                }
            }
        }

        public void Delete(int Id)
        {
            var model = localDb.QueryById(Id);
            if (model != null)
            {
                var r = MessageBox.Show($"确定要删除吗【{model.Name}】?", "提示", MessageBoxButton.YesNo);
                if (r == MessageBoxResult.Yes)
                {
                    localDb.DelStudent(Id);
                    this.Query();
                }
            }
        }

        public void Add()
        {
            Student model = new Student();
            StudentWindow view = new StudentWindow(model);
            var r = view.ShowDialog();
            if (r.Value)
            {
                model.Id = GridModelList.Max(t => t.Id) + 1;
                localDb.AddStudent(model);
                this.Query();
            }
        }


    }
}
  • 小结: MVVM具有低耦合,可重用,可测试,独立开发的优点,核心要素就两个
- 属性发生变化时的通知,即可达到数据的实时更新。
- 命令是实现用户与程序之间数据和算法的桥梁。

未命名绘图-第 2 页

  • ViewModel(视图模型)解析
- 包含视图所需的所有数据和命令
	- 数据(绑定): INotifyPropertyChanged接口
	- 命令(绑定): ICommand对象
 public class User
 {
     public string? Name { get; set; }
     public string? Email { get; set; }
 }


 public static class UserManager
 {
     public static ObservableCollection<User> DataBaseUsers = new       ObservableCollection<User>()
     {
         new User() { Name = "小王", Email = "123@qq.com" },
         new User() { Name = "小红", Email = "456@qq.com" },
         new User() { Name = "小五", Email = "789@qq.com" }
     };

     public static ObservableCollection<User> GetUsers()
     {
         return DataBaseUsers;
     }

     public static void AddUser(User user)
     {
         DataBaseUsers.Add(user);
     }
 }

ObservableCollection 详解

基本概念

ObservableCollection 是 .NET 中的一个特殊集合类,位于 System.Collections.ObjectModel 命名空间中。它与普通集合最大的区别在于实现了数据变更通知机制

核心特性

1. 自动通知机制

public class ObservableCollection<T> : Collection<T>, INotifyCollectionChanged, INotifyPropertyChanged
  • 实现了 INotifyCollectionChanged 接口
  • 实现了 INotifyPropertyChanged 接口
  • 当集合内容变化时自动发出通知

2. 主要事件

// 集合内容变化时触发(添加、删除、移动、重置等)
public event NotifyCollectionChangedEventHandler CollectionChanged;

// 属性变化时触发(如Count属性)
public event PropertyChangedEventHandler PropertyChanged;

在示例代码中的作用

public static ObservableCollection<User> DataBaseUsers = new ObservableCollection<User>()
{
    new User() { Name = "小王", Email = "123@qq.com" },
    new User() { Name = "小红", Email = "456@qq.com" },
    new User() { Name = "小五", Email = "789@qq.com" }
};

public static void AddUser(User user)
{
    DataBaseUsers.Add(user); // 这里会触发CollectionChanged事件
}

实际应用场景

1. WPF/XAML 数据绑定

<!-- XAML 中绑定到 ObservableCollection -->
<ListBox ItemsSource="{Binding DataBaseUsers}">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <StackPanel>
                <TextBlock Text="{Binding Name}"/>
                <TextBlock Text="{Binding Email}"/>
            </StackPanel>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

2. MVVM 模式中的典型用法

public class UserViewModel
{
    public ObservableCollection<User> Users { get; set; }
    
    public UserViewModel()
    {
        Users = UserManager.GetUsers();
        // 当UserManager中添加新用户时,UI会自动更新
    }
}

与普通集合的区别

特性 List ObservableCollection
数据变更通知 ❌ 无 ✅ 自动通知
UI自动更新 ❌ 需要手动刷新 ✅ 自动同步
内存占用 较小 稍大(维护事件)
使用场景 纯数据处理 UI数据绑定

触发通知的操作

var users = new ObservableCollection<User>();

// 以下操作都会触发CollectionChanged事件:
users.Add(new User());                    // 添加
users.RemoveAt(0);                        // 删除
users[0] = new User();                    // 替换
users.Move(0, 1);                         // 移动
users.Clear();                            // 清空

事件处理示例

// 监听集合变化
DataBaseUsers.CollectionChanged += (sender, e) =>
{
    switch (e.Action)
    {
        case NotifyCollectionChangedAction.Add:
            Console.WriteLine($"添加了 {e.NewItems.Count} 个用户");
            break;
        case NotifyCollectionChangedAction.Remove:
            Console.WriteLine($"删除了 {e.OldItems.Count} 个用户");
            break;
        case NotifyCollectionChangedAction.Reset:
            Console.WriteLine("集合被重置");
            break;
    }
};

优点总结

  1. 自动化 - UI自动同步,无需手动刷新
  2. 响应式 - 实时响应数据变化
  3. 简化代码 - 减少样板代码
  4. 框架集成 - 与WPF、Xamarin等完美集成

注意事项

  • 线程安全:ObservableCollection不是线程安全的,跨线程操作需要调度
  • 性能:频繁的大批量操作可能影响性能,可使用AddRange扩展方法优化
  • 内存:事件订阅可能导致内存泄漏,注意及时取消订阅

这就是为什么在您的代码中使用 ObservableCollection - 它使得用户界面的数据绑定变得简单而高效!

RelayComand解析

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;

namespace WpfApp2MVVMTestInit.Commands
{
    public class RelayComand: ICommand
    {
        public event EventHandler CanExecuteChanged;
        private Action<object> _Excute { get; set; }
        private Predicate<object> _CanExcute { get; set; }

        public RelayComand(Action<object> ExcuteMethod,Predicate<object> canExcuteMethod)
        {
            _Excute = ExcuteMethod;
            _CanExcute = canExcuteMethod;
        }

        public bool CanExecute(object parameter)
        {
            return _CanExcute(parameter);
        }

        public void Execute(object parameter) {
            _Excute(parameter);
        }
        
           
    }
}

这是一个WPF MVVM模式中常用的RelayCommand(中继命令)实现。让我逐部分解释:

类结构概述

public class RelayComand : ICommand
  • 实现了 ICommand 接口,这是WPF命令系统的核心
  • 用于将UI操作(如按钮点击)绑定到ViewModel中的方法

核心字段

private Action<object> _Excute { get; set; }
private Predicate<object> _CanExcute { get; set; }
  • _Excute: 存储要执行的实际方法(无返回值,接受object参数)
  • _CanExcute: 存储判断命令是否可执行的方法(返回bool,接受object参数)

构造函数

public RelayComand(Action<object> ExcuteMethod, Predicate<object> canExcuteMethod)

接受两个委托参数:

  • 执行逻辑的方法
  • 判断是否可执行的方法

ICommand接口实现

CanExecute 方法

public bool CanExecute(object parameter)
{
    return _CanExcute(parameter);
}
  • 决定命令是否可用(控制UI元素的启用/禁用状态)
  • 返回true时按钮可用,false时按钮变灰

Execute 方法

public void Execute(object parameter) {
    _Excute(parameter);
}
  • 实际执行命令时调用的方法
  • 包含主要的业务逻辑

CanExecuteChanged 事件

public event EventHandler CanExecuteChanged;
  • 当命令的可用状态改变时触发
  • WPF自动监听此事件来更新UI状态

使用示例

// 在ViewModel中
public ICommand SaveCommand { get; private set; }

public ViewModel()
{
    SaveCommand = new RelayComand(
        ExecuteSave,     // 执行方法
        CanExecuteSave   // 判断是否可执行的方法
    );
}

private void ExecuteSave(object parameter)
{
    // 保存逻辑
    MessageBox.Show("数据已保存!");
}

private bool CanExecuteSave(object parameter)
{
    // 只有数据有效时才允许保存
    return !string.IsNullOrEmpty(FileName);
}

XAML中的绑定

<Button Content="保存" 
        Command="{Binding SaveCommand}"
        CommandParameter="一些参数"/>

这个RelayCommand是MVVM模式的关键组件,它解耦了UI和业务逻辑,使代码更易于测试和维护。

各个功能模块解释:

RelayCommand - 命令模板

public class RelayCommand : ICommand
{
    private Action<object> _Excute { get; set; }
    private Predicate<object> _CanExcute { get; set; }
    
    public RelayCommand(Action<object> ExcuteMethod, Predicate<object> CanExcuteMethod)
    {
        _Excute = ExcuteMethod;      // 执行逻辑的委托
        _CanExcute = CanExcuteMethod; // 判断能否执行的委托
    }
}

RelayCommand的作用

  • 提供一个通用的命令实现模板
  • 将具体的执行逻辑"外包"给外部方法
  • 本身不包含任何业务逻辑,只是一个"中间人"

MainViewModel - 具体实现

public class MainViewModel
{
    // 数据源
    public ObservableCollection<User> Users { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
    
    // 命令
    public ICommand AddUserCommand { get; set; }
    
    public MainViewModel()
    {
        // 初始化数据源
        Users = UserManager.GetUsers();
        
        // 创建命令,传入具体的实现方法
        AddUserCommand = new RelayCommand(AddUser, CanAddUser);
    }
    
    // 具体的命令执行逻辑
    private void AddUser(object obj)
    {
        User user = new User() { Name = Name, Email = Email };
        UserManager.AddUser(user);
    }
    
    // 具体的命令可用性判断逻辑
    private bool CanAddUser(object obj) => true;
}

工作流程比喻

可以把这想象成一个餐厅

  • RelayCommand = 服务员

    • 只知道"点菜流程",但不知道具体怎么做菜
    • 负责接收顾客的"命令"
  • MainViewModel = 厨师 + 厨房

    • 知道具体的"做菜方法"(AddUser方法)
    • 拥有所有的"食材"(数据源:Users, Name, Email
  • 顾客点菜服务员接收厨师做菜

完整的数据流向

View (界面)
  ↓ (数据绑定)
ViewModel (MainViewModel)
  ↓ (属性/命令)
RelayCommand (命令模板)
  ↓ (委托调用)
具体业务逻辑 (AddUser方法)
  ↓ (数据操作)
数据源 (Users集合)

总结

你的理解非常准确:

  • RelayCommand 是通用的命令模板/框架
  • MainViewModel 提供具体的命令实现逻辑
  • MainViewModel 管理所有数据源和业务逻辑
  • ✅ 两者通过委托连接起来,实现松耦合

这种设计让代码更加模块化,易于测试和维护!

一个神奇的BUG现象(经典的MVVM数据绑定问题)

  • 我们在MainViewModel作出小小的修改
    • 新增命令并写死数据源
......

namespace WpfApp2AboutMVVMWindowDemo.ViewModel
{
    public class MainViewModel
    {
        public ObservableCollection<User> Users { get; set; }
        public ICommand AddUserCommand { get; set; }
        public ICommand TestCommand { get; set; } // 新增
        public string Name { get; set; }
        public string Email { get; set; }

        public MainViewModel()
        {
            Users = UserManager.GetUsers();
            AddUserCommand = new RelayCommand(AddUser, CanAddUser);

            TestCommand = new RelayCommand(Test, CanTest); // 新增

        }

        private bool CanAddUser(object obj)
        {
            return true;
        }

        private void AddUser(object obj)
        {
            User user = new User();
            user.Name = Name;
            user.Email = Email;
            UserManager.AddUser(user);
        }

        private bool CanTest(object obj) // 新增
        {
            return true;
        }

        private void Test(object obj) // 新增
        {
            Name = "小1";
            Email = "111@qq.com";
        }

    }
}

  • UI新增一个测试按钮
<StackPanel>
    <ToolBar>
        ......
        <Button Content="添加" Command="{Binding AddUserCommand}"></Button>
        <!--新增-->
        <Button Content="测试" Command="{Binding TestCommand}"></Button>
    </ToolBar>
   ......
</StackPanel>
  • 神奇的现象: 当点击测试按钮的时候,表格不会增加新的内容(没任何反应),而当我点击增加按钮的时候,神奇的事情发生了,表格新增的内容,居然是"点击测试按钮"的预期效果(文本框的输入内容没用,没效果)

问题分析

当前代码的关键点:

  1. Test方法只是修改了NameEmail属性的值:
private void Test(object obj)
{
    Name = "小1";
    Email = "111@qq.com";
}
  1. AddUser方法使用当前的NameEmail值创建用户:
private void AddUser(object obj)
{
    User user = new User();
    user.Name = Name;    // 使用当前Name值
    user.Email = Email;  // 使用当前Email值
    UserManager.AddUser(user);
}

现象解释:

  1. 点击测试按钮

    • TestCommand执行,设置Name = "小1", Email = "111@qq.com"
    • 但界面没有更新,因为缺少属性变更通知
  2. 点击添加按钮

    • AddUserCommand执行,读取当前的NameEmail值(就是"小1"和"111@qq.com")
    • 创建新用户并添加到集合中
    • 表格显示新用户,数据确实是测试按钮设置的值

根本原因:缺少INotifyPropertyChanged

你的ViewModel没有实现属性变更通知机制,所以:

  • 界面不知道属性值已改变
  • 文本框仍然显示旧值
  • 但ViewModel内部属性值确实改变了

解决方案

让MainViewModel实现INotifyPropertyChanged接口:

using System.ComponentModel;
using System.Runtime.CompilerServices;

public class MainViewModel : INotifyPropertyChanged
{
    private string _name;
    private string _email;

    public string Name
    {
        get => _name;
        set
        {
            _name = value;
            OnPropertyChanged();
        }
    }

    public string Email
    {
        get => _email;
        set
        {
            _email = value;
            OnPropertyChanged();
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    // 其他代码保持不变...
}

修复后的行为

  1. 点击测试按钮

    • 文本框立即显示"小1"和"111@qq.com"
    • 界面与数据保持同步
  2. 点击添加按钮

    • 使用当前文本框中的值创建用户
    • 行为符合预期

总结

这个"神奇"现象揭示了MVVM中数据绑定属性通知的重要性:

  • 没有INotifyPropertyChanged:界面与数据可能不同步,导致困惑的行为
  • 有INotifyPropertyChanged:界面实时响应数据变化,行为可预测

这就是为什么在MVVM模式中,ViewModel必须实现INotifyPropertyChanged接口!

ICommand

image-20240528093349406

注意: 此时若在ViewModel中修改User模型的数据,并不会触发UI界面的更新,因为User模型没有实现INotifyPropertyChanged接口

 private void Test(object obj)
 {
     Users[0].Name = "小1";
     Users[0].Email = "111@qq.com";
 }

- 点击`测试按钮`,第一行数据没有发生改变
- 解决办法: User模型继承INotifyPropertyChanged接口即可

// User.cs

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace WpfAppMVVMCommunityToolkitMvvm
{
    //public class User
    //{
    //    public string? Name { get; set; }
    //    public string? Email { get; set; }
    //}

    public class User : INotifyPropertyChanged
    {
        private string? _name;
        public string? Name
        {
            get { return _name; }
            set
            {
                if (_name != value)
                {
                    _name = value;
                    OnPropertyChanged(nameof(Name));
                }
            }
        }

        private string? _email;
        public string? Email
        {
            get { return _email; }
            set
            {
                if (_email != value)
                {
                    _email = value;
                    OnPropertyChanged(nameof(Email));
                }
            }
        }

        public event PropertyChangedEventHandler? PropertyChanged;

        protected virtual void OnPropertyChanged(string propertyName)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }

}

- 现在,再次点击"测试按钮",第一行数据就发生变化了

CommunityToolkit.Mvvm库使用

  • 安装踩坑
- 项目必须是WPF项目(非.net framework项目,如果是这个项目,有坑...)
- 安装CommunityToolkit.Mvvm(最新稳定版即可)
  • 修改MainViewModelUser.cs代码,其他不用改,效果和之前一模一样
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;

using WpfAppMVVMCommunityToolkitMvvm.Commands;
using static System.Net.Mime.MediaTypeNames;

namespace WpfAppMVVMCommunityToolkitMvvm.ViewModel
{
   
    public partial class MainViewModel : ObservableObject
    {
        public ObservableCollection<User> Users { get; set; }

        [ObservableProperty]
        private string? name;

        [ObservableProperty]
        private string? email;


        public MainViewModel()
        {
            Users = UserManager.GetUsers();
        }

        [RelayCommand]
        private void Test(object obj)
        {
            Users[0].Name = "小1";
            Users[0].Email = "111@qq.com";
        }

        [RelayCommand]
        private void AddUser(object obj)
        {
            User user = new User();
            user.Name = Name;
            user.Email = Email;
            UserManager.AddUser(user);
        }

    }

}


using CommunityToolkit.Mvvm.ComponentModel;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace WpfAppMVVMCommunityToolkitMvvm
{
    
    public partial class User : ObservableObject
    {
        [ObservableProperty]
        private string? _name;

        [ObservableProperty]
        private string? _email;
    }

}


posted @ 2025-07-09 14:56  清安宁  阅读(266)  评论(0)    收藏  举报