目录
一、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示意图如下所示:
在笔者理解看来,它也算是某种意义上的实体,因为完全可以简化真正意义上实体部分(仅需涵盖需与前端交互的字段信息)及可补充需交互时的辅助属性字段,也可以完全以实际实体结构为主;
Example:
public class LoginModel
{
///
/// 登录账号
///
public string Logno { get; set; }
///
/// 用户名称
///
public string Userna { get; set; }
///
/// 账号密码
///
public string Pwd { get; set; }
///
/// 数据填写正确(可视属性字段)
///
public bool IsValid { get; set; }
}
Model实际发挥的作用,当然是贯穿DAL层的读写以及与ViewModel层的交互作用;具体对DAL层的交互读写在此就不具体展开了。
在这里,可能也有人和笔者同样有着一个好奇的问题,为什么它只称为MVVM,而为什么不称为MVMV?本文开头就已经明确MVMM本质就是由MVC演变而来,由Controller演变成ViewModel视图模型。如果还有人争议为什么MVC不称为MCV,这个问题我想只能去问问MVC的创造者(Trygve Reenskaugh和Adele Goldberg两位大神)了,当然仍感兴趣的朋友可参考:从MVC到现代Web框架 | 码农网
言归正传,ViewModel是MVVM核心思想,主要承担的就是两件事:
直白的理解,就是消息收发室的概念。通过INotifyPropertyChanged接口实现与客户端属性变更通知。
public class NotificationProperty : INotifyPropertyChanged, IDataErrorInfo
{
#region 属性发生改变通知
public event PropertyChangedEventHandler PropertyChanged;
///
/// 发起通知
///
/// 属性名
public void RaisePropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
public class NotificationProperty : INotifyPropertyChanged, IDataErrorInfo
{
#region 数据验证
protected virtual string GetValidationErrors()
{
//验证上下文类实例化
var vc = new ValidationContext(this, null, null);
//验证请求的结果容器
var vResults = new List();
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();
//检查确定指定的对象是否有效
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);
///
/// 页面中是否所有控制数据验证正确
///
public virtual bool IsValid { get; set; }
#endregion
}
继承所有验证特性ValidationAttribute基类。
///
/// 检查字段是否为空
///
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 "不能为空";
}
}
///
/// 检查字段是否为数值
///
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 "账号名必须为纯数字字符!";
}
}
这里既采用预定义的验证标识符,同时将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;
///
/// 是否要关闭窗口
///
public bool ToClose
{
get => toClose;
set
{
toClose = value;
if (toClose) RaisePropertyChanged("ToClose");
}
}
///
/// 数据填写正确
///
public override bool IsValid
{
get => loginModel.IsValid;
set
{
loginModel.IsValid = value;
RaisePropertyChanged("IsValid");
}
}
private BaseCommand loginClick;
///
/// 登录事件
///
public BaseCommand LoginClick
{
get
{
if (loginClick == null)
{
loginClick = new BaseCommand(new Action
题外话,NuGet中MvvmLight包,实际上就是省去INotifyPropertyChanged属性通知基类和ICommand基类的一个框架包。
感兴趣的朋友可参考:走进WPF之MVVM完整案例 - 小六公子 - 博客园
///
/// 命令基类
///
public class BaseCommand : ICommand
{
private Func
注:在XAML UI前端中,Click事件实现的RoutedEventArgs事件,实际上也是经ICommand接口从而实现的。
感兴趣的朋友可参考:WPF Command - Clingingboy - 博客园
WPF 命令(RoutedCommand自定义命令,实现 ICommand 接口自定义命令)。推荐使用实现 ICommand 接口自定义命令_tiz198183的博客-CSDN博客_routedcommand
///
/// 窗口管理器
///
public static class WindowManager
{
private static Hashtable _RegisterWindow = new Hashtable();
public static void Register(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");
Init();
}
///
/// 实例化计时器
///
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调用请见下文[附加事件绑定]处内容。
说到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
这里,笔者使用的是微软NuGet包:Microsoft.Xaml.Behaviors.Wpf,有助于精简对行为的定义与交互配置:
///
/// 验证异常行为
///
public class ValidationExceptionBehavior : Behavior
{
///
/// 记录异常的数量
///
///
/// 在一个页面里面,所有控件的验证错误信息都会传到这个类上,每个控制需不需要显示验证错误,需要分别记录
///
private Dictionary ExceptionCount;
///
/// 缓存页面的提示装饰器
///
private Dictionary AdornerDict;
///
/// 隐藏错误信息提示
///
private void HideAdorner(UIElement element)
{
if (AdornerDict.ContainsKey(element))
{
AdornerLayer adornerLayer = AdornerLayer.GetAdornerLayer(element);
adornerLayer.Remove(AdornerDict[element]);
AdornerDict.Remove(element);
}
}
///
/// 显示错误信息提示
///
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);
}
}
///
/// 获得行为所在窗口的DataContext
///
private NotificationProperty GetValidationExceptionHandler()
{
if (AssociatedObject.DataContext is NotificationProperty)
{
NotificationProperty handler = AssociatedObject.DataContext as NotificationProperty;
return handler;
}
return null;
}
///
/// 当验证错误信息改变时,首先调用此函数
///
private void OnValidationError(object sender, ValidationErrorEventArgs e)
{
try
{
//错误信息发生改变的控件
//插入 此语句的窗口的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 kvp in ExceptionCount)
{
TotalExceptionCount += kvp.Value;
}
handler.IsValid = TotalExceptionCount <= 0;//ViewModel里面的IsValid
}
catch (Exception ex)
{
throw ex;
}
}
protected override void OnAttached()
{
ExceptionCount = new Dictionary();
AdornerDict = new Dictionary();
AssociatedObject.AddHandler(Validation.ErrorEvent, new EventHandler(OnValidationError));
}
}
注意,目前最新版本仅支持到.NET 5.0的框架使用。
装饰器是绑定到一个UIElement自定义FrameworkElement。 装饰器在装饰器层中呈现,它是始终位于装饰元素或装饰元素集合之上的呈现图面:装饰器呈现与装饰器绑定到的呈现 UIElement 无关。 装饰器通常使用位于装饰元素左上部的标准 2D 坐标原点,相对于其绑定到的元素进行定位。
详情请见微软官方Api文档:Adorner 类 (System.Windows.Documents) | Microsoft Learn
具体实现带惊叹号的提示框:
///
/// 带有惊叹号的提示图形
///
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);
}
}
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
xmlns:c="clr-namespace:SmartCar.ViewModel.Common"
xmlns:CustCtl="clr-namespace:SmartCar.BaseUI.CustCtl;assembly=SmartCar.BaseUI"
xmlns:model="clr-namespace:SmartCar.ViewModel.Model"
d:DataContext="{d:DesignInstance Type=model:LoginViewModel}"
微软官方Api技术文档请见:附加事件概述 - WPF .NET | Microsoft Learn
附加事件的应用场景非常广泛,其定义形式与绑定方式也有很多种。具体还有哪些,欢迎各位大神留言提出宝贵的意见思路。
最终效果