本篇定位 前面 11 篇分析了源码,这篇用一个完整的便签应用 串联所有知识点。
项目结构 1 2 3 4 5 6 7 8 9 10 11 12 13 14 NoteApp/ ├── Models/ │ └── Note.cs # 数据模型(POCO) ├── ViewModels/ │ ├── MainViewModel.cs # 主 ViewModel │ ├── NoteDetailViewModel.cs # 详情 ViewModel │ └── NoteListViewModel.cs # 列表 ViewModel ├── Views/ │ ├── MainWindow.xaml │ ├── NoteDetailView.xaml │ └── NoteListView.xaml ├── Services/ │ └── INoteService.cs # 数据服务 └── App.xaml.cs # DI 配置
1. Model — 数据模型 1 2 3 4 5 6 7 8 9 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 bool IsCompleted { get ; set ; } }
纯 POCO,没有通知机制。需要用 ViewModel 包装。
2. Service — 数据服务 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 public interface INoteService { Task<List<Note>> GetAllAsync(); Task<Note?> GetByIdAsync(int id); Task SaveAsync (Note note ) ; Task DeleteAsync (int id ) ; } public class NoteService : INoteService { private List<Note> _notes = new (); private int _nextId = 1 ; public Task<List<Note>> GetAllAsync() => Task.FromResult(_notes.ToList()); public Task<Note?> GetByIdAsync(int id) => Task.FromResult(_notes.FirstOrDefault(n => n.Id == id)); public Task SaveAsync (Note note ) { if (note.Id == 0 ) { note.Id = _nextId++; _notes.Add(note); } else { var index = _notes.FindIndex(n => n.Id == note.Id); if (index >= 0 ) _notes[index] = note; } return Task.CompletedTask; } public Task DeleteAsync (int id ) { _notes.RemoveAll(n => n.Id == id); return Task.CompletedTask; } }
3. MainViewModel — 页面导航 + 消息通信 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 public partial class MainViewModel : ObservableRecipient { private readonly INoteService _noteService; [ObservableProperty ] private object ? _currentViewModel; public MainViewModel (INoteService noteService, NoteListViewModel noteList, NoteDetailViewModel noteDetail ) { _noteService = noteService; IsActive = true ; } protected override void OnActivated () { Messenger.Register<NavigateToDetailMessage>(this , (r, m) => { var vm = (MainViewModel)r; var detail = App.Services.GetRequiredService<NoteDetailViewModel>(); detail.LoadNote(m.NoteId); vm.CurrentViewModel = detail; }); Messenger.Register<NavigateToListMessage>(this , (r, m) => { var vm = (MainViewModel)r; vm.CurrentViewModel = App.Services.GetRequiredService<NoteListViewModel>(); }); CurrentViewModel = App.Services.GetRequiredService<NoteListViewModel>(); } } public sealed record NavigateToDetailMessage (int NoteId ) ;public sealed record NavigateToListMessage ;
4. NoteListViewModel — 列表页(命令 + 异步 + 消息) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 public partial class NoteListViewModel : ObservableObject { private readonly INoteService _noteService; [ObservableProperty ] private List<Note> _notes = new (); [ObservableProperty ] private bool _isLoading; [ObservableProperty ] private string _statusText = "就绪" ; [RelayCommand ] private async Task LoadNotesAsync () { IsLoading = true ; StatusText = "加载中..." ; try { Notes = await _noteService.GetAllAsync(); StatusText = $"共 {Notes.Count} 条便签" ; } catch (Exception ex) { StatusText = $"加载失败: {ex.Message} " ; } finally { IsLoading = false ; } } [RelayCommand ] private void AddNote () { WeakReferenceMessenger.Default.Send( new NavigateToDetailMessage(0 )); } [RelayCommand ] private void EditNote (Note? note ) { if (note is null ) return ; WeakReferenceMessenger.Default.Send( new NavigateToDetailMessage(note.Id)); } [RelayCommand(CanExecute = nameof(CanDeleteNote)) ] private async Task DeleteNoteAsync (Note? note ) { if (note is null ) return ; await _noteService.DeleteAsync(note.Id); await LoadNotesAsync(); } private bool CanDeleteNote (Note? note ) => note is not null ; public NoteListViewModel (INoteService noteService ) { _noteService = noteService; } }
XAML:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 <UserControl > <Grid > <Grid.RowDefinitions > <RowDefinition Height ="Auto" /> <RowDefinition Height ="*" /> <RowDefinition Height ="Auto" /> </Grid.RowDefinitions > <StackPanel Grid.Row ="0" Orientation ="Horizontal" > <Button Content ="刷新" Command ="{Binding LoadNotesCommand}" /> <Button Content ="新建" Command ="{Binding AddNoteCommand}" /> </StackPanel > <ListView Grid.Row ="1" ItemsSource ="{Binding Notes}" SelectedItem ="{Binding SelectedNote}" > <ListView.ItemTemplate > <DataTemplate > <StackPanel > <TextBlock Text ="{Binding Title}" FontWeight ="Bold" /> <TextBlock Text ="{Binding CreatedAt, StringFormat={}{0:yyyy-MM-dd}}" /> <StackPanel Orientation ="Horizontal" HorizontalAlignment ="Right" > <Button Content ="编辑" Command ="{Binding DataContext.EditNoteCommand, RelativeSource={RelativeSource AncestorType=UserControl}}" CommandParameter ="{Binding}" /> <Button Content ="删除" Command ="{Binding DataContext.DeleteNoteCommand, RelativeSource={RelativeSource AncestorType=UserControl}}" CommandParameter ="{Binding}" /> </StackPanel > </StackPanel > </DataTemplate > </ListView.ItemTemplate > </ListView > <StatusBar Grid.Row ="2" > <TextBlock Text ="{Binding StatusText}" /> </StatusBar > </Grid > </UserControl >
5. NoteDetailViewModel — 详情页(验证 + 异步保存) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 public partial class NoteDetailViewModel : ObservableValidator { private readonly INoteService _noteService; private int _noteId; [ObservableProperty ] [Required(ErrorMessage = "标题不能为空" ) ] [MinLength(1) ] [NotifyDataErrorInfo ] private string _title = "" ; [ObservableProperty ] private string _content = "" ; [ObservableProperty ] private bool _isSaving; public async void LoadNote (int noteId ) { _noteId = noteId; if (noteId == 0 ) return ; var note = await _noteService.GetByIdAsync(noteId); if (note is null ) return ; Title = note.Title; Content = note.Content; } [RelayCommand ] private async Task SaveAsync () { ValidateAllProperties(); if (HasErrors) return ; IsSaving = true ; try { var note = new Note { Id = _noteId, Title = Title, Content = Content, CreatedAt = DateTime.Now }; await _noteService.SaveAsync(note); WeakReferenceMessenger.Default.Send(new NavigateToListMessage()); } finally { IsSaving = false ; } } [RelayCommand ] private void Cancel () { WeakReferenceMessenger.Default.Send(new NavigateToListMessage()); } public NoteDetailViewModel (INoteService noteService ) { _noteService = noteService; } }
XAML:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 <UserControl > <Grid > <Grid.RowDefinitions > <RowDefinition Height ="Auto" /> <RowDefinition Height ="Auto" /> <RowDefinition Height ="*" /> <RowDefinition Height ="Auto" /> </Grid.RowDefinitions > <StackPanel Grid.Row ="0" > <Label > 标题</Label > <TextBox Text ="{Binding Title, UpdateSourceTrigger=PropertyChanged, ValidatesOnNotifyDataErrors=True}" /> </StackPanel > <StackPanel Grid.Row ="2" > <Label > 内容</Label > <TextBox Text ="{Binding Content}" AcceptsReturn ="True" MinHeight ="200" /> </StackPanel > <StackPanel Grid.Row ="3" Orientation ="Horizontal" > <Button Content ="保存" Command ="{Binding SaveCommand}" /> <Button Content ="取消" Command ="{Binding CancelCommand}" /> </StackPanel > <Grid Grid.RowSpan ="4" Background ="#80000000" Visibility ="{Binding IsSaving, Converter={StaticResource BoolToVis}}" > <TextBlock Text ="保存中..." Foreground ="White" VerticalAlignment ="Center" HorizontalAlignment ="Center" /> </Grid > </Grid > </UserControl >
6. App.xaml.cs — 依赖注入配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public partial class App : Application { public static IServiceProvider Services { get ; private set ; } = null !; protected override void OnStartup (StartupEventArgs e ) { var services = new ServiceCollection(); services.AddSingleton<INoteService, NoteService>(); services.AddTransient<NoteListViewModel>(); services.AddTransient<NoteDetailViewModel>(); services.AddTransient<MainViewModel>(); services.AddSingleton<MainWindow>(); Services = services.BuildServiceProvider(); var mainWindow = Services.GetRequiredService<MainWindow>(); mainWindow.DataContext = Services.GetRequiredService<MainViewModel>(); mainWindow.Show(); } }
7. 运行流程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 应用启动 → MainViewModel 构造(注入 INoteService) → IsActive = true → OnActivated() → 注册消息 → CurrentViewModel = NoteListViewModel → 用户点击"新建" → NoteListViewModel.AddNote() → Send(NavigateToDetailMessage(0)) → MainViewModel 收到,创建 NoteDetailViewModel → LoadNote(0) → 清空表单 → 用户输入标题、内容 → NoteDetailViewModel 自动验证 → 标题为空时显示验证错误 → 用户点击"保存" → ValidateAllProperties() → HasErrors 为 false → 保存到 NoteService → Send(NavigateToListMessage) → 返回列表 → NoteListViewModel 自动刷新
8. 用时序图看消息流 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 NoteListVM MainVM NoteDetailVM │ │ │ ├─Send(NavigateToDetail)──►│ │ │ ├─new NoteDetailVM │ │ │─CurrentVM = detail─────►├─LoadNote(0) │ │ │ │ │ 用户编辑... │ │ │ │ │ 用户点击保存 │ │ │ ├─ValidateAllProperties │ │ ├─SaveAsync │ │ ├─Send(NavigateToList)──► │◄─────────────────────────┤ │ │ 收到 NavigateToList │ │ │ 重新加载列表 │ │
下一篇预告:社区工具包 MVVM 单元测试实战 — 如何测试 ViewModel