目录

一、Model实体层

二、ViewModel视图模型层

1、定义属性通知基类

1.1 数据验证接口的实现

1.2 验证标识类定义

2、ViewModel前端交互实现

2.1 ICommand命令基类

2.2 窗口管理器实现

三、View前端实现

1、交互行为

2、Adorner装饰器

3、XMAL设计

3.1 引用程序集

3.2  引用装饰器行为

3.3 属性绑定

3.4 附加事件绑定


先套用下老话,什么是MVVM?

MVVM是Model-View-ViewModel的简写。它本质上就是MVC (Model-View- Controller)的改进版。即模型-视图-视图模型。分别定义如下:

  • 【模型】指的是后端传递的数据。
  • 【视图】指的是所看到的页面。
  • 【视图模型】mvvm模式的核心,它是连接view和model的桥梁。它有两个方向:
    • 一是将【模型】转化成【视图】,即将后端传递的数据转化成所看到的页面。实现的方式是:数据绑定。
    • 二是将【视图】转化成【模型】,即将所看到的页面转化成后端的数据。实现的方式是:DOM 事件监听。这两个方向都实现的,我们称之为数据的双向绑定。

MVVM示意图如下所示:

一、Model实体层

在笔者理解看来,它也算是某种意义上的实体,因为完全可以简化真正意义上实体部分(仅需涵盖需与前端交互的字段信息)及可补充需交互时的辅助属性字段,也可以完全以实际实体结构为主;

Example:

public class LoginModel
{/// <summary>/// 登录账号/// </summary>public string Logno { get; set; }/// <summary>/// 用户名称/// </summary>public string Userna { get; set; }/// <summary>/// 账号密码/// </summary>public string Pwd { get; set; }/// <summary>/// 数据填写正确(可视属性字段)/// </summary>public bool IsValid { get; set; }
}

Model实际发挥的作用,当然是贯穿DAL层的读写以及与ViewModel层的交互作用;具体对DAL层的交互读写在此就不具体展开了。

二、ViewModel视图模型层

在这里,可能也有人和笔者同样有着一个好奇的问题,为什么它只称为MVVM,而为什么不称为MVMV?本文开头就已经明确MVMM本质就是由MVC演变而来,由Controller演变成ViewModel视图模型。如果还有人争议为什么MVC不称为MCV,这个问题我想只能去问问MVC的创造者(Trygve Reenskaugh和Adele Goldberg两位大神)了,当然仍感兴趣的朋友可参考:从MVC到现代Web框架 | 码农网

言归正传,ViewModel是MVVM核心思想,主要承担的就是两件事:

  • Model -> View (通知)
  • View -> Model (通知)

直白的理解,就是消息收发室的概念。通过INotifyPropertyChanged接口实现与客户端属性变更通知。

1、定义属性通知基类

public class NotificationProperty : INotifyPropertyChanged, IDataErrorInfo
{#region 属性发生改变通知public event PropertyChangedEventHandler PropertyChanged;/// <summary>/// 发起通知/// </summary>/// <param name="propertyName">属性名</param>public void RaisePropertyChanged(string propertyName){PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));}#endregion
}

1.1 数据验证接口的实现

public class NotificationProperty : INotifyPropertyChanged, IDataErrorInfo
{#region 数据验证protected virtual string GetValidationErrors(){//验证上下文类实例化var vc = new ValidationContext(this, null, null);//验证请求的结果容器var vResults = new List<ValidationResult>();return !Validator.TryValidateObject(this, vc, vResults, true)? vResults.Aggregate("", (current, ve) => current + ve.ErrorMessage + Environment.NewLine): "";}protected virtual string GetValidationErrors(string columnName){//验证上下文类实例化var vc = new ValidationContext(this, null, null);//验证请求的结果容器var vResults = new List<ValidationResult>();//检查确定指定的对象是否有效if (!Validator.TryValidateObject(this, vc, vResults, true)){string error = "";foreach (var ve in vResults){if (ve.MemberNames.Contains(columnName, StringComparer.CurrentCultureIgnoreCase))error += ve.ErrorMessage + Environment.NewLine;}return error;}return "";}string IDataErrorInfo.Error => GetValidationErrors();string IDataErrorInfo.this[string columnName] => GetValidationErrors(columnName);/// <summary>/// 页面中是否所有控制数据验证正确/// </summary>public virtual bool IsValid { get; set; }#endregion
}

