wpfmvvm从入门到精通8:数据验证(代码片段)

lonelyxmas lonelyxmas     2023-03-13     198

关键词:

原文:WPF MVVM从入门到精通8:数据验证

WPF MVVM从入门到精通1:MVVM模式简介

WPF MVVM从入门到精通2:实现一个登录窗口

WPF MVVM从入门到精通3:数据绑定

WPF MVVM从入门到精通4:命令和事件

WPF MVVM从入门到精通5:PasswordBox的绑定

WPF MVVM从入门到精通6:RadioButton等一对多控件的绑定

WPF MVVM从入门到精通7:关闭窗口和打开新窗口

WPF MVVM从入门到精通8:数据验证

完整示例代码下载LoginDemo

到目前为止,登录窗口的基本功能似乎都完成了。但我们知道,很多时候用户名的格式是有要求的,例如是只有字母数字下划线,或者字数有限制。这要求我们在登录之前,验证输入内容的正确性。在这一节,我们需要验证用户名和密码的正确性,如果上面两个框的输入非法,禁用登录按钮。

在数据验证错误的时候,我们显示一个叹号在输入框的旁边,如下图所示:

技术图片

数据验证的方法有很多,我们使用了一种比较优雅的。

首先定义一些验证属性:

using System.ComponentModel.DataAnnotations;

namespace LoginDemo.ViewModel.Login

    public class NotEmptyCheck : ValidationAttribute
    
        public override bool IsValid(object value)
        
            var name = value as string;
            if (string.IsNullOrEmpty(name))
            
                return false;
            
            return true;
        

        public override string FormatErrorMessage(string name)
        
            return "不能为空";
        
    

    public class UserNameExists : ValidationAttribute
    
        public override bool IsValid(object value)
        
            var name = value as string;
            if (name.Contains("abc"))
            
                return true;
            
            return false;
        

        public override string FormatErrorMessage(string name)
        
            return "用户名必须包含abc";
        
    

第一个验证属性要求宿主的内容不能为空,第二个验证属性要求内容必须含有abc这个字符串。

然后我们又要用到Behavior了。当绑定的内容校验出异常后,它会一起冒泡,只到Window。这时候,Window的Behavior接收到异常,做出相应的处理。

using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Interactivity;

namespace LoginDemo.ViewModel.Common

    /// <summary>
    /// 验证异常行为
    /// </summary>
    public class ValidationExceptionBehavior : Behavior<FrameworkElement>
    
        /// <summary>
        /// 记录异常的数量
        /// </summary>
        /// <remarks>在一个页面里面,所有控件的验证错误信息都会传到这个类上,每个控制需不需要显示验证错误,需要分别记录</remarks>
        private Dictionary<UIElement, int> ExceptionCount;
        /// <summary>
        /// 缓存页面的提示装饰器
        /// </summary>
        private Dictionary<UIElement, NotifyAdorner> AdornerDict;

        protected override void OnAttached()
        
            ExceptionCount = new Dictionary<UIElement, int>();
            AdornerDict = new Dictionary<UIElement, NotifyAdorner>();

            this.AssociatedObject.AddHandler(Validation.ErrorEvent, new EventHandler<ValidationErrorEventArgs>(OnValidationError));
        

        /// <summary>
        /// 当验证错误信息改变时,首先调用此函数
        /// </summary>
        private void OnValidationError(object sender, ValidationErrorEventArgs e)
        
            try
            
                var handler = GetValidationExceptionHandler();//插入<c:ValidationExceptionBehavior></c:ValidationExceptionBehavior>此语句的窗口的DataContext,也就是ViewModel
                var element = e.OriginalSource as UIElement;//错误信息发生改变的控件
                if (handler == null || element == null)
                
                    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;
            
        

        /// <summary>
        /// 获得行为所在窗口的DataContext
        /// </summary>
        private NotificationObject GetValidationExceptionHandler()
        
            if (this.AssociatedObject.DataContext is NotificationObject)
            
                var handler = this.AssociatedObject.DataContext as NotificationObject;

                return handler;
            

            return null;
        

        /// <summary>
        /// 显示错误信息提示
        /// </summary>
        private void ShowAdorner(UIElement element, string errorMessage)
        
            if (AdornerDict.ContainsKey(element))
            
                AdornerDict[element].ChangeToolTip(errorMessage);
            
            else
            
                var adornerLayer = AdornerLayer.GetAdornerLayer(element);
                NotifyAdorner adorner = new NotifyAdorner(element, errorMessage);
                adornerLayer.Add(adorner);
                AdornerDict.Add(element, adorner);
            
        

        /// <summary>
        /// 隐藏错误信息提示
        /// </summary>
        private void HideAdorner(UIElement element)
        
            if (AdornerDict.ContainsKey(element))
            
                var adornerLayer = AdornerLayer.GetAdornerLayer(element);
                adornerLayer.Remove(AdornerDict[element]);
                AdornerDict.Remove(element);
            
        
    

这里异常的处理方式是显示我们最开始戴图的叹号图形。这个图形由NotifyAdnoner完成显示:

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Media;
using System.Windows.Media.Imaging;

