目录
先套用下老话,什么是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 - 博客园
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,也就是ViewModel
NotificationProperty 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
附加事件的应用场景非常广泛,其定义形式与绑定方式也有很多种。具体还有哪些,欢迎各位大神留言提出宝贵的意见思路。
最终效果