# WPF-MVVM-Learning **Repository Path**: tendercool/wpf-mvvm-learning ## Basic Information - **Project Name**: WPF-MVVM-Learning - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-08-05 - **Last Updated**: 2026-08-17 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # WPF 与 MVVM 学习路线 本文用于在已有 C# 基础上学习 WPF 和 MVVM。 最终项目以 `DailyNotes` 为主线,将控制台版本逐步迁移为桌面应用。 ## 学习原则 不要一开始就追求完整架构,推荐按照以下顺序学习: 1. 先用 XAML 和代码后置完成界面; 2. 掌握控件、布局和事件; 3. 使用数据绑定代替直接操作控件; 4. 手动实现属性通知和 Command; 5. 将代码后置版重构为 MVVM; 6. 增加 Repository、Service、JSON 和异步; 7. 最后再使用 MVVM Toolkit 或 Prism 减少模板代码。 推荐演进路线: 代码后置 → Data Binding → INotifyPropertyChanged → ObservableCollection → ICommand → ViewModel → Service → Repository → JSON 异步存储 → 依赖注入 --- # 学习目标 完成后应掌握: - WPF 项目结构和启动过程 - XAML 基础语法 - 常用控件和布局容器 - 事件和代码后置 - DataContext - Data Binding - Binding Mode - DataTemplate - INotifyPropertyChanged - ObservableCollection - SelectedItem - ICommand - MVVM 分层 - 搜索、过滤和排序 - ICollectionView - Converter 和 DataTrigger - Style 和 ResourceDictionary - 输入验证 - Repository 和 Service - JSON 异步加载与保存 - UI 线程和 Dispatcher - 对话框服务 - 依赖注入 - ViewModel 单元测试 --- # 第 0 组:WPF 项目准备 ## 0.1 创建 WPF 项目 目标:能够创建并运行 WPF 程序。 项目文件应包含: WinExe net9.0-windows true enable enable 需要理解: - `WinExe` 和 `Exe` 的区别; - `net9.0` 和 `net9.0-windows` 的区别; - `UseWPF` 的作用; - WPF 为什么只能运行在 Windows 上。 ## 0.2 理解项目结构 重点文件: App.xaml App.xaml.cs MainWindow.xaml MainWindow.xaml.cs DailyNotes.csproj 需要理解: - `App.xaml` 是应用入口和应用级资源; - `StartupUri` 决定初始窗口; - XAML 用于描述界面; - `.xaml.cs` 是代码后置; - `x:Class` 如何关联 C# 类; - `InitializeComponent()` 的作用。 --- # 第 1 组:XAML 和基础控件 ## 1. TextBlock、TextBox 和 Button 需求: - 使用 `TextBlock` 显示标签; - 使用 `TextBox` 输入标题; - 使用 `Button` 提交; - 点击按钮后用 `MessageBox` 显示输入内容。 需要复习: - `x:Name` - 控件属性 - `Click` 事件 - `RoutedEventArgs` - XAML 与代码后置的关系 这一阶段允许使用代码后置。 ## 2. 登录界面 需求: - 输入用户名和密码; - 用户名为 `admin`、密码为 `123456` 时提示成功; - 否则提示失败; - 输入为空时给出提示。 建议控件: TextBlock TextBox PasswordBox Button 需要理解: - `PasswordBox.Password` - 输入验证 - `MessageBox` - 为什么 PasswordBox 的绑定比较特殊 --- # 第 2 组:WPF 布局 ## 3. StackPanel 需求: - 垂直排列标签和输入框; - 水平排列保存和取消按钮; - 调整 Margin 和对齐方式。 需要复习: - `Orientation` - `Margin` - `Padding` - `HorizontalAlignment` - `VerticalAlignment` ## 4. Grid 需求: - 创建两列表单; - 第一列放标签; - 第二列放输入框; - 创建标题、内容和按钮三行。 需要理解: - `Grid.Row` - `Grid.Column` - `RowDefinition` - `ColumnDefinition` - 固定尺寸 - `Auto` - `*` ## 5. DailyNotes 主界面布局 目标界面: ┌──────────────────────────────────────┐ │ 搜索框、新增、删除、保存 │ ├──────────────┬───────────────────────┤ │ Note 列表 │ Note 编辑区域 │ │ │ 标题 │ │ │ 内容 │ └──────────────┴───────────────────────┘ 建议使用: - Grid - Border - GridSplitter - StackPanel - DockPanel --- # 第 3 组:集合控件 ## 6. ListBox 显示字符串 需求: - 创建字符串集合; - 绑定或赋值给 ListBox; - 点击列表项后显示选中内容。 需要复习: - `ItemsSource` - `SelectedItem` - `SelectionChanged` ## 7. ListBox 显示 Note Note 包含: - Id - Title - Content - CreatedAt 需求: - ListBox 显示 Note 集合; - 列表只显示标题; - 选中 Note 后显示详情。 先使用: DisplayMemberPath="Title" 需要复习: - 对象集合 - `DisplayMemberPath` - `SelectedItem` - 可空引用类型 ## 8. DataTemplate 将 `DisplayMemberPath` 替换为 `ItemTemplate`。 列表项显示: - 标题 - 创建时间 - 内容摘要 需要理解: - `DataTemplate` - 模板中的 DataContext - `{Binding Title}` - 一个数据对象如何生成一项 UI --- # 第 4 组:Data Binding ## 9. DataContext 创建 MainViewModel,并设置到窗口的 DataContext。 需求: - ViewModel 提供 Title 属性; - TextBox 绑定 Title; - 不再通过控件名称读取标题。 需要理解: - Binding 从 DataContext 查找属性; - DataContext 会沿视觉树或逻辑树继承; - View 不需要知道 ViewModel 属性的具体存储方式。 ## 10. Binding Mode 分别练习: - OneWay - TwoWay - OneTime - OneWayToSource 同时测试: - `UpdateSourceTrigger=LostFocus` - `UpdateSourceTrigger=PropertyChanged` 需要回答: - TextBox 内容何时写回 ViewModel? - ViewModel 属性变化后 UI 是否更新? - Binding 写错时在哪里查看错误? --- # 第 5 组:属性变化通知 ## 11. INotifyPropertyChanged 手动实现一次属性通知。 需要理解: - 普通 C# 属性改变后,UI 为什么不一定更新; - `PropertyChanged` 事件的作用; - `CallerMemberName` 的作用; - setter 中为什么要比较新旧值。 ## 12. ViewModelBase 将通知逻辑提取为基类: ViewModelBase ├── PropertyChanged ├── OnPropertyChanged └── SetProperty 需要复习: - 泛型 - `ref` - 基类复用 - `EqualityComparer` --- # 第 6 组:ObservableCollection ## 13. List 与 ObservableCollection 先使用 `List`,再改成 `ObservableCollection`。 测试: - 新增 Note 后列表是否刷新; - 删除 Note 后列表是否刷新; - 修改 Note.Title 后列表项是否刷新。 需要区分: INotifyPropertyChanged → 对象属性发生改变 ObservableCollection → 集合新增、删除或重置 注意:ObservableCollection 不会自动替集合元素实现属性通知。 ## 14. SelectedNote ViewModel 提供可空的 SelectedNote 属性。 需求: - 左侧 ListBox 双向绑定 SelectedItem; - 右侧显示 SelectedNote 的标题和内容; - 没有选择时禁用编辑区; - 删除后清空选择。 --- # 第 7 组:ICommand ## 15. 从 Click 迁移到 Command 演进顺序: Button Click → ICommand → RelayCommand → ViewModel Command 需要手动实现一次: - Execute - CanExecute - CanExecuteChanged - RaiseCanExecuteChanged 需要理解: - Command 和 Click 事件的区别; - 为什么 CanExecute 为 false 时按钮会禁用; - 哪些状态变化后需要重新计算 CanExecute。 ## 16. AddNoteCommand 新增命令负责: - 验证标题; - 创建 Note; - 添加到 ObservableCollection; - 自动选中新 Note; - 清空新增输入。 ## 17. DeleteNoteCommand 删除命令负责: - 未选择 Note 时不可执行; - 删除前确认; - 调用 Service 或 Repository; - 删除后清空 SelectedNote; - 删除失败时显示错误。 学习阶段可以暂时直接使用 MessageBox,之后再抽象对话框服务。 --- # 第 8 组:MVVM 分层 这一组要把前面散落在 `MainWindow.xaml.cs` 中的逻辑拆出来,但不要一次性追求“完美架构”。推荐顺序仍然和 C# 基础 README 一样: 1. 先让界面能跑; 2. 再把重复逻辑抽成方法; 3. 再把页面状态抽成 ViewModel; 4. 最后才加入 Service、Repository、JSON 和异步。 ## 18. Model Model 负责: - 数据结构; - 领域状态; - 必要的领域规则。 Model 不应包含: - Button - TextBox - MessageBox - 页面导航 - Window 引用 ## 19. View View 负责: - XAML - 布局 - Style - DataTemplate - 纯 UI 行为 View 不应负责: - ID 生成 - JSON 序列化 - 搜索算法 - 删除业务规则 ## 20. ViewModel ViewModel 负责: - 页面状态 - 页面数据 - SelectedNote - SearchText - IsBusy - ErrorMessage - Commands - 调用 Service ViewModel 不应直接操作: - TextBox - ListBox - 控件名称 - 具体文件格式 ## 21. Service 和 Repository Service 负责: - 业务验证 - ID 生成 - 新增和删除规则 - 调用 Repository Repository 负责: - 数据存取 - 内存集合 - 文件 - 数据库 - 数据源相关查询 典型调用关系: View → MainViewModel → NoteService → INoteRepository → JsonNoteRepository --- # 关键代码段:手写 MVVM 基础设施 这些代码段不是要求一开始全部写完,而是在对应阶段逐步引入。学习重点是理解它们分别解决什么问题。 ## Note Model ```csharp public class Note { public int Id { get; set; } public string Title { get; set; } = ""; public string Content { get; set; } = ""; public DateTime CreatedAt { get; set; } = DateTime.Now; public string Summary { get { if (Content.Length <= 40) { return Content; } return Content[..40] + "..."; } } } ``` Model 只描述数据和少量领域规则,不引用 WPF 控件。 ## ViewModelBase ```csharp using System.ComponentModel; using System.Runtime.CompilerServices; public abstract class ViewModelBase : INotifyPropertyChanged { public event PropertyChangedEventHandler? PropertyChanged; protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } protected bool SetProperty( ref T field, T value, [CallerMemberName] string? propertyName = null) { if (EqualityComparer.Default.Equals(field, value)) { return false; } field = value; OnPropertyChanged(propertyName); return true; } } ``` 它解决的是“ViewModel 属性变了以后,Binding 如何知道要刷新 UI”。 ## RelayCommand ```csharp using System.Windows.Input; public class RelayCommand : ICommand { private readonly Action _execute; private readonly Func? _canExecute; public RelayCommand(Action execute, Func? canExecute = null) { _execute = execute; _canExecute = canExecute; } public event EventHandler? CanExecuteChanged; public bool CanExecute(object? parameter) { return _canExecute?.Invoke() ?? true; } public void Execute(object? parameter) { _execute(); } public void RaiseCanExecuteChanged() { CanExecuteChanged?.Invoke(this, EventArgs.Empty); } } ``` 它解决的是“按钮点击后如何调用 ViewModel,并且按钮什么时候应该禁用”。 ## MainViewModel 最小版本 ```csharp using System.Collections.ObjectModel; using System.Windows.Input; public class MainViewModel : ViewModelBase { private string _newTitle = ""; private string _newContent = ""; private Note? _selectedNote; private int _nextId = 1; public MainViewModel() { AddNoteCommand = new RelayCommand(AddNote, CanAddNote); DeleteNoteCommand = new RelayCommand(DeleteNote, CanDeleteNote); } public ObservableCollection Notes { get; } = new(); public string NewTitle { get => _newTitle; set { if (SetProperty(ref _newTitle, value)) { ((RelayCommand)AddNoteCommand).RaiseCanExecuteChanged(); } } } public string NewContent { get => _newContent; set => SetProperty(ref _newContent, value); } public Note? SelectedNote { get => _selectedNote; set { if (SetProperty(ref _selectedNote, value)) { ((RelayCommand)DeleteNoteCommand).RaiseCanExecuteChanged(); } } } public ICommand AddNoteCommand { get; } public ICommand DeleteNoteCommand { get; } private bool CanAddNote() { return !string.IsNullOrWhiteSpace(NewTitle); } private void AddNote() { Note note = new() { Id = _nextId++, Title = NewTitle.Trim(), Content = NewContent.Trim(), CreatedAt = DateTime.Now }; Notes.Add(note); SelectedNote = note; NewTitle = ""; NewContent = ""; } private bool CanDeleteNote() { return SelectedNote is not null; } private void DeleteNote() { if (SelectedNote is null) { return; } Notes.Remove(SelectedNote); SelectedNote = null; } } ``` 这是 MVVM 的第一版,不需要 Repository,也不需要 DI。先用它验证 Binding、ObservableCollection、SelectedItem 和 Command。 ## MainWindow.xaml 绑定示例 ```xml