1.2 验证标识类定义

继承所有验证特性ValidationAttribute基类。

/// <summary>
/// 检查字段是否为空
/// </summary>
public class IsNullCheck : ValidationAttribute
{public override bool IsValid(object value){var name = value as string;return !string.IsNullOrEmpty(name);}public override string FormatErrorMessage(string name){return "不能为空";}
}/// <summary>
/// 检查字段是否为数值
/// </summary>
public class LognoExists : ValidationAttribute
{public override bool IsValid(object value){if (value == null) return false;var name = value as string;Regex regex = new Regex("^[0-9]*$");return regex.IsMatch(name);}public override string FormatErrorMessage(string name){return "账号名必须为纯数字字符!";}
}

2、ViewModel前端交互实现

这里既采用预定义的验证标识符,同时将Model中属性逐个均做了客户端通知绑定。重点要说明的是额外增加了ToClose属性(非Model字段),同时此处定义了一个ICommand命令事件。

public class LoginViewModel : NotificationProperty
{private LoginModel loginModel = new LoginModel();public LoginViewModel(){}[IsNullCheck][LognoExists]public string Logno{get => loginModel.Logno;set{loginModel.Logno = value;RaisePropertyChanged("Logno");}}[IsNullCheck]public string Password{get => loginModel.Pwd;set{loginModel.Pwd = value;RaisePropertyChanged("Password");}}[IsNullCheck]public string Userna{get => loginModel.Userna;set{loginModel.Userna = value;RaisePropertyChanged("Userna");}}private bool toClose = false;/// <summary>/// 是否要关闭窗口/// </summary>public bool ToClose{get => toClose;set{toClose = value;if (toClose) RaisePropertyChanged("ToClose");}}/// <summary>/// 数据填写正确/// </summary>public override bool IsValid{get => loginModel.IsValid;set{loginModel.IsValid = value;RaisePropertyChanged("IsValid");}}private BaseCommand loginClick;/// <summary>/// 登录事件/// </summary>public BaseCommand LoginClick{get{if (loginClick == null){loginClick = new BaseCommand(new Action<object>(o =>{//执行登录逻辑WindowManager.Show("MainWindow", null);ToClose = true;}));}return loginClick;}}
}

题外话,NuGet中MvvmLight包,实际上就是省去INotifyPropertyChanged属性通知基类和ICommand基类的一个框架包。

感兴趣的朋友可参考:走进WPF之MVVM完整案例 - 小六公子 - 博客园

2.1 ICommand命令基类

/// <summary>
/// 命令基类
/// </summary>
public class BaseCommand : ICommand
{private Func<object, bool> _canExecute;private Action<object> _execute;public BaseCommand(Action<object> execute, Func<object, bool> canExecute){_execute = execute;_canExecute = canExecute;}public BaseCommand(Action<object> execute) : this(execute, null) { }public event EventHandler CanExecuteChanged{add{if (_canExecute != null) CommandManager.RequerySuggested += value;}remove{if (_canExecute != null) CommandManager.RequerySuggested -= value;}}public bool CanExecute(object parameter){return _canExecute == null ? true : _canExecute(parameter);}public void Execute(object parameter){if (_execute != null && CanExecute(parameter)) _execute(parameter);}
}

注:在XAML UI前端中,Click事件实现的RoutedEventArgs事件,实际上也是经ICommand接口从而实现的。

感兴趣的朋友可参考:WPF Command - Clingingboy - 博客园

WPF 命令(RoutedCommand自定义命令,实现 ICommand 接口自定义命令)。推荐使用实现 ICommand 接口自定义命令_tiz198183的博客-CSDN博客_routedcommand

2.2 窗口管理器实现

