MVVM框架详解:原理、实现与框架对比
文章目录
- 1. 引言
- 2. MVVM的基本概念
- 3. MVVM的原理与实现
- 3.1 数据绑定原理
- 3.2 命令模式实现
- 4. MVVM的优势与局限性
- 4.1 优势
- 4.2 局限性
- 5. 常见MVVM框架对比
- 5.1 MVVM Light
- 5.2 Prism
- 5.3 Caliburn.Micro
- 5.4 MvvmCross
- 5.5 ReactiveUI
- 6. 实际应用示例
- 7. 最佳实践与注意事项
- 7.1 MVVM最佳实践
- 7.2 常见陷阱与解决方案
- 8. 未来趋势
- 9. 结论
- 参考资料
1. 引言
MVVM(Model-View-ViewModel)是一种软件架构设计模式,已成为现代UI应用程序开发的主流模式之一。它通过将UI逻辑与业务逻辑分离,简化了开发过程,提高了代码的可维护性和可测试性。本文将深入探讨MVVM的原理、实现方式以及市面上常见MVVM框架的对比。

2. MVVM的基本概念
MVVM模式由三个关键组件组成:
-
Model(模型):表示应用程序的数据和业务逻辑,与UI完全无关。模型可以是简单的数据对象,也可以是复杂的业务领域模型。
-
View(视图):定义UI的结构、布局和外观,是用户与应用程序交互的界面。在MVVM中,视图是被动的,它通过数据绑定从ViewModel获取数据并显示。
-
ViewModel(视图模型):作为View和Model之间的中介,负责处理View的所有显示逻辑和用户交互逻辑。ViewModel暴露Model的数据和命令,使它们易于View进行绑定。
MVVM的核心思想是通过数据绑定和命令实现View和ViewModel的松耦合。这种方式降低了直接操作UI元素的需要,使代码更易于维护和测试。
3. MVVM的原理与实现
3.1 数据绑定原理
数据绑定是MVVM模式的核心机制,它建立了View与ViewModel之间的自动同步关系。当ViewModel中的数据变化时,View会自动更新;同样,当用户在View中输入数据时,这些变化也会自动反映到ViewModel中。
数据绑定的实现主要依赖于以下几个关键技术:
- 数据劫持/代理:通过Object.defineProperty或Proxy等技术拦截对象属性的访问和修改。
- 发布-订阅模式:建立数据变化与UI更新之间的通知机制。
- 数据监听:观察数据变化并触发相应的更新操作。
以下是简化的数据绑定实现示例:
// 数据劫持 - 使对象的属性变为可响应的
function defineReactive(obj, key, value) {const dep = new Dep();Object.defineProperty(obj, key, {get() {// 添加订阅者Dep.target && dep.addSub(Dep.target);return value;},set(newValue) {if (value !== newValue) {value = newValue;// 通知订阅者数据已更新dep.notify();}}});
}// 发布者 - 管理订阅者并发布通知
class Dep {constructor() {this.subs = []; // 订阅者列表}addSub(sub) {this.subs.push(sub);}notify() {// 通知所有订阅者this.subs.forEach(sub => sub.update());}
}// 订阅者 - 负责View的更新
class Watcher {constructor(vm, key, callback) {this.vm = vm;this.key = key;this.callback = callback;// 添加自己到依赖中Dep.target = this;this.value = vm[key]; // 触发getter,添加依赖Dep.target = null;}update() {const newValue = this.vm[this.key];if (this.value !== newValue) {this.value = newValue;this.callback(newValue);}}
}
3.2 命令模式实现
命令是MVVM模式中处理用户交互的主要方式。命令将UI事件(如按钮点击)绑定到ViewModel中的方法上,实现了用户操作与业务逻辑的解耦。
典型的命令实现通常包括:
- 可执行状态管理(CanExecute)
- 执行操作(Execute)
- 可执行状态变更通知(CanExecuteChanged)
以下是简化的命令模式实现示例:
// C#示例
public class RelayCommand : ICommand
{private readonly Action<object> _execute;private readonly Func<object, bool> _canExecute;public RelayCommand(Action<object> execute, Func<object, bool> canExecute = null){_execute = execute ?? throw new ArgumentNullException(nameof(execute));_canExecute = canExecute;}public bool CanExecute(object parameter){return _canExecute == null || _canExecute(parameter);}public void Execute(object parameter){_execute(parameter);}public event EventHandler CanExecuteChanged;public void RaiseCanExecuteChanged(){CanExecuteChanged?.Invoke(this, EventArgs.Empty);}
}
4. MVVM的优势与局限性
4.1 优势
-
关注点分离:MVVM清晰地分离了UI、表现逻辑和业务逻辑,使代码结构更清晰。
-
可测试性:ViewModel不依赖于View,可以独立进行单元测试,提高了测试覆盖率。
-
可维护性:由于关注点分离和松耦合,代码更容易维护和拓展。
-
代码复用:ViewModel可以被多个不同的View重用,增强了代码复用性。
-
设计与开发分离:设计师可以专注于UI设计,开发者专注于业务逻辑实现。
4.2 局限性
-
学习曲线:对于初学者来说,MVVM的概念和实现可能较为复杂。
-
性能开销:数据绑定和命令机制可能带来额外的性能开销,特别是在复杂应用中。
-
调试困难:数据绑定错误可能很难调试,特别是在复杂的绑定关系中。
-
过度设计:对于简单应用,使用MVVM可能导致过度设计。
5. 常见MVVM框架对比
目前市场上存在多种MVVM框架,以下是几个主流框架的对比:

5.1 MVVM Light
MVVM Light是一个轻量级的MVVM框架,主要针对WPF、UWP和Xamarin平台。它提供了基本的MVVM实现,包括ViewModelBase类、RelayCommand和Messenger(消息传递)。
优势:
- 轻量级,学习曲线低
- 易于集成到现有项目
- 灵活性高
劣势:
- 功能相对简单
- 缺乏高级特性(如导航框架、依赖注入容器等)
5.2 Prism
Prism是一个全面的应用程序框架,支持WPF和Xamarin.Forms。它提供了模块化、导航、区域管理、事件聚合等功能。
优势:
- 功能全面
- 模块化架构支持
- 内置依赖注入容器
- 强大的导航系统
劣势:
- 学习曲线较陡峭
- 可能对简单应用过于复杂
5.3 Caliburn.Micro
Caliburn.Micro采用"约定优于配置"的方法,通过命名约定自动连接View和ViewModel,减少了样板代码。
优势:
- 减少样板代码
- 强大的约定系统
- 内置屏幕导航
劣势:
- 约定可能导致隐式行为,增加调试难度
- 可能不适合大型团队或新手
5.4 MvvmCross
MvvmCross是一个强大的跨平台MVVM框架,支持几乎所有主流平台,包括Xamarin、WPF、UWP等。
优势:
- 出色的跨平台支持
- 强大的插件系统
- 活跃的社区和文档
劣势:
- 配置相对复杂
- 一些API设计不够直观
5.5 ReactiveUI
ReactiveUI结合了MVVM模式和响应式编程(Reactive Programming),特别适合复杂UI交互和异步操作。
优势:
- 强大的响应式编程模型
- 优雅处理异步和事件流
- 跨平台支持
劣势:
- 学习曲线陡峭
- 需要理解响应式编程概念
6. 实际应用示例
以下是一个使用MVVM模式的简单登录界面实现示例(以C#/WPF为例):
// Model
public class User
{public string Username { get; set; }public string Password { get; set; }public bool Validate(){// 实际应用中,这里应该有实际的验证逻辑return !string.IsNullOrEmpty(Username) && Password.Length >= 6;}
}// ViewModel
public class LoginViewModel : ViewModelBase
{private User _user;private string _errorMessage;private bool _isLoading;public LoginViewModel(){_user = new User();LoginCommand = new RelayCommand(ExecuteLogin, CanExecuteLogin);}public string Username{get => _user.Username;set{_user.Username = value;OnPropertyChanged();LoginCommand.RaiseCanExecuteChanged();}}public string Password{get => _user.Password;set{_user.Password = value;OnPropertyChanged();LoginCommand.RaiseCanExecuteChanged();}}public string ErrorMessage{get => _errorMessage;set{_errorMessage = value;OnPropertyChanged();}}public bool IsLoading{get => _isLoading;set{_isLoading = value;OnPropertyChanged();LoginCommand.RaiseCanExecuteChanged();}}public RelayCommand LoginCommand { get; }private bool CanExecuteLogin(object parameter){return _user.Validate() && !IsLoading;}private async void ExecuteLogin(object parameter){try{IsLoading = true;ErrorMessage = string.Empty;// 模拟网络请求await Task.Delay(2000);if (Username == "admin" && Password == "password"){// 登录成功,导航到主页面// NavigationService.Navigate(typeof(MainPage));}else{ErrorMessage = "用户名或密码错误";}}catch (Exception ex){ErrorMessage = $"登录失败: {ex.Message}";}finally{IsLoading = false;}}
}
<!-- View (XAML) -->
<Grid><StackPanel Width="300" VerticalAlignment="Center"><TextBlock Text="用户登录" FontSize="24" HorizontalAlignment="Center" Margin="0,0,0,20"/><TextBlock Text="用户名:"/><TextBox Text="{Binding Username, UpdateSourceTrigger=PropertyChanged}" Margin="0,5,0,10"/><TextBlock Text="密码:"/><PasswordBox x:Name="PasswordBox" Margin="0,5,0,10"/><TextBlock Text="{Binding ErrorMessage}" Foreground="Red" Margin="0,10"/><Button Content="登录" Command="{Binding LoginCommand}" Height="40" Margin="0,10,0,0"><Button.Style><Style TargetType="Button"><Style.Triggers><DataTrigger Binding="{Binding IsLoading}" Value="True"><Setter Property="Content" Value="登录中..."/><Setter Property="IsEnabled" Value="False"/></DataTrigger></Style.Triggers></Style></Button.Style></Button></StackPanel>
</Grid>
7. 最佳实践与注意事项
7.1 MVVM最佳实践
-
保持ViewModel独立于View:ViewModel不应包含任何UI相关的引用,确保它可以被独立测试。
-
使用命令处理用户交互:避免在View的代码后台处理UI事件,而是使用命令将事件绑定到ViewModel的方法。
-
合理划分责任:
- Model:业务逻辑和数据
- ViewModel:UI逻辑和状态管理
- View:UI展示和用户交互
-
避免过度设计:对于简单应用,完整实现MVVM可能是过度设计。根据项目复杂度选择适当的模式。
-
适当使用事件聚合器:对于不相关组件之间的通信,考虑使用事件聚合器或消息总线模式。
7.2 常见陷阱与解决方案
-
过度绑定:不是所有属性都需要绑定,过度绑定会导致性能问题。
解决方案:只绑定需要在UI中显示或由用户修改的属性。
-
视图逻辑泄漏到ViewModel:ViewModel包含特定于视图的逻辑。
解决方案:使用值转换器处理视图特定的转换逻辑。
-
巨大的ViewModels:随着功能增加,ViewModel变得臃肿。
解决方案:将大型ViewModel分解为更小的、更专注的组件,使用组合模式。
-
内存泄漏:事件订阅未取消导致的内存泄漏。
解决方案:确保在适当的时机(如视图卸载时)取消事件订阅。
8. 未来趋势
-
MVVM与响应式编程的结合:如ReactiveUI所展示的,结合响应式编程与MVVM模式可以更优雅地处理复杂UI交互和异步操作。
-
跨平台MVVM框架的普及:随着.NET MAUI等跨平台框架的发展,统一的MVVM实现将变得更加普遍。
-
服务器端MVVM:MVVM模式正在扩展到服务器端渲染的Web应用中,如Blazor。
-
AI辅助MVVM开发:借助AI工具生成ViewModel样板代码,提高开发效率。
9. 结论
MVVM模式通过分离关注点、提高代码可测试性和可维护性,为复杂UI应用程序的开发提供了强大的架构支持。不同的MVVM框架各有优缺点,开发者应根据项目需求和团队经验选择合适的框架。
随着技术的发展,MVVM模式将继续演化,但其核心原则——分离UI与业务逻辑,通过数据绑定实现松耦合——将保持不变,继续为开发高质量应用程序提供坚实的基础。
无论选择哪种框架,理解MVVM的基本原理和实现机制是掌握这种模式的关键。希望本文能帮助读者更深入地理解MVVM,并在实际项目中更好地应用这一模式。
参考资料
- MVVM Light
- Prism
- Caliburn.Micro
- MvvmCross
- ReactiveUI
- Vue.js
相关文章:
MVVM框架详解:原理、实现与框架对比
文章目录 1. 引言2. MVVM的基本概念3. MVVM的原理与实现3.1 数据绑定原理3.2 命令模式实现 4. MVVM的优势与局限性4.1 优势4.2 局限性 5. 常见MVVM框架对比5.1 MVVM Light5.2 Prism5.3 Caliburn.Micro5.4 MvvmCross5.5 ReactiveUI 6. 实际应用示例7. 最佳实践与注意事项7.1 MVV…...
opencv--图像处理
这里所说的图像处理并不是专业术语,而是值开发人员对图像的处理技术方法。 教程 菜鸟教程 书籍推介--<opencv4.5 计算机视觉开发实践 基于vc>.朱文伟 获取图像数据 三种方式: cv::VideoCapture: OpenCV 提供的视频捕获类࿰…...
达梦官方管理工具 SQLark——全面支持达梦、Oracle、MySQL、PostgreSQL 数据库!
SQLark 是一款面向信创应用开发者的数据库开发和管理工具,用于快速查询、创建和管理不同类型的数据库系统,已支持达梦、Oracle、MySQL数据库;在最新的 V3.4 版本中,SQLark 新增了对 PostgreSQL 的支持,兼容 PostgreSQL…...
解读大型语言模型:从Transformer架构到模型量化技术
一、生成式人工智能概述 生成式人工智能(Generative Artificial Intelligence)是一种先进的技术,能够生成多种类型的内容,包括文本、图像、音频以及合成数据等。其用户界面的便捷性极大地推动了其广泛应用,用户仅需在…...
理解计算机系统_网络编程(1)
前言 以<深入理解计算机系统>(以下称“本书”)内容为基础,对程序的整个过程进行梳理。本书内容对整个计算机系统做了系统性导引,每部分内容都是单独的一门课.学习深度根据自己需要来定 引入 网络是计算机科学中非常重要的部分,笔者过去看过相关的内…...
前端面试场景题
目录 1.项目第一次加载太慢优化 / vue 首屏加载过慢如何优化 2.说说了解的es6-es10的东西有哪些 ES6(ES2015)之后,JavaScript 新增了许多实用的数组和对象方法,下面为你详细介绍: 3.常见前端安全性问题 XSS&#…...
Unity使用Rider的常用快捷键
最近换了IDE,改用Rider进行Unity的代码编写 Rider提供了几个快捷键方案供选择,默认的是Visual Studio的快捷键方案。我索性直接选择了Rider的快捷键方案,一则这2年搞H5没用Visual Studio,快捷键已经忘的差不多了;二则…...
Spring Boot + MyBatis 动态字段更新方法
在Spring Boot和MyBatis中,实现动态更新不固定字段的步骤如下: 方法一:使用MyBatis动态SQL(适合字段允许为null的场景) 定义实体类 包含所有可能被更新的字段。 Mapper接口 定义更新方法,参数为实体对象&…...
Unity多线程渲染指令队列设计与集成技术详解
一、多线程渲染架构设计背景 1. 传统渲染管线瓶颈分析 阶段单线程耗时占比可并行化潜力场景遍历与排序35%★★★★☆材质属性更新20%★★★★★GPU指令提交25%★★☆☆☆资源上传20%★★★★☆ 2. 多线程渲染优势 CPU核心利用率:从单线程到全核心并行 指令缓冲优…...
栈和队列学习记录
一、栈 1.栈的概念 操作受限的线性表-----栈:栈只允许在表的一端进行插入和删除操作,这一端被称为栈顶(Top),另一端则是栈底(Bottom)。这种受限的操作方式使得栈遵循后进先出(LIFO…...
位运算练习:起床困难综合征(贪心,位运算)【算法竞赛进阶指南学习笔记】
目录 前情提要起床困难综合征(贪心,位运算) 前情提要 一些基础运算操作用法看看上一篇; 起床困难综合征(贪心,位运算) 题目原文 [P2114 NOI2014] 起床困难综合症 - 洛谷 思路分析 题目很长…...
ubuntu24设置拼音输入法,解决chrome不能输入中文
## 推荐方案:使用 Fcitx5 Fcitx5 是当前在 Wayland 环境下兼容性最好的输入法框架。 ### 1. 安装 Fcitx5 bash sudo apt update sudo apt install fcitx5 fcitx5-chinese-addons fcitx5-frontend-gtk3 fcitx5-frontend-gtk4 fcitx5-frontend-qt5 fcitx5-module-c…...
React SSR + Redux 导致的 Hydration 报错踩坑记录与修复方案
一条“Hydration failed”的错误,让我损失了半天时间 背景 我在用 Next.js App Router Redux 开发一个任务管理应用,一切顺利,直到打开了 SSR(服务端渲染),突然看到这个令人头皮发麻的报错: …...
轻量级景好鼠标录制器
景好鼠标录制器(详情请戳 官网)是一款免费无广的键鼠动作录制/循环回放工具,轻松自动化应对一些重复繁琐的操作任务,如来回切换窗口、文档同一相对位置的复制粘贴等场景,兼容Win XP - 11 。毕竟此款本身主打简约类型&a…...
leetcode--两数之和 三数之和
1.两数之和 给你一个下标从 1 开始的整数数组 numbers ,该数组已按 非递减顺序排列 ,请你从数组中找出满足相加之和等于目标数 target 的两个数。如果设这两个数分别是 numbers[index1] 和 numbers[index2] ,则 1 < index1 < index2 …...
FFMPEG-视频解码-支持rtsp|rtmp|音视频文件(低延迟)
本人亲测解码显示对比延迟达到7到20毫秒之间浮动兼容播放音视频文件、拉流RTSP、RTMP等网络流 基于 Qt 和 FFmpeg 的视频解码播放器类,继承自 QThread,实现了视频流的解码、播放控制、帧同步和错误恢复等功能 工作流程初始化阶段: 用户设置URL和显示尺寸 调用play()启动线程解…...
openEuler安装nvidia驱动【详细版】
注意:在 openEuler 24.03 LTS 系统中安装 NVIDIA 驱动(RTX 3090)需要禁用默认的 Nouveau 驱动并手动安装官方驱动。 一、准备工作 系统更新与依赖安装 更新系统并安装必要依赖包:sudo dnf update -y sudo dnf install gcc make k…...
力扣DAY63-67 | 热100 | 二分:搜索插入位置、搜索二维矩阵、排序数组查找元素、搜索旋转排序数组、搜索最小值
前言 简单、中等 √ 二分法思路很简单,但是判断边界太麻烦了!难道真的要去背模板吗 搜索插入位置 我的题解 循环条件左不超过右,目标大于中间值(向下取整)时,左中1,小于,右中-1&…...
基于Python爬虫的豆瓣电影信息爬取(可以根据选择电影编号得到需要的电影信息)
# 豆瓣电影信息爬虫(展示效果如下图所示:) 这是一个功能强大的豆瓣电影信息爬虫程序,可以获取豆瓣电影 Top 250 的详细信息。 ## 功能特点 - 自动爬取豆瓣电影 Top 250 的所有电影信息 - 支持分页获取,每页 25 部电影,共 10 页 - 获取每部电影的详细信息,包括: - 标题…...
程序员思维体操:TDD修炼手册
程序员思维体操:TDD修炼手册 ——从"先写代码"到"测试先行"的认知革命 一、重新认识TDD:不仅仅是写测试 什么是TDD(测试驱动开发) TDD其实很简单,不要看名字很高级复杂,传统开发是直…...
PHP异常处理__RuntimeException运行时错误
以下是对 PHP 中 RuntimeException 的详细解释: 一、RuntimeException 概述 RuntimeException 是 PHP 内置的异常类,它继承自 Exception 类。它通常用于表示在程序运行时发生的异常情况,这些异常情况通常是在程序正常执行过程中出现的错误&…...
从性能到安全:大型网站系统架构演化的 13 个核心维度
大型网站系统架构的演化是一个复杂的过程,涉及到多个维度的技术内容,从关键维度进行详细分析: 1.性能维度 缓存技术:包括浏览器缓存、CDN(内容分发网络)缓存、服务器端缓存(如 Memcached、Red…...
基于PaddleOCR对图片中的excel进行识别并转换成word优化(二)
0、原图 一、优化地方 计算行的时候,采用概率分布去统计差值概率比较大的即为所要的值。 def find_common_difference(array):"""判断数组中每个元素的差值是否相等,并返回该差值:param array: 二维数组,其中每个元素是一个…...
spring Ai---向量知识库(二)
RAG:检索增强,结合了检索和生成两种技术;用于提升生成模型的效果。 1.信息检索(R) :系统从一个大型文档库中检索出与查询最相关的文档片段。这一步的目标是找到那些可能包含答案或相关信息的文档。 2.生成增强…...
Nvidia显卡架构演进
1 简介 显示卡(英语:Display Card)简称显卡,也称图形卡(Graphics Card),是个人电脑上以图形处理器(GPU)为核心的扩展卡,用途是提供中央处理器以外的微处理器帮…...
rollup使用讲解
rollup 总结 什么是 rollup? rollup 是一个 JavaScript 模块打包器,在功能上要完成的事和 webpack 性质一样,就是将小块代码编译成大块复杂的代码,例如 library 或应用程序。在平时开发应用程序时,我们基本上选择用 webpack,相比之下,rollup.js 更多是用于 library 打…...
USO服务器操作系统手动升级GCC 12.2.0版本
1. 从 GNU 官方 FTP 服务器下载 GCC 12.2.0 的源码包,并解压进入源码目录。 wget https://ftp.gnu.org/gnu/gcc/gcc-12.2.0/gcc-12.2.0.tar.gz tar -zxvf gcc-12.2.0.tar.gz cd gcc-12.2.0 2. 运行脚本下载并配置 GCC 编译所需的依赖库。此步骤会自动下载如 GMP…...
STM32F407使用ESP8266实现阿里云OTA(上)
文章目录 前言一、阿里云OTA二、命令调试1.升级包上传2.OTA订阅和上报的主题3.命令调试4.具体效果三、所用到的工具和材料前言 在经过前面对ESP8266、SD卡、FLASH的了解之后,终于要进入我们的正题了,就是使用STM32和ESP8266实现阿里云的OTA。这一功能并不复杂,只是需要主要…...
玩转Docker | 使用Docker部署DashMachine个人书签工具
玩转Docker | 使用Docker部署DashMachine个人书签工具 前言一、DashMachine介绍DashMachine简介DashMachine使用场景二、系统要求环境要求环境检查Docker版本检查检查操作系统版本三、部署DashMachine服务下载镜像创建容器创建容器检查容器状态检查服务端口安全设置四、访问Das…...
测试基础笔记第九天
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 一、数据类型和约束1.数据类型2.约束3.主键4.不为空5.唯一6.默认值 二、数据库操作1.创建数据库2.使用数据库3.修改数据库4.删除数据库和查看所有数据库5.重点&…...
