从零手写WPF MVVM:核心三件套与五个典型坑
2026/9/15 23:19:04 网站建设 项目流程

平时带新人,我一般会给他们扔一句话:前面六篇你学完,你只是会用WPF;第七篇你学完MVVM,你才算是能写WPF。这句话听起来有点标题党,但实际带过项目的人都会有同感——前面学的XAML布局、控件、绑定、样式,本质上都是在教你“怎么把界面画出来”,而MVVM教的是“怎么把程序写下去”。这篇是新手村系列的最后一篇,我会把MVVM里最容易让新人劝退的几个点掰开揉碎讲清楚,包括INotifyPropertyChanged到底在干什么、Command和Click事件有什么本质区别、DataContext是怎么把View和ViewModel粘起来的,以及我见过的新手在MVVM里最常翻车的五个细节。你可以把它当成一次“点灯式”的复盘,学完之后,C#那头你差不多就能正式脱离新手村了。

1. 为什么新手村最后一关是MVVM:先理解它解决的是哪种痛

老实说,MVVM并不是一个“必须会,不会就写不了WPF”的东西。你完全可以照着事件驱动的老路子,把按钮的Click、TextBox的TextChanged、ListBox的SelectionChanged全部在Code-behind里写一遍,程序照样能跑,而且跑的还很快。那为什么几乎所有WPF岗位面试都要问MVVM?因为你不写MVVM,项目活不过第一轮需求变更。

1.1 事件驱动式开发的甜蜜与失控

新手阶段写WPF,最舒服的姿势是这样的:窗口上拖一个按钮,双击进去写private void Button_Click,然后从控件取值、做计算、把结果塞回别的控件。两三年前我自己带的一个小工具,一开始就三个按钮、两个输入框,Code-behind代码不过一百行,那叫一个清爽。

等到功能加到十几个按钮、四五个弹窗、好几组联动输入,事情就开始不对了。你会发现按钮的Click和TextBox的事件散落在窗口代码文件里的各个角落,A控件改了个值,B控件要跟着刷新,你不得不在A的事件里写一句b.Text = xxx;反过来B的变更又影响了C的显示,你又在B的事件里写一句c.Visibility = ...。三四个控件之间来回耦合,代码还是能跑的,但是每一次改需求,你都要把整个事件网络在脑子里重放一遍,改完上一个联动,下一个联动又炸了。那种感觉,就像自己给自己织了一张蜘蛛网,最后自己也被粘在了网上。

事件驱动开发的失控期来得多快,取决于界面复杂度增长的速度。做了两三年上位机或工具类软件的朋友,应该都见过那种一万多行的MainWindow.xaml.cs。打开文件之后想定位一个功能逻辑,只能靠搜索框,搜到之后还得小心翼翼地在密密麻麻的事件方法里挪动,生怕动错一行引发连锁反应。

1.2 MVVM的匹配逻辑:界面、状态、行为的三角关系

MVVM的思路说白了很朴素:把“界面长什么样”和“界面上发生了什么”彻底拆开。界面长什么样,由XAML告诉你;界面上的数据状态是什么,由ViewModel告诉你;界面上用户操作了某个东西之后程序该怎么反应,由Command告诉你。View只负责显示和收集输入,剩下的逻辑全部移到ViewModel里。

这个三角关系里,最核心的并不是什么高深技术,而是一句边界声明:View里不写逻辑,ViewModel里不碰控件。这句话新手往往难以接受,因为很多人会问:“如果不碰控件,那我怎么拿到TextBox里输入的文字?怎么改ListBox的选中项?”答案是让绑定机制去替你拿、替你做。你的ViewModel只关心“我有一个字符串属性叫UserName”,至于这个字符串是被TextBox显示出来了还是被ComboBox选出来的,ViewModel不需要知道。绑定就是那条看不见的输送带,它把控件上的输入搬进属性,把属性里的变化搬回控件。