namespace LoginDemo.ViewModel.Common

    /// <summary>
    /// 带有感叹号的提示图形
    /// </summary>
    public class NotifyAdorner : Adorner
    
        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("/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
        
            get
            
                return _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);
        

        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);
        
    

我们的ViewModel也要对数据验证做出支持。由于我们先前让ViewModel继承了NotificationObject,它并不是一个接口,我们不能继承两个类。所以,我们在NotificationObject里面加入验证有内容(虽然这样不太好)。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;

namespace LoginDemo.ViewModel.Common

    public abstract class NotificationObject : 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

        #region 数据验证

        public string Error
        
            get  return ""; 
        

        public string this[string columnName]
        
            get
            
                var vc = new ValidationContext(this, null, null);
                vc.MemberName = columnName;
                var res = new List<ValidationResult>();
                var result = Validator.TryValidateProperty(this.GetType().GetProperty(columnName).GetValue(this, null), vc, res);
                if (res.Count > 0)
                
                    return string.Join(Environment.NewLine, res.Select(r => r.ErrorMessage).ToArray());
                
                return string.Empty;
            
        

        /// <summary>
        /// 页面中是否所有控制数据验证正确
        /// </summary>
        public virtual bool IsValid  get; set; 

        #endregion
    

至此,准备就绪。我们修改ViewModel里面的UserName和Password属性:

/// <summary>
/// 用户名
/// </summary>
[NotEmptyCheck]
[UserNameExists]
public string UserName

    get
    
        return obj.UserName;
    
    set
    
        obj.UserName = value;
        this.RaisePropertyChanged("UserName");
    


/// <summary>
/// 密码
/// </summary>
[NotEmptyCheck]
public string Password

    get
    
        return obj.Password;
    
    set
    
        obj.Password = value;
        this.RaisePropertyChanged("Password");
    

没错,就是加了头上中括号的内容。这样的话,UserName就被要求非空和包含abc,而密码则被要求非空。由于我们在NotificationObject里加入了IsValid虚属性,还必须实现一下:

/// <summary>
/// 数据填写正确
/// </summary>
public override bool IsValid

    get
    
        return obj.IsValid;
    
    set
    
        if (value == obj.IsValid)
        
            return;
        
        obj.IsValid = value;
        this.RaisePropertyChanged("IsValid");
    

这个IsValid的设置是在ValidationExceptionBehavior里完成的。登录按钮只要绑定这个属性,就能在出现验证异常时,变成禁用。

我们修改XAML文件的用户名、密码和登录按钮:

<TextBox Grid.Row="0" Grid.Column="1" Margin="5" Text="Binding UserName,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged, ValidatesOnExceptions=True, ValidatesOnDataErrors=True, NotifyOnValidationError=True"/>

<PasswordBox Grid.Row="1" Grid.Column="1" Margin="5" c:PasswordBoxHelper.Password="Binding Password,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged,ValidatesOnExceptions=True,ValidatesOnDataErrors=True,NotifyOnValidationError=True">
    <i:Interaction.Behaviors>
        <c:PasswordBoxBehavior/>
    </i:Interaction.Behaviors>
</PasswordBox>

<Button Grid.Row="3" Grid.ColumnSpan="2" Content="登录" Width="200" Height="30" IsEnabled="Binding IsValid">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="Click">
            <c:EventCommand Command="Binding LoginClick"/>
        </i:EventTrigger>
    </i:Interaction.Triggers>
</Button>

窗口刚打开的时候是这样的,登录按钮被禁用:

技术图片

当数据都输入正确,登录按钮被启用:

技术图片

至此,登录窗口的所有功能就介绍完了。也恭喜你,你已经能熟练地使用MVVM模式了。

wpfmvvm从入门到精通6:radiobutton等一对多控件的绑定(代码片段)

原文:WPFMVVM从入门到精通6:RadioButton等一对多控件的绑定 ?WPFMVVM从入门到精通1:MVVM模式简介WPFMVVM从入门到精通2:实现一个登录窗口WPFMVVM从入门到精通3:数据绑定WPFMVVM从入门到精通4:命令和事件WPFMVVM从入门到... 查看详情

wpfmvvm从入门到精通3:数据绑定

原文:WPFMVVM从入门到精通3:数据绑定 ?WPFMVVM从入门到精通1:MVVM模式简介WPFMVVM从入门到精通2:实现一个登录窗口WPFMVVM从入门到精通3:数据绑定WPFMVVM从入门到精通4:命令和事件WPFMVVM从入门到精通5:PasswordBox的绑... 查看详情

wpfmvvm从入门到精通7:关闭窗口和打开新窗口

原文:WPFMVVM从入门到精通7:关闭窗口和打开新窗口 WPFMVVM从入门到精通1:MVVM模式简介WPFMVVM从入门到精通2:实现一个登录窗口WPFMVVM从入门到精通3:数据绑定WPFMVVM从入门到精通4:命令和事件WPFMVVM从入门到精通5:... 查看详情

wpfmvvm从入门到精通3:数据绑定

