ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

C#+WPF+MVVM 实战教程

C#+WPF+MVVM 实战教程 目录一、项目结构二、MVVM 三层职责三、完整 Demo 代码1. MvvmTodoDemo.csproj2. App.xaml3. App.xaml.cs4. Common/ViewModelBase.cs5. Models/TodoItem.cs6. ViewModels/RelayCommand.cs7. ViewModels/MainViewModel.cs8. MainWindow.xaml9. MainWindow.xaml.cs四、如何运行五、重点理解数据是如何流动的1. 输入任务2. 点击添加按钮3. 勾选任务4. 删除任务六、这个 Demo 中的 MVVM 对应关系七、问题1.为什么要使用ObservableCollection2.为什么Model也实现INotifyPropertyChange八、MVVM中最重要的几个概念1.DataConext2.Binding3. TwoWay Binding4. ICommand5. CanExecute6. RelativeSource项目介绍C# WPF MVVM Todo Demo不依赖 CommunityToolkit.Mvvm 等第三方库自己实现INotifyPropertyChangedICommandRelayCommandObservableCollectionICollectionView 数据过滤MVVM 数据绑定添加任务删除任务完成/取消完成任务按条件筛选任务清空已完成任务数据统计一、项目结构MvvmTodoDemo├── MvvmTodoDemo.csproj├── App.xaml├── App.xaml.cs├── MainWindow.xaml├── MainWindow.xaml.cs├── Models│ └── TodoItem.cs├── ViewModels│ ├── MainViewModel.cs│ └── RelayCommand.cs└── Common└── ViewModelBase.cs二、MVVM 三层职责1. ModelModel 表示业务数据。在本 Demo 中TodoItem它表示一条待办事项包含标题是否完成创建时间2. ViewView 负责界面展示。在本 Demo 中MainWindow.xamlView 中主要写界面布局控件样式数据绑定命令绑定View 尽量不写业务逻辑。3. ViewModelViewModel 是 View 和 Model 之间的桥梁。在本 Demo 中MainViewModel它负责保存任务集合添加任务删除任务修改任务状态过滤任务暴露给 View 绑定的属性暴露给 View 绑定的命令三、完整 Demo 代码1. MvvmTodoDemo.csprojProject SdkMicrosoft.NET.Sdk PropertyGroup !-- 使用 .NET 8如果你使用的是 .NET 6 或 .NET 7可以修改这里 -- OutputTypeWinExe/OutputType TargetFrameworknet8.0-windows/TargetFramework !-- 开启 WPF -- UseWPFtrue/UseWPF !-- 开启可空引用类型检查 -- Nullableenable/Nullable !-- 开启隐式 using -- ImplicitUsingsenable/ImplicitUsings /PropertyGroup /Project2. App.xamlApplication x:ClassMvvmTodoDemo.App xmlnshttp://schemas.microsoft.com/winfx/2006/xaml/presentation xmlns:xhttp://schemas.microsoft.com/winfx/2006/xaml StartupUriMainWindow.xaml !-- Application.Resources 可以放置整个应用程序共享的资源 例如 1. 全局样式 2. 颜色 3. 转换器 4. 控件模板 -- Application.Resources /Application.Resources /Application3. App.xaml.csusing System.Windows; namespace MvvmTodoDemo; /// summary /// App 是 WPF 应用程序的入口类。 /// /summary public partial class App : Application { }4. Common/ViewModelBase.csusing System.ComponentModel; using System.Runtime.CompilerServices; namespace MvvmTodoDemo.Common; /// summary /// ViewModelBase 是所有 ViewModel 的基类。 /// /// 主要作用 /// 统一实现 INotifyPropertyChanged。 /// /// WPF 的数据绑定机制会监听 PropertyChanged 事件。 /// 当 ViewModel 中的属性发生变化时 /// 只要触发 PropertyChanged界面就可以自动刷新。 /// /summary public abstract class ViewModelBase : INotifyPropertyChanged { /// summary /// 当某个属性发生变化时触发。 /// /summary public event PropertyChangedEventHandler? PropertyChanged; /// summary /// 通知界面刷新某个属性。 /// /summary /// param namepropertyName /// 属性名称。 /// /// [CallerMemberName] 的作用是 /// 如果调用时不传参数编译器会自动传入当前属性的名称。 /// /// 例如 /// OnPropertyChanged(); /// /// 等价于 /// OnPropertyChanged(nameof(Name)); /// /param protected void OnPropertyChanged( [CallerMemberName] string? propertyName null) { PropertyChanged?.Invoke( this, new PropertyChangedEventArgs(propertyName)); } /// summary /// 通用的属性设置方法。 /// /// 这个方法可以减少大量重复代码。 /// /summary protected bool SetPropertyT( ref T field, T value, [CallerMemberName] string? propertyName null) { // 如果旧值和新值相同就不需要通知界面。 if (EqualityComparerT.Default.Equals(field, value)) { return false; } field value; // 通知 WPF这个属性已经发生变化。 OnPropertyChanged(propertyName); return true; } }5. Models/TodoItem.csusing MvvmTodoDemo.Common; namespace MvvmTodoDemo.Models; /// summary /// TodoItem 是 Model。 /// /// 它表示一条待办事项。 /// /// Model 主要负责保存业务数据 /// 不应该直接操作界面控件。 /// /summary public class TodoItem : ViewModelBase { private string _title string.Empty; private bool _isCompleted; private DateTime _createdTime; /// summary /// Todo 的标题。 /// /summary public string Title { get _title; set SetProperty(ref _title, value); } /// summary /// 是否已完成。 /// /summary public bool IsCompleted { get _isCompleted; set SetProperty(ref _isCompleted, value); } /// summary /// 创建时间。 /// /summary public DateTime CreatedTime { get _createdTime; set SetProperty(ref _createdTime, value); } }6. ViewModels/RelayCommand.csusing System.Windows.Input; namespace MvvmTodoDemo.ViewModels; /// summary /// RelayCommand 是一个通用命令实现。 /// /// WPF 中的 Button、MenuItem 等控件都可以绑定 ICommand。 /// /// 这样做的好处是 /// /// View 中只负责绑定命令 /// /// Command{Binding AddTodoCommand} /// /// 具体执行什么逻辑由 ViewModel 决定。 /// /// 因此不需要在 MainWindow.xaml.cs 中编写按钮点击事件。 /// /summary public class RelayCommand : ICommand { /// summary /// 命令真正要执行的方法。 /// /summary private readonly Actionobject? _execute; /// summary /// 判断命令当前是否可以执行的方法。 /// /summary private readonly Predicateobject?? _canExecute; /// summary /// 构造函数。 /// /summary /// param nameexecute命令执行逻辑/param /// param namecanExecute判断命令是否可执行/param public RelayCommand( Actionobject? execute, Predicateobject?? canExecute null) { _execute execute ?? throw new ArgumentNullException(nameof(execute)); _canExecute canExecute; } /// summary /// 当命令是否可执行状态发生变化时触发。 /// /// WPF 会根据这个事件重新判断按钮是否应该启用。 /// /summary public event EventHandler? CanExecuteChanged { add CommandManager.RequerySuggested value; remove CommandManager.RequerySuggested - value; } /// summary /// 判断命令是否可以执行。 /// /summary public bool CanExecute(object? parameter) { return _canExecute null || _canExecute(parameter); } /// summary /// 执行命令。 /// /summary public void Execute(object? parameter) { _execute(parameter); } /// summary /// 通知 WPF 重新判断所有命令的 CanExecute 状态。 /// /// 例如 /// 当输入框内容发生变化后 /// 可以调用这个方法让“添加”按钮重新判断是否可用。 /// /summary public void RaiseCanExecuteChanged() { CommandManager.InvalidateRequerySuggested(); } }7. ViewModels/MainViewModel.csusing System.Collections.ObjectModel; using System.Collections.Specialized; using System.ComponentModel; using System.Windows.Data; using MvvmTodoDemo.Common; using MvvmTodoDemo.Models; namespace MvvmTodoDemo.ViewModels; /// summary /// MainViewModel 是主窗口对应的 ViewModel。 /// /// 它负责 /// /// 1. 保存 Todo 数据 /// 2. 提供给界面绑定的属性 /// 3. 提供添加、删除、清空等命令 /// 4. 处理任务筛选 /// 5. 维护统计信息 /// /summary public class MainViewModel : ViewModelBase { private string _newTodoTitle string.Empty; private string _selectedFilter 全部; /// summary /// 保存所有待办事项的集合。 /// /// ObservableCollection 的特点 /// /// 当集合新增、删除元素时 /// 会自动触发 CollectionChanged 事件 /// WPF 可以根据这个事件自动更新界面。 /// /summary private readonly ObservableCollectionTodoItem _allTodos new(); /// summary /// 用于显示在界面上的任务视图。 /// /// ICollectionView 可以在不修改原始集合的情况下 /// 对集合进行 /// /// 1. 筛选 /// 2. 排序 /// 3. 分组 /// /summary public ICollectionView TodosView { get; } /// summary /// 用于绑定 ComboBox 的筛选选项。 /// /summary public IReadOnlyListstring FilterOptions { get; } new Liststring { 全部, 未完成, 已完成 }; /// summary /// 新任务标题。 /// /// TextBox 会绑定到这个属性。 /// /summary public string NewTodoTitle { get _newTodoTitle; set { if (SetProperty(ref _newTodoTitle, value)) { // 输入框内容变化后 // 重新判断“添加任务”按钮是否可以点击。 AddTodoCommand.RaiseCanExecuteChanged(); } } } /// summary /// 当前选择的筛选条件。 /// /summary public string SelectedFilter { get _selectedFilter; set { if (SetProperty(ref _selectedFilter, value)) { // 筛选条件改变后刷新任务视图。 TodosView.Refresh(); } } } /// summary /// 所有任务数量。 /// /summary public int TotalCount _allTodos.Count; /// summary /// 已完成任务数量。 /// /summary public int CompletedCount _allTodos.Count(todo todo.IsCompleted); /// summary /// 未完成任务数量。 /// /summary public int RemainingCount _allTodos.Count(todo !todo.IsCompleted); /// summary /// 添加任务命令。 /// /summary public RelayCommand AddTodoCommand { get; } /// summary /// 删除任务命令。 /// /summary public RelayCommand DeleteTodoCommand { get; } /// summary /// 清空所有已完成任务命令。 /// /summary public RelayCommand ClearCompletedCommand { get; } /// summary /// 构造函数。 /// /summary public MainViewModel() { /* * 将 ObservableCollection 包装成 ICollectionView。 * * View 绑定的不是 _allTodos * 而是 TodosView。 */ TodosView CollectionViewSource.GetDefaultView(_allTodos); // 设置筛选方法。 TodosView.Filter FilterTodo; /* * 监听集合变化。 * * 当集合新增或删除任务时 * 可以更新统计信息和命令状态。 */ _allTodos.CollectionChanged OnTodosCollectionChanged; /* * 创建命令。 * * AddTodoCommand * 执行 AddTodo 方法。 * 只有输入不为空时才允许执行。 * * DeleteTodoCommand * 执行 DeleteTodo 方法。 * 参数必须是 TodoItem。 * * ClearCompletedCommand * 执行 ClearCompleted 方法。 * 只有存在已完成任务时才允许执行。 */ AddTodoCommand new RelayCommand( execute: _ AddTodo(), canExecute: _ !string.IsNullOrWhiteSpace(NewTodoTitle)); DeleteTodoCommand new RelayCommand( execute: parameter DeleteTodo(parameter), canExecute: parameter parameter is TodoItem); ClearCompletedCommand new RelayCommand( execute: _ ClearCompleted(), canExecute: _ CompletedCount 0); // 初始化一些示例数据方便程序启动后直接观察效果。 AddSampleData(); } /// summary /// 添加示例数据。 /// /summary private void AddSampleData() { AddTodoInternal(学习 WPF 数据绑定); AddTodoInternal(理解 ICommand 命令); AddTodoInternal(完成 MVVM Todo Demo); // 将最后一个示例任务设置为已完成。 _allTodos[2].IsCompleted true; } /// summary /// 添加任务。 /// /summary private void AddTodo() { string title NewTodoTitle.Trim(); // 理论上按钮已经通过 CanExecute 做了判断 // 这里仍然再次校验避免空数据进入业务层。 if (string.IsNullOrWhiteSpace(title)) { return; } AddTodoInternal(title); // 添加成功后清空输入框。 NewTodoTitle string.Empty; } /// summary /// 真正添加任务的内部方法。 /// /summary private void AddTodoInternal(string title) { var todo new TodoItem { Title title, IsCompleted false, CreatedTime DateTime.Now }; _allTodos.Add(todo); } /// summary /// 删除任务。 /// /summary /// param nameparameter /// 来自 Button 的 CommandParameter。 /// /param private void DeleteTodo(object? parameter) { if (parameter is not TodoItem todo) { return; } _allTodos.Remove(todo); } /// summary /// 清空已经完成的任务。 /// /summary private void ClearCompleted() { /* * ToList 的作用是创建一个新列表。 * * 不能直接遍历 _allTodos 的同时修改 _allTodos * 否则可能出现集合被修改异常。 */ var completedTodos _allTodos .Where(todo todo.IsCompleted) .ToList(); foreach (var todo in completedTodos) { _allTodos.Remove(todo); } } /// summary /// 任务集合发生变化时执行。 /// /summary private void OnTodosCollectionChanged( object? sender, NotifyCollectionChangedEventArgs e) { /* * 新增任务时监听 TodoItem 的属性变化。 * * 例如 * 当用户勾选 IsCompleted 时 * ViewModel 需要重新计算 CompletedCount。 */ if (e.NewItems ! null) { foreach (TodoItem todo in e.NewItems) { todo.PropertyChanged OnTodoPropertyChanged; } } // 删除任务时取消监听避免不必要的事件引用。 if (e.OldItems ! null) { foreach (TodoItem todo in e.OldItems) { todo.PropertyChanged - OnTodoPropertyChanged; } } // 通知界面刷新统计属性。 OnPropertyChanged(nameof(TotalCount)); OnPropertyChanged(nameof(CompletedCount)); OnPropertyChanged(nameof(RemainingCount)); // 集合变化后刷新筛选视图。 TodosView.Refresh(); // 重新判断清空按钮是否可用。 ClearCompletedCommand.RaiseCanExecuteChanged(); } /// summary /// TodoItem 内部属性发生变化时执行。 /// /summary private void OnTodoPropertyChanged( object? sender, PropertyChangedEventArgs e) { /* * 当 IsCompleted 改变时 * 统计信息也需要重新计算。 */ if (e.PropertyName nameof(TodoItem.IsCompleted)) { OnPropertyChanged(nameof(CompletedCount)); OnPropertyChanged(nameof(RemainingCount)); // 刷新当前筛选结果。 TodosView.Refresh(); // 已完成数量变化后 // 清空已完成任务按钮的状态也可能变化。 ClearCompletedCommand.RaiseCanExecuteChanged(); } } /// summary /// ICollectionView 的筛选方法。 /// /summary private bool FilterTodo(object obj) { if (obj is not TodoItem todo) { return false; } return SelectedFilter switch { 未完成 !todo.IsCompleted, 已完成 todo.IsCompleted, _ true }; } }8. MainWindow.xamlWindow x:ClassMvvmTodoDemo.MainWindow xmlnshttp://schemas.microsoft.com/winfx/2006/xaml/presentation xmlns:xhttp://schemas.microsoft.com/winfx/2006/xaml xmlns:dhttp://schemas.microsoft.com/expression/blend/2008 xmlns:mchttp://schemas.openxmlformats.org/markup-compatibility/2006 mc:Ignorabled TitleMVVM Todo Demo Width850 Height650 MinWidth650 MinHeight500 WindowStartupLocationCenterScreen !-- 将窗口的 DataContext 设置为 MainViewModel。 DataContext 是 WPF 数据绑定的上下文。 设置之后下面这些绑定 NewTodoTitle AddTodoCommand TodosView DeleteTodoCommand 都会去 MainViewModel 中查找。 -- Window.DataContext local:MainViewModel / /Window.DataContext !-- 注意 上面需要使用 local 命名空间 所以在 Window 中添加如下声明 xmlns:localclr-namespace:MvvmTodoDemo -- Window.Resources !-- TodoItem 样式。 当 IsCompleted 为 True 时 1. 文字添加删除线 2. 文字颜色变成灰色 3. 文字透明度降低 这是纯 View 层逻辑 不需要写在 ViewModel 中。 -- Style x:KeyTodoTitleStyle TargetTypeTextBlock Setter PropertyFontSize Value16 / Setter PropertyVerticalAlignment ValueCenter / Style.Triggers DataTrigger Binding{Binding IsCompleted} ValueTrue Setter PropertyTextDecorations ValueStrikethrough / Setter PropertyForeground ValueGray / Setter PropertyOpacity Value0.6 / /DataTrigger /Style.Triggers /Style !-- 通用按钮样式 -- Style TargetTypeButton Setter PropertyMargin Value4 / Setter PropertyPadding Value12,6 / Setter PropertyCursor ValueHand / /Style /Window.Resources !-- 页面整体布局 -- Grid Background#F3F4F6 Grid.RowDefinitions RowDefinition HeightAuto / RowDefinition HeightAuto / RowDefinition HeightAuto / RowDefinition Height* / RowDefinition HeightAuto / /Grid.RowDefinitions !-- 标题区域 -- StackPanel Grid.Row0 Margin24,20,24,10 TextBlock TextMVVM Todo Demo FontSize28 FontWeightBold Foreground#111827 / TextBlock Text使用 C# WPF MVVM 实现的待办事项示例 Margin0,6,0,0 Foreground#6B7280 / /StackPanel !-- 添加任务区域 -- Border Grid.Row1 Margin24,8,24,8 Padding12 BackgroundWhite CornerRadius8 Grid Grid.ColumnDefinitions ColumnDefinition Width* / ColumnDefinition WidthAuto / /Grid.ColumnDefinitions !-- TextBox 绑定到 MainViewModel.NewTodoTitle。 UpdateSourceTriggerPropertyChanged 表示 用户每输入一个字符 就立即把内容同步到 ViewModel。 如果不设置这个属性 默认通常是在 TextBox 失去焦点后才更新数据源。 -- TextBox Grid.Column0 Height36 Padding10,6 VerticalContentAlignmentCenter Text{Binding NewTodoTitle, UpdateSourceTriggerPropertyChanged} ToolTip请输入任务内容 / !-- Button 绑定到 AddTodoCommand。 Button 不需要 Click 事件。 这就是 MVVM 中命令绑定的核心用法。 -- Button Grid.Column1 Height36 Content添加任务 Command{Binding AddTodoCommand} Background#4F46E5 ForegroundWhite / /Grid /Border !-- 工具栏 -- Border Grid.Row2 Margin24,8,24,8 Padding12 BackgroundWhite CornerRadius8 Grid Grid.ColumnDefinitions ColumnDefinition WidthAuto / ColumnDefinition Width160 / ColumnDefinition Width* / ColumnDefinition WidthAuto / /Grid.ColumnDefinitions TextBlock Grid.Column0 Margin0,0,8,0 VerticalAlignmentCenter Text筛选 / !-- ComboBox 的 ItemsSource 绑定到 FilterOptions。 SelectedItem 绑定到 SelectedFilter。 -- ComboBox Grid.Column1 Height32 ItemsSource{Binding FilterOptions} SelectedItem{Binding SelectedFilter} / !-- 占位区域 -- TextBlock Grid.Column2 / !-- 绑定清空已完成任务命令。 -- Button Grid.Column3 Content清空已完成 Command{Binding ClearCompletedCommand} Background#DC2626 ForegroundWhite / /Grid /Border !-- Todo 列表 -- Border Grid.Row3 Margin24,8,24,8 Padding12 BackgroundWhite CornerRadius8 Grid !-- 没有任务时显示提示 -- TextBlock Text当前没有任务 HorizontalAlignmentCenter VerticalAlignmentCenter FontSize18 Foreground#9CA3AF TextBlock.Style Style TargetTypeTextBlock Setter PropertyVisibility ValueCollapsed / !-- 通过数据触发器判断 TodosView 是否为空。 这里为了保持示例简单 即使有筛选条件导致列表为空 也会显示这个提示。 -- /Style /TextBlock.Style /TextBlock !-- ListBox 的 ItemsSource 绑定到 TodosView。 TodosView 是 ICollectionView 因此界面显示的是经过筛选后的数据。 -- ListBox ItemsSource{Binding TodosView} BorderThickness0 BackgroundTransparent ListBox.ItemTemplate DataTemplate Border Margin0,4 Padding10 BorderBrush#E5E7EB BorderThickness1 CornerRadius6 Grid Grid.ColumnDefinitions ColumnDefinition WidthAuto / ColumnDefinition Width* / ColumnDefinition WidthAuto / /Grid.ColumnDefinitions !-- CheckBox 绑定到 TodoItem.IsCompleted。 ModeTwoWay 表示 1. Model 变化界面更新 2. 用户操作界面Model 也更新 这就是双向绑定。 -- CheckBox Grid.Column0 Margin0,0,12,0 VerticalAlignmentCenter IsChecked{Binding IsCompleted, ModeTwoWay} / !-- Todo 标题 -- TextBlock Grid.Column1 Text{Binding Title} Style{StaticResource TodoTitleStyle} / !-- 创建时间和删除按钮 -- StackPanel Grid.Column2 OrientationHorizontal TextBlock Margin0,0,10,0 VerticalAlignmentCenter Foreground#9CA3AF Text{Binding CreatedTime, StringFormat{}{0:yyyy-MM-dd HH:mm}} / !-- Command 绑定到父级 MainViewModel。 当前 DataTemplate 的 DataContext 是 TodoItem 所以不能直接写 Command{Binding DeleteTodoCommand} 因为 TodoItem 中没有 DeleteTodoCommand。 这里使用 RelativeSource 找到外层 Window 的 DataContext 也就是 MainViewModel。 -- Button Content删除 Command{Binding DataContext.DeleteTodoCommand, RelativeSource{RelativeSource AncestorTypeWindow}} CommandParameter{Binding} Background#FEE2E2 Foreground#B91C1C / /StackPanel /Grid /Border /DataTemplate /ListBox.ItemTemplate /ListBox /Grid /Border !-- 底部统计区域 -- Border Grid.Row4 Margin24,8,24,20 Padding12 BackgroundWhite CornerRadius8 StackPanel OrientationHorizontal !-- 这些数字绑定到 MainViewModel 中的计算属性。 当 ViewModel 调用 OnPropertyChanged 后 界面会自动刷新。 -- TextBlock Foreground#374151 Run Text全部 / Run Text{Binding TotalCount} FontWeightBold / /TextBlock TextBlock Margin20,0,0,0 Foreground#2563EB Run Text未完成 / Run Text{Binding RemainingCount} FontWeightBold / /TextBlock TextBlock Margin20,0,0,0 Foreground#16A34A Run Text已完成 / Run Text{Binding CompletedCount} FontWeightBold / /TextBlock /StackPanel /Border /Grid /Window9. MainWindow.xaml.csusing System.Windows; namespace MvvmTodoDemo; /// summary /// MainWindow 是 View。 /// /// 在这个 MVVM Demo 中 /// 这里不写按钮点击事件 /// 也不直接操作控件。 /// /// 所有业务逻辑都放在 MainViewModel 中。 /// /summary public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); /* * DataContext 也可以在这里设置 DataContext new MainViewModel(); * 但本 Demo 选择直接在 XAML 中设置 DataContext * 方便观察 XAML 数据绑定。 */ } }四、如何运行使用命令行dotnet new wpf -n MvvmTodoDemocd MvvmTodoDemodotnet run五、重点理解数据是如何流动的1. 输入任务用户在界面输入学习 WPF由于 XAML 中存在Text{Binding NewTodoTitle,UpdateSourceTriggerPropertyChanged}数据会流向TextBox.Text↓MainViewModel.NewTodoTitle2. 点击添加按钮Command{Binding AddTodoCommand}点击按钮后执行AddTodoCommand↓MainViewModel.AddTodo()↓_allTodos.Add(todo)↓ObservableCollection.CollectionChanged↓ListBox 自动显示新任务3. 勾选任务CheckBox 绑定IsChecked{Binding IsCompleted,ModeTwoWay}用户勾选 CheckBox 后CheckBox.IsChecked↓TodoItem.IsCompleted↓PropertyChanged↓MainViewModel 重新计算统计数据↓界面刷新4. 删除任务删除按钮绑定Command{Binding DataContext.DeleteTodoCommand,RelativeSource{RelativeSource AncestorTypeWindow}}同时把当前任务传给命令CommandParameter{Binding}执行过程删除按钮↓DeleteTodoCommand↓DeleteTodo(todo)↓_allTodos.Remove(todo)↓ListBox 自动移除对应任务六、这个 Demo 中的 MVVM 对应关系┌─────────────────────────────────────┐ │ View │ │ MainWindow.xaml │ │ │ │ TextBox、Button、CheckBox、ListBox │ └─────────────────┬───────────────────┘ │ 数据绑定 / 命令绑定 ▼ ┌─────────────────────────────────────┐ │ ViewModel │ │ MainViewModel │ │ │ │ NewTodoTitle │ │ SelectedFilter │ │ AddTodoCommand │ │ DeleteTodoCommand │ │ ClearCompletedCommand │ └─────────────────┬───────────────────┘ │ 操作和管理 ▼ ┌─────────────────────────────────────┐ │ Model │ │ TodoItem │ │ │ │ Title │ │ IsCompleted │ │ CreatedTime │ └─────────────────────────────────────┘七、问题1.为什么要使用ObservableCollection答为了方便集合发生变化时WPF界面能够知道然后进行自动的更新最新的数据。如果使用普通集合ListTodoItem即使代码中添加了新任务todos.Add(todo)WPF 界面也不一定知道集合发生了变化。而使用ObservableCollectionTodoItem集合发生添加、删除时会自动触发CollectionChangedWPF 监听这个事件之后就可以自动更新 ListBox2.为什么Model也实现INotifyPropertyChange很多初学者只让 ViewModel 实现 INotifyPropertyChanged但 Model 中的数据变化也可能需要通知 View例如IsChecked{Binding IsCompleted,ModeTwoWay}当TodoItem.IsCompleted发生变化时CheckBox 修改了 IsCompletedWindow.DataContextlocal:MainViewModel //Window.DataContextTodoItem 触发 PropertyChangedViewModel 重新计算统计数据界面刷新“已完成”和“未完成”的数量因此Model 如果需要参与 WPF 数据绑定也通常会实现INotifyPropertyChanged八、MVVM中最重要的几个概念1.DataConextWindow.DataContextlocal:MainViewModel //Window.DataContextDataContext是数据绑定的查找起点。例如Text{Binding NewTodoTitle}WPF 会在当前 DataContext也就是 MainViewModel 中查找public string NewTodoTitle2.BindingText{Binding NewTodoTitle}表示控件属性和 ViewModel 属性建立绑定关系。3. TwoWay BindingIsChecked{Binding IsCompleted, ModeTwoWay}表示数据可以双向流动ViewModel → ViewView → ViewModel适合使用在TextBoxCheckBoxComboBoxSliderDatePicker4. ICommand按钮不直接绑定事件而是绑定命令Command{Binding AddTodoCommand}好处是业务逻辑在 ViewModel 中View 中没有大量 Click 事件命令可以控制按钮是否可用更容易进行单元测5. CanExecutecanExecute: _ !string.IsNullOrWhiteSpace(NewTodoTitle)只有当输入框中存在有效内容时添加按钮才可以点击。6. RelativeSource在 ListBox.ItemTemplate 中DataContext{Binding}当前数据上下文已经变成了单个TodoItem但删除命令在MainViewModel因此需要通过RelativeSource{RelativeSource AncestorTypeWindow}找到外层 Window然后访问 Window 的 DataContextDataContext.DeleteTodoCommand
返回列表