一旦接受了这个设定,整个程序的逻辑就变成了可测试的纯C#代码。你不需要启动窗口、不需要点击按钮,就能通过直接给ViewModel的属性赋值、调用命令方法,验证业务逻辑对不对。这一点对后期维护的价值无可估量——代码里最贵的永远不是写出来的那一瞬间,而是半年后改需求的那一天。

1.3 新手常犯的心理误区:不是“多了一个类”,而是“换了一种组织代码的思维”

我见过很多新人学MVVM时最纠结的一件事:为什么要为这么简单的功能写这么多类?一个登录窗口,要写一个ViewModel类、一个登录命令类、再来一个继承INotifyPropertyChanged的基类,这一通操作下来,显示一个用户名的代码量比原来翻了三倍。这个困惑很正常,但我想说的是——MVVM的第一份代码必然要比事件驱动啰嗦,因为你在为后续的每一次改动省时间。换句话说,MVVM这种写法不是“减少代码量”的,它是“减少耦合度”的。

我当时带的人里,有一个很典型的转变过程。他一开始写MVVM怎么都不顺手,每写一个窗口都觉得在绕远路。后来项目做到第三次需求变更,他的ViewModel几乎没怎么大改,只是加了两个属性和一个命令,窗口代码完全没有动,他才意识到这套思维方式的威力:“原来我上一次花时间搭的这个结构,是在为这一次改需求买单。”所以说到底,MVVM不是让你一开始写得快,而是让你三个月后改得动。

2. MVVM三件套:INotifyPropertyChanged、Command与DataContext的正确打开方式

MVVM看起来概念很多,但你真正要用起来的核心就三个东西:属性通知、命令、数据上下文。把这三位理解透,其它什么Messenger、依赖注入、框架之类的东西都是后话。这一节我先不去写一个完整项目,先把这三个零件各自的功能边界说清楚,顺便纠正一些我在新手代码里经常看到的概念性偏差。

2.1 让属性会喊话:INotifyPropertyChanged的真正意义

很多教程管INotifyPropertyChanged叫“属性通知”,这个叫法没问题,但新手容易误解成“属性一有变化就会自动通知界面”。其实它没有自动的魔法。这个接口的完整协议是:你的属性在值改变时,主动去调用PropertyChanged事件,告诉外界“我这个属性变了,值已经更新了,你们谁要刷新就抓紧刷新”。你不调用这个事件,界面就永远不知道属性变了。

把这句话翻译成容易理解的场景:ViewModel里有一个属性叫Progress,后台线程把进度从0算到了80。如果这个属性只是普通属性,界面上的进度条永远停在0,因为进度条只会在你主动通知的时候去读新值。所以我们必须写一段样板代码:

private int _progress; public int Progress { get => _progress; set { if (_progress == value) return; _progress = value; OnPropertyChanged(nameof(Progress)); } }

这里有两行容易被新手省略的细节。第一行if (_progress == value) return是防止无效赋值触发多余通知,性能小事,主要能避免某些场景下陷入无意义的循环刷新。第二行OnPropertyChanged(nameof(Progress))是真正的关键,它告诉绑定系统:你该重新拉取这个属性了。我见过有人把通知事件名写错、写漏,常见的就是nameof用成了字符串硬编码,改属性名时忘了改字符串,界面数据死活不刷新的问题一查一个准。后面我会专门讲这个坑。

2.2 命令不是Click事件的马甲:ICommand与CanExecute

按钮的点击,在MVVM里不是靠Click事件,而是靠命令。命令的本质是一个对象,它封装了“这个操作能不能执行”和“这个操作怎么执行”两个逻辑。ICommand接口正好就是这两个问题的指针:

public interface ICommand { event EventHandler CanExecuteChanged; bool CanExecute(object parameter); void Execute(object parameter); }

CanExecute决定按钮可不可点,Execute决定点了之后干什么。这个设计最大的好处是,按钮的可用状态不再是你在代码里btnSave.IsEnabled = false这样一句句手动设置的,而是由ViewModel根据当前数据状态实时算出来的。比如用户没填用户名,保存按钮就自动置灰;填了才亮。这个“自动”是WPF绑定系统替你调用了CanExecute之后的结果。