我们前面已经说过,现在后端和前端可以分头行事了。我们先来看看后端要做的事情。对应于用户名输入框,ViewModel里面应该有一个相应的对象。当这个对象状态发生改变时,需要向View发出一个通知。因为所有的属性都要做这... 查看详情

wpfmvvm从入门到精通1:mvvm模式简介

刚开始接触和使用MVVM模式的时候,就有一种感觉:哇,实现这么一丁点的功能,竟然要写这么多代码,太麻烦了吧!但是后来当我熟悉了这种模式之后,感觉就变成了:哇,还是这么麻烦。没错,使用MVVM模式的确要在项目中增... 查看详情

wpfmvvm从入门到精通4:命令和事件

这一部分我们要做的事情,是把点击登录按钮的事件也在ViewModel里实现。若不是用MVVM模式,可能XAML文件里是这样的:<ButtonGrid.Row="3"Grid.ColumnSpan="2"Content="登录"Width="200"Height="30"Click="Button_Click"/>而跟XAML文件相关的CS文件里则... 查看详情

wpfmvvm从入门到精通2:实现一个登录窗口

我们究竟要做一个怎样的东西呢?直接上图:这看起来比较简单,但把这个登录窗口做完,MVVM的入门就基本完成了。(为什么登录界面要选择性别这么奇怪?无非是因为RadioButton的绑定也是一个课题)很多教程都是举一个小例子... 查看详情

oracle从入门到精通系列讲解-总目录(代码片段)

总目录欢迎大家来到Lucifer三思而后行的《Oracle从入门到精通系列》,开始前博主先列出Oracle学习的大纲,同时这也可以作为大家学习Oracle的参考。下面蓝字都是传送门,点击进入即可:学前必读Oracle从入门到精通... 查看详情

一篇文章教你从入门到精通google指纹验证功能(代码片段)

本文首发于vivo互联网技术微信公众号?链接:https://mp.weixin.qq.com/s/EHomjBy4Tvm8u962J6ZgsA作者:SunDaxiangGoogle从Android6.0开始,提供了开放的指纹识别相关API,通过此篇文章可以帮助开发者接入指纹验证的基础功能,并且提供了系统应用... 查看详情

vuevuejs从入门到精通-邂逅vuejs(代码片段)

学习视频来源:B站《Vue、Vuejs从入门到精通》个人在视频学习过程中也同步完成课堂练习等,现将授课材料与个人笔记分享出来。   <!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"><title... 查看详情

vuejs从入门到精通(代码片段)

尚硅谷Vue2.0+Vue3.0全套教程,全网最新最强vuejs从入门到精通001课程简介002vue简介003Vue官网使用指南004搭建Vue开发环境005Hello小案例006分析Hello案例007模板语法008数据绑定009el与data的两种写法010理解MVVM011Object.define013Vue中的数... 查看详情

phoenix从入门到精通(代码片段)

 第一章、phoenix入门简介1.Phoenix定义Phoenix最早是saleforce的一个开源项目,后来成为Apache基金的顶级项目。Phoenix是构建在HBase上的一个SQL层,能让我们用标准的JDBCAPIs而不是HBase客户端APIs来创建表,插入数据和对HBase数据进行查... 查看详情

vuevuejs从入门到精通-组件化开发(代码片段)

学习视频来源:B站《Vue、Vuejs从入门到精通》 个人在视频学习过程中也同步完成课堂练习等,现将授课材料与个人笔记分享出来。<!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"><title>... 查看详情

es6从入门到精通#11:map数据类型(代码片段)

说明ES6从入门到精通系列(全23讲)学习笔记。Map类型Map类型是键值对的有序列表,键和值是任意类型。letkaimo=newMap();console.log(kaimo)赋值kaimo.set("name","kaimo313");kaimo.set("age",666);console.lo 查看详情

neo4j图数据库从入门到精通(代码片段)

目录第一章:介绍Neo4j是什么Neo4j的特点Neo4j的优点第二章:安装1.环境2.下载3.开启远程访问4.启动第三章:CQL1.CQL简介2.Neo4jCQL命令/条款3.Neo4jCQL函数4.Neo4jCQL数据类型第四章:命令1.CREATE创建2.MATCH查询3.RETURN返回4.关系基础5.WHERE子... 查看详情

mybatis从入门到精通—mybatis基础知识和快速入门(代码片段)

Mybatis简介原始jdbc操作(查询数据)原始jdbc操作(插入数据)原始jdbc操作的分析原始jdbc开发存在的问题如下:①数据库连接创建、释放频繁造成系统资源浪费从而影响系统性能②sql语句在代码中硬编码,造成代码不易维护,实... 查看详情

es6从入门到精通#10:set集合数据类型(代码片段)

说明ES6从入门到精通系列(全23讲)学习笔记。Set集合集合:表示无重复值的有序列表letkaimo=newSet();console.log(kaimo)添加元素letkaimo=newSet();kaimo.add(3);kaimo.add("1");kaimo.add(3);kaimo.add([3,1,3])con 查看详情

es6从入门到精通#22:类的继承(代码片段)

说明ES6从入门到精通系列(全23讲)学习笔记。类的继承使用关键字extends<!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"><metahttp-equiv="X-UA-Compatibl 查看详情