/// <summary>
/// 窗口管理器
/// </summary>
public static class WindowManager
{private static Hashtable _RegisterWindow = new Hashtable();public static void Register<T>(string key){if (!_RegisterWindow.Contains(key)){_RegisterWindow.Add(key, typeof(T));}}public static void Register(string key, Type t){if (!_RegisterWindow.Contains(key)){_RegisterWindow.Add(key, t);}}public static void Remove(string key){if (_RegisterWindow.ContainsKey(key)){_RegisterWindow.Remove(key);}}public static void Show(string key, object VM){if (_RegisterWindow.ContainsKey(key)){Window win = (Window)Activator.CreateInstance((Type)_RegisterWindow[key]);win.DataContext = VM;win.Show();}}
}

该管理器通过哈希表对窗体的注册、移除管理,同时实现Show方法窗体实例化等;使用方式此处就介绍两种最常用的方式:

(1) 直接在窗体后端调用:

public WpfLogin()
{InitializeComponent();WindowManager.Register<MainWindow>("MainWindow");Init();
}/// <summary>
/// 实例化计时器
/// </summary>
private void Init()
{int count = 0, nTimer = 100;timer = new DispatcherTimer{//间隔秒数Interval = TimeSpan.FromMilliseconds(nTimer)};//间隔时触发事件timer.Tick += (s, e) =>{count++;timer.Stop();//执行登录逻辑WindowManager.Show("MainWindow", null);Close();};}

(2) 封装ICommand事件进行调用

具体封装方式,上文[VIewModel前端交互实现]处已经详细给出实现部分。XAML调用请见下文[附加事件绑定]处内容。

三、View前端实现

说到Windows Presentation Foundation (WPF)前端,我们不得不得到它的核心Api【FrameworkElement】:此类表示所提供的 WPF 框架级别实现基于 UIElement 定义的 WPF 核心级别 API。

FrameworkElement UIElement扩展并添加以下功能:

  • 布局系统定义: FrameworkElement 为定义为虚拟成员 UIElement的某些方法提供特定的 WPF 框架级实现。最值得注意的是, FrameworkElement 提供一个与派生类应替代的 WPF 框架级等效项。
  • 逻辑树: 一般 WPF 编程模型通常以元素树表示。 支持将元素树表示为逻辑树,并支持在标记中定义树是在级别实现的 FrameworkElement 。
  • 对象生存期事件: 在调用构造函数) 或首次加载到逻辑树中时,知道何时初始化元素 (通常很有用。 FrameworkElement 定义与对象生存期相关的多个事件,这些事件为涉及元素的代码隐藏操作(例如添加更多子元素)提供有用的挂钩。
  • 支持数据绑定和动态资源引用: 对数据绑定和资源的属性级支持由 DependencyProperty 类实现,并体现在属性系统中,但解析存储 Expression 为 (编程构造中存储的成员值的能力由) 实现 FrameworkElement。
  • 风格: FrameworkElement 定义 Style 属性。 但是, FrameworkElement 尚未定义对模板或支持修饰器的支持。 这些功能由控件类(如 Control 和 ContentControl)引入。

  • 更多动画支持: 某些动画支持已在 WPF 核心级别定义,但 FrameworkElement 通过实现 BeginStoryboard 和相关成员来扩展此支持。

详情请参考微软官方API文档:FrameworkElement 类 (System.Windows) | Microsoft Learn

1、交互行为

这里,笔者使用的是微软NuGet包:Microsoft.Xaml.Behaviors.Wpf,有助于精简对行为的定义与交互配置:

/// <summary>
/// 验证异常行为
/// </summary>
public class ValidationExceptionBehavior : Behavior<FrameworkElement>
{/// <summary>/// 记录异常的数量/// </summary>/// <remarks>/// 在一个页面里面,所有控件的验证错误信息都会传到这个类上,每个控制需不需要显示验证错误,需要分别记录/// </remarks>private Dictionary<UIElement, int> ExceptionCount;/// <summary>/// 缓存页面的提示装饰器/// </summary>private Dictionary<UIElement, NotifyAdorner> AdornerDict;/// <summary>/// 隐藏错误信息提示/// </summary>private void HideAdorner(UIElement element){if (AdornerDict.ContainsKey(element)){AdornerLayer adornerLayer = AdornerLayer.GetAdornerLayer(element);adornerLayer.Remove(AdornerDict[element]);AdornerDict.Remove(element);}}/// <summary>/// 显示错误信息提示/// </summary>private void ShowAdorner(UIElement element, string errorMessage){if (AdornerDict.ContainsKey(element)){AdornerDict[element].ChangeToolTip(errorMessage);}else{AdornerLayer adornerLayer = AdornerLayer.GetAdornerLayer(element);NotifyAdorner adorner = new NotifyAdorner(element, errorMessage);adornerLayer.Add(adorner);AdornerDict.Add(element, adorner);}}/// <summary>/// 获得行为所在窗口的DataContext/// </summary>private NotificationProperty GetValidationExceptionHandler(){if (AssociatedObject.DataContext is NotificationProperty){NotificationProperty handler = AssociatedObject.DataContext as NotificationProperty;return handler;}return null;}/// <summary>/// 当验证错误信息改变时,首先调用此函数/// </summary>private void OnValidationError(object sender, ValidationErrorEventArgs e){try{//错误信息发生改变的控件//插入<c:ValidationExceptionBehavior></c:ValidationExceptionBehavior>此语句的窗口的DataContext,也就是ViewModelNotificationProperty handler = GetValidationExceptionHandler();if (handler == null || !(e.OriginalSource is UIElement element)){return;}if (e.Action == ValidationErrorEventAction.Added){if (ExceptionCount.ContainsKey(element)){ExceptionCount[element]++;}else{ExceptionCount.Add(element, 1);}}else if (e.Action == ValidationErrorEventAction.Removed){if (ExceptionCount.ContainsKey(element)){ExceptionCount[element]--;}else{ExceptionCount.Add(element, -1);}}if (ExceptionCount[element] <= 0){HideAdorner(element);}else{ShowAdorner(element, e.Error.ErrorContent.ToString());}int TotalExceptionCount = 0;foreach (KeyValuePair<UIElement, int> kvp in ExceptionCount){TotalExceptionCount += kvp.Value;}handler.IsValid = TotalExceptionCount <= 0;//ViewModel里面的IsValid}catch (Exception ex){throw ex;}}protected override void OnAttached(){ExceptionCount = new Dictionary<UIElement, int>();AdornerDict = new Dictionary<UIElement, NotifyAdorner>();AssociatedObject.AddHandler(Validation.ErrorEvent, new EventHandler<ValidationErrorEventArgs>(OnValidationError));}
}

注意,目前最新版本仅支持到.NET 5.0的框架使用。

2、Adorner装饰器

装饰器是绑定到一个UIElement自定义FrameworkElement。 装饰器在装饰器层中呈现,它是始终位于装饰元素或装饰元素集合之上的呈现图面:装饰器呈现与装饰器绑定到的呈现 UIElement 无关。 装饰器通常使用位于装饰元素左上部的标准 2D 坐标原点,相对于其绑定到的元素进行定位。

详情请见微软官方Api文档:Adorner 类 (System.Windows.Documents) | Microsoft Learn

具体实现带惊叹号的提示框:

/// <summary>
/// 带有惊叹号的提示图形
/// </summary>
public class NotifyAdorner : Adorner
{//Visual对象有序集合private VisualCollection _visuals;//绘制区域private Canvas _canvas;//图像控件private Image _image;//轻型控件,用于显示少量流内容private TextBlock _toolTip;public NotifyAdorner(UIElement adornedElement, string errorMessage) : base(adornedElement){_visuals = new VisualCollection(this);_image = new Image(){Width = 16,Height = 16,Source = new BitmapImage(new Uri("/Resources/warning.png", UriKind.RelativeOrAbsolute))};_toolTip = new TextBlock() { Text = errorMessage };_image.ToolTip = _toolTip;_canvas = new Canvas();_canvas.Children.Add(_image);_visuals.Add(_canvas);}//获取此元素内可视子元素的数目protected override int VisualChildrenCount => _visuals.Count;//重写从子元素集合中返回指定索引处的子元素protected override Visual GetVisualChild(int index){return _visuals[index];}public void ChangeToolTip(string errorMessage){_toolTip.Text = errorMessage;}//实现装饰器的任何自定义度量行为protected override Size MeasureOverride(Size constraint){return base.MeasureOverride(constraint);}//为 FrameworkElement 派生类定位子元素并确定大小protected override Size ArrangeOverride(Size finalSize){_canvas.Arrange(new Rect(finalSize));_image.Margin = new Thickness(finalSize.Width + 3, 0, 0, 0);return base.ArrangeOverride(finalSize);}
}

3、XMAL设计

3.1 引用程序集

<!-- Microsoft.Xaml.Behaviors.Wpf程序集 -->
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
<!-- Mdel实体、ViewModel交互模型以及数据校验ValidationAttribute标识实现 -->
xmlns:c="clr-namespace:SmartCar.ViewModel.Common"
<!-- UserControl自定义控件 -->
xmlns:CustCtl="clr-namespace:SmartCar.BaseUI.CustCtl;assembly=SmartCar.BaseUI"
<!-- DataContext 上下文绑定 ViewModel模型 -->
xmlns:model="clr-namespace:SmartCar.ViewModel.Model"
d:DataContext="{d:DesignInstance Type=model:LoginViewModel}"

3.2  引用装饰器行为

<i:Interaction.Behaviors><c:ValidationExceptionBehavior></c:ValidationExceptionBehavior><c:WindowBehavior Close="{Binding ToClose}"/>
</i:Interaction.Behaviors>

3.3 属性绑定

<!-- Binding 属性名 -->
<!-- Mode=TwoWay:双向绑定通知模式 -->
<!-- UpdateSourceTrigger=PropertyChanged: 每当绑定目标属性发生更改时,都会更新绑定源 -->
<!-- ValidatesOnExceptions=True: 是否包含ExceptionValidationRule 用于检查绑定源属性更新过程中抛出的异常的规则 -->
<!-- ValidatesOnDataErrors=True: 检查由源对象的 IDataErrorInfo 实现所引发的错误 -->
<!-- NotifyOnValidationError=True: 是否对绑定对象引发 Error 附加事件 -->
<TextBox x:Name="tBoxLogno" Text="{Binding Logno,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged,ValidatesOnExceptions=True, ValidatesOnDataErrors=True, NotifyOnValidationError=True}" />
<TextBox x:Name="tUserna" Text="{Binding Userna,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged,ValidatesOnExceptions=True, ValidatesOnDataErrors=True, NotifyOnValidationError=True}"/><CustCtl:CustPasswordBox x:Name="tBoxPassword" FontSize="14" MaxLength="10"Password="{Binding Password,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged,ValidatesOnExceptions=True, ValidatesOnDataErrors=True, NotifyOnValidationError=True}"Height="22" Width="150" Margin="0,8,180,8" >
</CustCtl:CustPasswordBox>

3.4 附加事件绑定

<!-- IsValid: ViewModel属性绑定 -->
<Button x:Name="LoginBtn" Content="登录" Width="100" Height="30" IsEnabled="{Binding IsValid}"><!-- 行为触发 --><i:Interaction.Triggers><i:EventTrigger EventName="Click"><!-- ICommand 命令事件绑定 --><c:EventCommand Command="{Binding LoginClick}" /></i:EventTrigger></i:Interaction.Triggers>
</Button>

微软官方Api技术文档请见:附加事件概述 - WPF .NET | Microsoft Learn

附加事件的应用场景非常广泛,其定义形式与绑定方式也有很多种。具体还有哪些,欢迎各位大神留言提出宝贵的意见思路。

最终效果

C# 浅谈基于Wpf下的MVVM模式的设计思想相关推荐

  1. 线上教学是计算机在什么方面的应用,浅谈线上线下混合式教学模式在计算机基础教学中应用...

    董兵波 摘要:随着时代的发展进步,科技拥有着至关重要的地位.因此,需要重视计算机教学,引进了线上线下混合式教学模式,望能够增强课堂教学效率和教学质量.该文主要阐述了线上线下混合式教学模式的基本概念以及 ...

  2. 计算机网络环境中学科教学,浅谈基于计算机网络环境下的农村小学的科学学科教育...

    浅谈基于计算机网络环境下的农村小学的科学学科教育 [内容摘要] <国家中长期教育改革和发展规划纲要(2010-2020年)>明确提出:加快教育信息化进程.重点加强农村学校信息基础建设,缩小 ...

  3. 浅谈 MVC、MVP 和 MVVM 架构模式

    2019独角兽企业重金招聘Python工程师标准>>> 谈谈 MVX 中的 Model 谈谈 MVX 中的 View 谈谈 MVX 中的 Controller 浅谈 MVC.MVP ...

  4. catia三维轴承_浅谈基于CATIA二次开发的单排四点接触球轴承三维设计论文

    浅谈基于CATIA二次开发的单排四点接触球轴承三维设计论文 一.概述 单排四点接触球转盘轴承是一种能够同时承受较大轴向负荷.径向负荷和倾覆力矩等综合载荷,集支承.旋转.传动.固定等多种功能于一身的特殊 ...

  5. 浅谈虚拟化技术下的云安全如何处置

    浅谈虚拟化技术下的云安全如何处置 近年来,云计算是目前非常热门的一个研究领域,其实它并不是一种全新的技术,而是许多技术的融合体,包括分布式计算.动态和拓展等各种各样的技术算法,而虚拟化技术是云计算里最 ...

  6. 浅谈基于SDL的2D纵版弹幕射击游戏相关经验心得

    浅谈基于SDL的2D游戏开发相关 前言:本人才疏学浅,大一新生一枚   写这篇文章的目是为了跟大家分享下自己的一些经验. 进入正题:首先本文这一切的一切都是基于SDL开发WINDOWS平台上的游戏.如 ...

  7. 浅谈全局视角下的设计模式

    写在前面: 以下内容,更多的是自己的思考总结,不可避免出现有争议的地方,请谨慎食用. 浅谈全局视角下的设计模式 1.业务开发经常使用的设计模式有哪些? 2.为什么有些设计模式不常见呢? 3.为什么这些 ...

  8. 浅谈互联网时代下融媒技术现状

    浅谈互联网时代下融媒技术现状 摘要:近年来,我国数字技术的迅速发展使得媒体技术在"互联网+"时代下不断发展融合,形成了如今的融合媒体技术.新兴融媒技术的发展给广播电视行业带来了新的 ...

  9. 集 承 享——浅谈基于内容的全链档案管理与服务

    2022年4月16日,由中国人民大学电子文件管理研究中心.中国人民大学信息资源管理学院主办的第十二届"中国电子文件管理论坛"成功在京举办.已举办十二届的该论坛是中国电子文件管理领域 ...

最新文章

  1. Can't connect to local MySQL server through socket ‘/var/run/mysqld/mysqld.sock‘ (2)解决思路
  2. PreparedStatement和Statement比较
  3. jquery对radio的操作汇总
  4. 面试题 16.18. Pattern Matching LCCI
  5. 将一个datetime的now转换为只有日期的_不要眨眼!中英文、大小写转换,一秒就搞定!
  6. Nmap的高级扫描(脚本)
  7. php 验证真实姓名,支付宝转账到支付宝 验证真实姓名
  8. 菜鸟教程工具(三)——Maven自己主动部署Tomcat
  9. 织梦(安装,模板,基本标签)
  10. PAT1030.——完美数列
  11. 高一计算机教学,高一信息技术教学计划参考
  12. Ubuntu中解决机箱前置耳机没声音
  13. 微软专利技术介绍系列课程
  14. ICCV 2021 | 性能炸裂的通道剪枝算法ResRep(Keras复现)
  15. 正则去掉首尾空格以及首尾的
  16. Eagle设计师必备利器管理工具
  17. poj2248 DFS+剪枝 or BFS
  18. 运动用品品牌排行榜,2022年最值得买的运动装备
  19. 设计LDO电路需考虑因素
  20. MATLAB中常用到的符号汇总(持续更新)

热门文章

  1. ​秋招上岸,机械转码经历和面经​
  2. Shiro 入门笔记,整合SpringBoot,Redis
  3. 数据恢复软件从iOS恢复Safari浏览记录
  4. box2d的角色邹形
  5. 阿里云天池——SQL训练计划_Task3
  6. Python中的排列和组合
  7. 用python进别人qq_采用python实现简单QQ单用户机器人的方法
  8. Unity场景渲染之自发光材质
  9. 计算机考证一级一般多少钱
  10. 《Centos的下载详细步骤》