很多新手会写一个假的命令——也就是命令方法里只调了一下普通逻辑,CanExecute永远返回true,然后把按钮的Command属性绑上去。这当然可以工作,但它绕过了命令最值钱的半壁江山。你等于还是在用Click事件的思维写命令,只不过换了个壳。真正的Command用法会在后面的例子里详细展开。

2.3 DataContext:把两个世界粘在一起的那桶胶水

DataContext这个概念,是新手阶段最容易懵的地方之一。它的作用简单粗暴:给XAML中的{Binding}提供一个默认的数据来源。你在XAML里写{Binding UserName},系统会沿着控件树往上找DataContext,找到的那个对象的UserName属性就是绑定的目标。这个“沿着控件树往上找”的特性很重要,因为子控件默认继承父级的DataContext。所以你只需要在Window层设置一次DataContext,整个窗口内的所有绑定就都有数据源了。

我个人调试的时候,喜欢在脑子里把DataContext想成一块“挂牌”。你把ViewModel这个牌子挂到Window上,Window下面的所有子控件都自动认这个牌子;你在某个Grid上又挂了自己的牌子,那Grid内部的控件就优先认Grid上这块牌子。这个思维模型基本能解释绝大多数绑定诡异问题——一旦绑定没数据显示,就先问自己:这个地方往上数,最近的那块牌子是它想要的数据吗?

2.4 一张表说清Model、View、ViewModel各自该干与不该干的事

MVVM分层新手容易走向两个极端,要么所有东西都塞进ViewModel,把ViewModel变成一个新的“Code-behind”;要么一个界面一个Model,把Model和ViewModel搅成一锅粥。为了把边界说透,我做了一张我培训时最常用的职责表:

层级该做的事不该做的事
View布局控件、定义样式、绑定属性与命令、处理纯粹的界面动画写业务逻辑、操作数据库、直接访问其他窗口的控件
ViewModel暴露界面所需的数据属性、定义命令、协调Model层数据与界面状态的转换引用任何View类型、操作具体控件(如TextBox.Text = xxx)、直接弹出消息窗
Model定义业务实体的数据结构、数据的存取规则、业务状态的核心算法引用WPF相关类型、包含包含布尔值怎么显示成颜色的界面状态逻辑

把这张表记住之后,写代码时每次写完一行的归属就能大致判断出来。Model不碰WPF类型是一个容易被忽视但非常重要的规定——一旦Model里引用了System.Windows.Visibility或者Brush,这个Model就再也无法脱离UI层单独做单元测试了。ViewModel虽然可以不引用具体控件,但对于转换本身还是需要用到WPF类型(比如把bool转成Visibility)。真要做到极致解耦还会引入ValueConverter来帮你做显示转换,不过新手阶段先把主体边界守住即可,不必一开始就把体系搭得非常重。

3. 手写一个最简MVVM:从空白窗口到可运行的双向同步

说再多概念都不如撸一个小项目来得通透。这一节我们什么框架都不用,纯手工从零搭一个最简单的MVVM例子:一个待办事项输入框,一个“添加”按钮,下面一个列表显示所有待办。这个例子麻雀虽小,但属性通知、命令、CanExecute、集合绑定全都覆盖到了,你跑通这一遍,后面套任何框架都轻松。

3.1 新建项目时如果发现WPF模板不见了,怎么处理

进实操第一步就可能卡人:好多人装了VS2022之后新建项目,发现C#分类下根本没有WPF应用程序模板。这不是你的VS坏了,一般是安装VS的时候没勾选“使用.NET 桌面开发”工作负载。处理办法有两种,第一种是打开Visual Studio Installer,点你已安装版本右侧的“修改”,在“工作负荷”里勾上.NET 桌面开发,然后点右下角修改,等它装完重启VS,模板就回来了。第二种图省事的话,可以直接新建一个控制台项目,手动把UseWPF打开改成true,加上MainWindow.xaml几个文件,也能跑,但没必要,还是建议走正常模板安装的路子。这个话题也在很多搜索引擎的热搜里挂着,顺带提一句省得有人卡在第一步。

