一、核心概念
| 术语 | 说明 |
|---|---|
| MVVM Light | 早期常用的 MVVM 轻量框架,适合理解 MVVM 的基本落地方式,但当前已停止更新 |
| ViewModelBase | MVVM Light 中最常用的视图模型基类,封装了属性通知、设计时支持等能力 |
| ObservableObject | ViewModelBase 的上层基础类型,负责实现 INotifyPropertyChanged |
| RelayCommand | 用于将按钮点击、菜单操作等 UI 行为绑定到 ViewModel 命令 |
| Messenger | 用于 View 与 ViewModel、ViewModel 与 ViewModel 之间做低耦合消息传递 |
| SimpleIoc | MVVM Light 自带的轻量 IoC 容器,可注册服务和 ViewModel |
| ViewModelLocator | 在 App.xaml 中作为资源注册,为 View 统一解析和提供对应的 ViewModel |
| ServiceLocator | 一个全局服务访问入口,MVVM Light 常配合 SimpleIoc 一起使用 |
结合现有笔记与示例项目可以看到,MVVM Light 的典型结构通常是
Models、Views、ViewModels、Services/Repositories、Helpers分层,并借助定位器完成视图与视图模型绑定。
二、常用操作
常用能力速查
| 操作/能力 | 常见类型或语法 | 说明 |
|---|---|---|
| 属性通知 | ViewModelBase、RaisePropertyChanged()、Set(...) |
ViewModel 属性变化后自动刷新界面 |
| 命令绑定 | RelayCommand、RelayCommand<T> |
将按钮、菜单、行为事件绑定到 ViewModel |
| 消息通信 | Messenger.Default.Send(...)、Register(...) |
降低窗体间或模块间直接引用 |
| 依赖注册 | SimpleIoc.Default.Register<T>() |
将服务或 ViewModel 交给容器管理 |
| 视图模型定位 | ViewModelLocator + StaticResource Locator |
为 View 提供统一的 DataContext 来源 |
| 行为触发 | Microsoft.Xaml.Behaviors.Wpf |
给没有 Command 属性的控件绑定事件命令 |
1. 安装后常见结构与定位器绑定
cs
// App.xaml 中通常注册 ViewModelLocator 为全局资源
<Application.Resources>
<vm:ViewModelLocator
xmlns:vm="clr-namespace:MVVMLigtDemo.ViewModel"
x:Key="Locator"
d:IsDataSource="True" />
</Application.Resources>
// 某个 View 中使用 Locator 解析 ViewModel
<Window
DataContext="{Binding Source={StaticResource Locator}, Path=Main}">
</Window>
using GalaSoft.MvvmLight.Ioc;
using CommonServiceLocator;
namespace MVVMLigtDemo.ViewModel;
public class ViewModelLocator
{
public ViewModelLocator()
{
ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);
// 注册 ViewModel
SimpleIoc.Default.Register<LoginViewModel>();
SimpleIoc.Default.Register<MainViewModel>();
SimpleIoc.Default.Register<UserListViewModel>();
SimpleIoc.Default.Register<BlogListViewModel>();
// 注册服务
SimpleIoc.Default.Register<IService<User, ViewUser>, UserService>();
}
public LoginViewModel Login => ServiceLocator.Current.GetInstance<LoginViewModel>();
public MainViewModel Main => ServiceLocator.Current.GetInstance<MainViewModel>();
public UserListViewModel UserList => ServiceLocator.Current.GetInstance<UserListViewModel>();
public BlogListViewModel BlogList => ServiceLocator.Current.GetInstance<BlogListViewModel>();
}
这一模式在示例项目中直接体现于
MVVM相关架构笔记/MVVMLigtDemo/MVVMLigtDemo/ViewModel/ViewModelLocator.cs,它的本质是:先注册,再解析,让 View 不直接new ViewModel。
2. 在 ViewModel 中实现属性通知
cs
using GalaSoft.MvvmLight;
namespace MVVMLigtDemo.ViewModel;
public class LoginViewModel : ViewModelBase
{
private string account = "admin";
public string Account
{
get => account;
set
{
account = value;
RaisePropertyChanged();
}
}
private string password = "admin";
public string Password
{
get => password;
set => Set(ref password, value);
}
}
说明:
-
RaisePropertyChanged()适合手动控制通知时机。 -
Set(ref field, value)会在值变化时自动赋值并触发通知,写法更简洁。 -
结合现有银行系统笔记,账号、密码、列表数据等都适合通过这种方式绑定到 UI。
3. 使用 RelayCommand 处理界面命令
cs
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using System.Windows;
namespace MVVMLigtDemo.ViewModel;
public class MainViewModel : ViewModelBase
{
public RelayCommand UnLoadedCommand
{
get
{
return new RelayCommand(() =>
{
// 关闭隐藏的登录窗体,让应用整体退出
Application.Current.MainWindow.Close();
});
}
}
public RelayCommand<object> OpenPageCommand
{
get
{
return new RelayCommand<object>((obj) =>
{
Type pageType = Type.GetType($"MVVMLigtDemo.View.UserContrls.{obj}");
var control = (UserControl)Activator.CreateInstance(pageType);
UserBlogControle = control;
});
}
}
private UserControl userBlogControl;
public UserControl UserBlogControle
{
get => userBlogControl;
set
{
userBlogControl = value;
RaisePropertyChanged();
}
}
}
适用点:
-
RelayCommand:无参命令。 -
RelayCommand<T>:带参数命令,如页面名称、行对象、菜单标识等。 -
在 WPF 中大多数按钮、菜单项都可以直接绑定
Command和CommandParameter。 -
Type.GetType(...)更适合作为示例项目中的动态创建演示;实际项目中更推荐显式映射、工厂模式或导航服务,而不是大量依赖字符串拼接。
4. 为没有 Command 属性的控件绑定事件
cs
<Window xmlns:i="http://schemas.microsoft.com/xaml/behaviors">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Unloaded">
<i:InvokeCommandAction Command="{Binding UnLoadedCommand}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</Window>
说明:
-
该方式来自现有笔记中对
Unloaded事件绑定的总结。 -
需要安装
Microsoft.Xaml.Behaviors.Wpf。 -
适合
Window、部分自定义控件或原生不带Command属性的事件型场景。
5. 使用 Messenger 完成模块消息通信
cs
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using GalaSoft.MvvmLight.Messaging;
namespace MVVMLigtDemo.ViewModel.UserContrls;
public class UserListViewModel : ViewModelBase
{
public RelayCommand<object> SendCommand
{
get
{
return new RelayCommand<object>((obj) =>
{
var user = new User
{
Account = obj?.ToString() + "1231232"
};
Messenger.Default.Send(user);
});
}
}
}
using GalaSoft.MvvmLight.Messaging;
public class ReceiverViewModel : ViewModelBase
{
public ReceiverViewModel()
{
Messenger.Default.Register<User>(this, user =>
{
// 接收并处理消息
CurrentUser = user;
});
}
public User CurrentUser { get; private set; }
}
实践建议:
-
只有在消息发送时已经完成注册的接收者,才能收到该次消息;
Messenger默认不会缓存历史消息。 -
消息适合跨模块通知,不适合承载复杂业务流程。
-
若消息类型较多,建议用明确的消息对象,而不是大量裸
string。
6. 通过 SimpleIoc 注册数据库访问服务
cs
using GalaSoft.MvvmLight.Ioc;
// 注册服务
SimpleIoc.Default.Register<IService<User, ViewUser>, UserService>();
// ViewModel 中通过构造函数注入服务
public class LoginViewModel : ViewModelBase
{
private readonly IService<User, ViewUser> service;
public LoginViewModel(IService<User, ViewUser> service)
{
this.service = service;
}
public RelayCommand LoginCommand
{
get
{
return new RelayCommand(() =>
{
var exp = Expressionable.Create<ViewUser>();
exp.And(u => u.Account == Account);
exp.And(u => u.Password == Password);
var list = service.GetList(exp);
if (list.Count != 1)
{
MessageBox.Show("登录失败", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
LoginInfo.CurrentUser = list[0];
});
}
}
}
这一流程在现有笔记中的抽象顺序是:
-
定义模型。
-
定义接口服务。
-
编写服务实现。
-
在 IoC 容器中注册服务。
-
在 ViewModel 中通过构造函数接收服务。
-
在命令中调用服务完成数据访问。
7. 登录成功后切换主窗体
cs
private void Application_Startup(object sender, StartupEventArgs e)
{
var loginWindow = new Login();
if (loginWindow.ShowDialog() == true)
{
var mainWindow = new Main();
mainWindow.ShowDialog();
return;
}
Application.Current.Shutdown();
}
public RelayCommand LoginCommand
{
get
{
return new RelayCommand(() =>
{
Application.Current.MainWindow.Hide();
string mainFullNamespace = "MVVMLigtDemo.View.Main";
Type type = Type.GetType(mainFullNamespace);
var mainWindow = (Window)Activator.CreateInstance(type);
mainWindow.ShowDialog();
});
}
}
补充说明:
-
如果先隐藏登录窗体再打开主窗体,可能需要将应用关闭模式设置为
ShutdownMode="OnExplicitShutdown"。 -
主窗体关闭时若应用未退出,可以在
Unloaded中调用Application.Current.Shutdown(),或关闭最初隐藏的登录窗体。
8. PasswordBox 绑定问题处理
cs
<PasswordBox
Helper:PasswordBoxHelper.Attach="True"
Helper:PasswordBoxHelper.Password="{Binding Password, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
原因:
-
PasswordBox.Password不是依赖属性,不能像TextBox.Text一样直接绑定。 -
实际项目中通常通过附加属性或行为封装间接绑定。
三、问题排查
错误1:ViewModelLocator 中命名空间报错
-
现象 :
ViewModelLocator.cs中ServiceLocator相关引用报错。 -
原因 :示例项目及新环境中,旧命名空间
Microsoft.Practices.ServiceLocation已不可用或版本不匹配。 -
解决 :改为引用
CommonServiceLocator,并确保对应包已安装。
cs
using GalaSoft.MvvmLight.Ioc;
using CommonServiceLocator;
ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);
错误2:主窗体关闭后程序没有退出
-
现象:登录成功后主窗体能打开,但关闭主窗体后进程仍在后台运行。
-
原因 :登录窗体只是
Hide(),它仍然是应用的第一个主窗体,没有真正关闭。 -
解决 :在主窗体
Unloaded中显式退出应用,或关闭隐藏的登录窗体。
cs
public RelayCommand UnLoadedCommand
{
get
{
return new RelayCommand(() =>
{
Application.Current.MainWindow.Close();
});
}
}
错误3:PasswordBox 不能直接绑定
-
现象 :出现
XamlParseException,提示不能对Password设置Binding。 -
原因 :
Password不是依赖属性。 -
解决:通过附加属性或行为方式中转绑定。
错误4:消息发送成功但接收端无反应
-
现象 :调用
Messenger.Default.Send(...)后,接收方没有收到数据。 -
原因:接收方没有提前注册,或者消息类型不一致。
-
解决 :确保接收方在初始化时执行
Register<T>(),并保证发送和接收的类型一致。
cs
Messenger.Default.Register<User>(this, user =>
{
CurrentUser = user;
});
错误5:动态页面切换时报空引用或类型解析失败
-
现象 :点击菜单后
Type.GetType(...)返回null,页面无法创建。 -
原因:命名空间字符串写错,或目标控件未使用完整限定名。
-
解决:检查完整命名空间、类名和程序集名称,必要时改为显式映射字典而不是拼接字符串。
结论上,MVVM Light 很适合作为理解传统 WPF MVVM 落地方式的学习样本:它把"属性通知、命令、消息、定位器、IoC"这几件核心事情拆得很清楚;但在新项目中,更推荐优先考虑仍在维护的
CommunityToolkit.Mvvm。