MVVM Light 架构笔记:定位器、命令、消息与 IoC 实践

一、核心概念

术语 说明
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 的典型结构通常是 ModelsViewsViewModelsServices/RepositoriesHelpers 分层,并借助定位器完成视图与视图模型绑定。

二、常用操作

常用能力速查

操作/能力 常见类型或语法 说明
属性通知 ViewModelBaseRaisePropertyChanged()Set(...) ViewModel 属性变化后自动刷新界面
命令绑定 RelayCommandRelayCommand<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 中大多数按钮、菜单项都可以直接绑定 CommandCommandParameter

  • 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];
            });
        }
    }
}

这一流程在现有笔记中的抽象顺序是:

  1. 定义模型。

  2. 定义接口服务。

  3. 编写服务实现。

  4. 在 IoC 容器中注册服务。

  5. 在 ViewModel 中通过构造函数接收服务。

  6. 在命令中调用服务完成数据访问。

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.csServiceLocator 相关引用报错。

  • 原因 :示例项目及新环境中,旧命名空间 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

相关推荐
蓝黑墨水1 小时前
动画角色的整个流程
学习
上海云盾第一敬业销售3 小时前
高防CDN与高防IP应用场景架构解析
网络协议·tcp/ip·架构
ZK_H3 小时前
MFC学习——简易计算器以及跨应用通信
学习·5g·mfc
kobesdu3 小时前
【ROS2实战笔记-24】ROS2 Launch 实用技巧:条件逻辑与节点动态生成
笔记·ros·slam
小满Autumn3 小时前
CommunityToolkit.Mvvm 架构笔记:现代 MVVM、源生成器与工程化实践
笔记·架构·c#·.net·wpf·mvvm
加号34 小时前
【C#】 JSON 序列化与反序列化:从入门到最佳实践
c#·json
上海云盾第一敬业销售4 小时前
高防CDN与传统CDN架构解析
web安全·架构·ddos
踏着七彩祥云的小丑5 小时前
Go学习第1天:入门
开发语言·学习·golang·go