3.2 项目中ViewModel基类与命令类的落地代码

MVVM里有个不成文的约定:ViewModel基类(通常叫ViewModelBase)和命令类(通常叫RelayCommandDelegateCommand)是每个项目都要往工程里带的“基础设施”。你完全不用自己从零发明,网上大把现成的实现,但我的建议是,新手先亲手抄一遍,别直接装NuGet包,只有亲手写过一遍才知道那几行代码到底在干什么。

先看基类:

using System.ComponentModel; using System.Runtime.CompilerServices; public class ViewModelBase : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; protected void OnPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } protected bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null) { if (Equals(storage, value)) return false; storage = value; OnPropertyChanged(propertyName); return true; } }

这里有两个地方很容易被忽略。第一个是[CallerMemberName],编译器会自动把调用方法的属性名作为字符串穿进来,这样你写SetProperty(ref _userName, value)的时候就不用手写属性名字符串,以后重命名属性的时候也不会留一个过期的字符串引用。第二个是SetProperty返回的bool值——不要小看它,它让你可以在属性setter里知道这次的赋值到底有没有产生变化,从而做进一步的联动判断。

然后是命令类:

using System; using System.Windows.Input; public class RelayCommand : ICommand { private readonly Action<object> _execute; private readonly Predicate<object> _canExecute; public RelayCommand(Action<object> execute, Predicate<object> canExecute = null) { _execute = execute ?? throw new ArgumentNullException(nameof(execute)); _canExecute = canExecute; } public bool CanExecute(object parameter) => _canExecute == null || _canExecute(parameter); public void Execute(object parameter) => _execute(parameter); public event EventHandler CanExecuteChanged { add => CommandManager.RequerySuggested += value; remove => CommandManager.RequerySuggested -= value; } }

这个版本是全网流传最广的经典实现,后面我会专门聊CanExecuteChangedCommandManager.RequerySuggested之间的关系,因为这块是新手在MVVM里最容易想当然的隐藏坑。现在你先把这个类抄进项目里,当作工具箱中的扳手备着。

3.3 业务ViewModel:把三件套串起来的核心逻辑

这次的业务场景是待办列表,那Model层我们就不单独再建类了,直接用一个字符串集合存数据就行。真正的主角是ViewModel。我在写这个类的时候,故意把逻辑写得稍微“胖一点”,这样更能展示MVVM的日常操作形态,但又不会胖到拿Model当摆设。

using System.Collections.ObjectModel; using System.Windows.Input; public class TodoViewModel : ViewModelBase { private string _newTodoText; public string NewTodoText { get => _newTodoText; set => SetProperty(ref _newTodoText, value); } public ObservableCollection<string> Todos { get; } = new ObservableCollection<string>(); public ICommand AddTodoCommand { get; } public TodoViewModel() { AddTodoCommand = new RelayCommand( execute: _ => AddTodo(), canExecute: _ => !string.IsNullOrWhiteSpace(NewTodoText)); } private void AddTodo() { Todos.Add(NewTodoText.Trim()); NewTodoText = string.Empty; } }

我特别想提醒的是这个类里的一个细节:AddTodoCommandcanExecute引用了NewTodoText属性,但NewTodoText变化时WPF默认并不会主动重新查询所有命令的CanExecute,那为什么“输入框中没字时按钮置灰、一输入文字按钮就亮”还是能生效呢?答案藏在上一节的RelayCommand实现里:CanExecuteChanged事件嫁接给了全局的CommandManager.RequerySuggested,而WPF会周期性地(通常在界面交互、焦点变化、输入发生等时机)广播这个事件,要求所有命令重新查一次自己的CanExecute。所以你不需要手动写任何代码,按钮状态就会跟着输入框内容刷新。这个机制既是好消息也是坏消息,后面第4章的第三个坑我详细展开说。

3.4 XAML端绑定:写绑定之前先问自己三个问题

ViewModel写完,接下来让View认这个ViewModel。在Window上挂DataContext有几种方式,我推荐新手最先掌握的是在构造函数里赋值,写起来最直观:

public MainWindow() { InitializeComponent(); DataContext = new TodoViewModel(); }

这是一种顺手但不算最“优雅”的挂法,等以后用了MVVM框架,一般会通过ViewModelLocator或者依赖注入在更高层的地方完成绑定,但新手阶段先从这个学起,理解成本最低。接下来是XAML里的三个控件:

<StackPanel Margin="20"> <TextBox Text="{Binding NewTodoText, UpdateSourceTrigger=PropertyChanged}" /> <Button Content="添加" Command="{Binding AddTodoCommand}" Margin="0,10,0,0" Padding="10,5" /> <ItemsControl ItemsSource="{Binding Todos}" Margin="0,10,0,0" /> </StackPanel>

写这个XAML之前,我建议每个绑定都先问自己三个问题:绑定源是什么?绑定路径找得到吗?源属性变化时界面需不需要主动刷新?以第一行Text="{Binding NewTodoText, UpdateSourceTrigger=PropertyChanged}"为例:绑定源就是DataContext(也就是TodoViewModel),路径就是NewTodoText,刷新时机则是每次按键立即把输入框内容写回源属性。如果不写UpdateSourceTrigger=PropertyChanged,TextBox默认失焦时才回写,那“输入一个字立即判断按钮能否可用”的效果就会延迟到失焦才生效,看起来像按钮状态坏了。

ItemsControlItemsSource绑定也一样,Todos是一个ObservableCollection<T>,这个集合有个特殊本事:它在添加、删除元素时会主动通知UI重新拉取列表内容,这正是WPF里列表绑定的默认选择。新手有个很常见的错误是用List<T>去代替ObservableCollection<T>,那界面是永远不会因为你往里Add新元素而自动多出一行的。这个知识点在这个例子里第一次被真正用到,我建议你跑起来之后,再故意把ObservableCollection换成List试一遍看效果,一次就能记住。

4. 新手最容易在MVVM里翻车的五个细节

代码跑通了只是第一步。我在过去几年带人用WPF的过程中,发现有一批错误几乎是所有初学者都逃不掉的。有的错误是界面数据空白,有的错误是按钮永远亮不起来,还有的错误是改了一个属性导致几十个地方崩盘。我下面把这五个最典型的坑一次性列全,每个都给出症状、根因和修复手段,你以后遇到可以直接对号入座。

4.1 DataContext写错位置:内容变空白时的第一排查点

界面绑定不上、数据一片空白的场景,大概能排进WPF新手问题Top 3。排查路径其实很有规律:先看输出窗口(Output),WPF运行时会输出类似BindingExpression path error: 'UserName' property not found的警告。这类警告出现后,基本可以锁定是DataContext的问题。常见情况有三个:一是你压根没赋DataContext,二是赋错了对象(比如把窗口本身赋给了DataContext,然后绑定了一个ViewModel上的属性名,窗口上自然找不到),三是在子控件上挂错了DataContext,导致上面的控件沿着控件树找数据源时找到了一个不对的东西。

有一种比较隐蔽的情况是:你在某个Grid上设置了DataContext,然后里面有个子窗口或者弹窗是独立创建的,它的DataContext并不会自动继承父窗口。举个例子,你写了一个ChildWindow,想在它上面显示TodoViewModel里的同一个属性,但你忘了给它赋值DataContext,那这个子窗口里的所有绑定都是空的。这种情况在开发中比想象中频繁得多,尤其是用ShowDialog弹窗的时候。

4.2 PropertyChanged的字符串手滑:小改动引来连锁反应

接着第2章的坑往深了说。我见过一个真实的事故:一个人把ViewModel里的属性从UserName改成DisplayName,所有XAML里的绑定都改过来了,结果就漏了setter里那行OnPropertyChanged(nameof(UserName))没改。程序编译不报错、运行不报错,就是界面上的用户名再也不刷新了。调试了一下午,最后发现是通知字符串和属性名对不上。

这种坑最好的解法就是永远不要手写属性名字符串,全部用nameof表达式,或者像我前面写的SetProperty配合[CallerMemberName]自动生成。一旦你把所有通知都改成编译期检查的写法,这种“手滑改名”问题就绝迹了。如果你接手了别人的老代码,里面大量硬编码字符串,我的建议是别急着全重构,项目稳定为先,但以后自己新写的代码一定要用安全写法。

4.3 CanExecute不刷新:界面按钮置灰后“永不超生”

前文提到RelayCommandCanExecuteChanged嫁接到了CommandManager.RequerySuggested上,这个方案在大多数场景下是“够用”的,但它不是“万能”的。最典型的问题场景是:按钮的CanExecute依赖的并不是界面输入变化,而是一个后台异步任务的结果。比如你的按钮一开始是禁用的,你在后台线程里跑了一个耗时检查,检查结束时把IsReady设为true,这时候你发现按钮还是灰的——因为CommandManager.RequerySuggested多半在你切换焦点、点击界面的时候才会广播,后台线程修改属性并不会主动触发重查。这个现象的经典形容就是:按钮像被点了死穴,再也亮不起来。

要解决这个坑,方法很简单:把RelayCommand稍微升级一下,让它支持手动触发CanExecuteChanged。常见做法是在RelayCommand里加一个公共方法,比如叫RaiseCanExecuteChanged(),事件引用改成直接用CanExecuteChanged?.Invoke(this, EventArgs.Empty),这样的话你在网络请求回调或异步任务结束时手动调用AddTodoCommand.RaiseCanExecuteChanged(),界面按钮状态就会立即刷新。不过这里有个新的注意点:WPF默认不允许在非UI线程直接操作界面元素,所以这个手动通知要么确保在UI线程上调用,要么用Dispatcher封一层。新手阶段写到异步时尤其要把这两点同步记牢。

4.4 把业务逻辑塞进错误层级:ViewModel膨胀与View里的“偷袭”

MVVM用一段时间后,最常见的坏味道就是ViewModel开始无限膨胀。今天加一个属性做计算,明天加一个命令跑数据加载,后天再放一个集合放下拉框选项,三个月后ViewModel变成了一千多行的“二把手Code-behind”。出现这个信号时,不要急于“重构”,而是先按旧代码里职责最重的那块功能试水拆分,把纯业务逻辑下沉到Model或独立的Service,ViewModel只保留“当前界面状态”与“用户操作意图”的翻译工作。

另一个反向坏味道也经常看到:有人不适应MVVM,在新项目里仍然在Code-behind里写了一段this.Canvas.MouseLeftButtonDown += ...的代码,从XAML后门绕了进来。这种“偷袭式”写法最麻烦的地方在于它破坏了绑定树的上下文,导致ViewModel无从得知这段逻辑的存在,后面人接代码时稍不留神就会漏看。我处理这类情况的原则很简单:除非是与View生命周期强相关的代码(比如窗口加载动画、或者需要操作视图树本身的特效),否则一律不允许出现在Code-behind里。新手可以从头就立下这个规矩,能省很多沟通成本。

4.5 调试绑定的终极武器:输出窗口、FallbackValue与实时验证

最后一个细节不是某个具体错误,而是整套排查思路。绑定不生效的时候,新手的第一反应往往是在代码里瞎猜乱试,我这里提供一条更高效的链路。第一步:打开VS的输出窗口,选择“调试”来源,程序跑起来以后所有绑定失败都会有一行System.Windows.Data Error: 40之类的记录,里面的信息会精确到是哪个绑定路径找不到、哪个转换器报错。第二步:在XAML里给绑定临时加一个FallbackValue=???或者TargetNullValue=NaN,让绑定失败时界面能显式显示一个占位字符,这样你就能从视觉上立刻区分是“数据源为空”还是“绑定路径错误”。第三步:如果第二步还看不明白,就在ViewModel属性的getter里打一个断点,运行后看它到底有没有被读取、被谁读取、值是多少。这三步下来,绝大多数绑定问题都能在十分钟内定位。

5. 从“跑通”到“架构”:MVVM之后你该怎么继续走

前面这些内容里我完全没有提Prism、MVVMLight、CommunityToolkit.Mvvm之类的框架。这不是因为它们不重要,而是以我带新人的经验来看,框架必须建立在“手工分层熟练”的基础上才学得扎实。你不亲手写过一遍一万行左右的灭驱动代码,不亲手体会过从事件驱动迁移到MVVM的痛,你就算装了Prism也只会把它当成装饰品。

5.1 先别急着上Prism,把手工分层练到条件反射

什么叫条件反射?就是你写完一个需求之后,不需要停下来思考就能下意识地判断:这几个状态码应该放Model还是ViewModel;这个用户点击应该定义一个Command还是普通的属性和方法;这个界面上的列表是否应该封装成ObservableCollection。如果这些判断还要犹豫,那你上框架只会更懵。我经常对来问我“要不要现在学Prism”的新人说一句话:你首先得有“拆”的能力,然后才能体会框架替你做的那部分“组装”到底省在了哪里。

Prism引入区域(Region)、导航(Navigation)和依赖注入之后,整个项目的结构复杂度是手工MVVM的好几倍。我见过真的有人在新手阶段就硬上Prism,结果连模块加载都理不清,最后项目变成了一堆管不住生命周期的Region容器的集合,比不用框架时更难维护。顺带说一个我在网上看到的提问:“Prism在弹出用户控件内定义的Region注册不上”。这种问题十有八九是因为RegionManager的实例在弹窗上下文里和主窗口不是同一个,或者弹窗的视图还没有被注册到Region中就被尝试导航。这种边界问题在你对Region和依赖注入没有基本理解的时候,排查起来会非常痛苦。所以我的建议是:Manual分层至少保持三个项目再考虑上框架。

5.2 什么时候可以判断自己准备好迁移到框架了

判断信号其实很具体。当你的手工MVVM项目里出现了这些烦恼中的任何一种,就是该看框架的时机:第一,你的ViewModel构造函数的参数越来越多,你还得手动一个个new,希望有一个容器能统一管理;第二,你的窗口和弹窗导航不用只靠ShowDialogClose,希望有带参数的导航机制;第三,你的消息交互频繁,比如一个子窗口改了数据,要让好几个ViewModel同时知道,你不想再手写事件注册和注销逻辑,想用事件聚合器。这时候再去看Prism的模块化、导航、对话服务这些功能,你会理解得飞快。

5.3 实质上,这套结构思想不仅仅是WPF的专属

我最后想补一句可能有些超纲但很实用的话:MVVM这套思想说白了就是“界面与逻辑解耦”的具体实践,而一旦你把这个思维模式练出来了,你会发现它不止适用于WPF。你把ViewModel换成BindableObject,就能把同样的思路搬到MAUI、WinUI甚至Avalonia上;你把它里的Model换成后端Service层的DTO,同样的分层理念能帮助你写任何客户端代码都保持结构清晰。这也是为什么很多招聘JD上WPF岗位都要求MVVM的原因——他们真正要找的,不是会写XAML的人,而是能写出长期可维护客户端代码的人。

写到这儿,我的建议已经全部给到了。这个系列到终章,剩下的路只能自己走,多用几个真实项目去验证你在MVVM里吸收到的每一个概念。若有一日你翻到一年前自己写的ViewModel,觉得当时的分层满是笨拙,别沮丧,那恰恰说明你已经在离开新手村的路上了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询