原文: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模式了。

WPF MVVM从入门到精通8:数据验证的更多相关文章

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

    原文:WPF MVVM从入门到精通6:RadioButton等一对多控件的绑定   WPF MVVM从入门到精通1:MVVM模式简介 WPF MVVM从入门到精通2:实现一个登录窗口 WPF MVVM ...

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

    原文:WPF MVVM从入门到精通7:关闭窗口和打开新窗口 WPF MVVM从入门到精通1:MVVM模式简介 WPF MVVM从入门到精通2:实现一个登录窗口 WPF MVVM从入门到精通3:数据绑定 ...

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

    原文:WPF MVVM从入门到精通5:PasswordBox的绑定   WPF MVVM从入门到精通1:MVVM模式简介 WPF MVVM从入门到精通2:实现一个登录窗口 WPF MVVM从入门到精通 ...

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

    原文:WPF MVVM从入门到精通3:数据绑定   WPF MVVM从入门到精通1:MVVM模式简介 WPF MVVM从入门到精通2:实现一个登录窗口 WPF MVVM从入门到精通3:数据绑定 WPF ...

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

    原文:WPF MVVM从入门到精通4:命令和事件   WPF MVVM从入门到精通1:MVVM模式简介 WPF MVVM从入门到精通2:实现一个登录窗口 WPF MVVM从入门到精通3:数据绑定 WP ...

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

    原文:WPF MVVM从入门到精通2:实现一个登录窗口   WPF MVVM从入门到精通1:MVVM模式简介 WPF MVVM从入门到精通2:实现一个登录窗口 WPF MVVM从入门到精通3:数据绑定 ...

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

    原文:WPF MVVM从入门到精通1:MVVM模式简介 WPF MVVM从入门到精通1:MVVM模式简介 WPF MVVM从入门到精通2:实现一个登录窗口 WPF MVVM从入门到精通3:数据绑定 W ...

  8. WPF/MVVM Quick Start Tutorial - WPF/MVVM 快速入门教程 -原文,翻译及一点自己的补充

    转载自 https://www.codeproject.com/articles/165368/wpf-mvvm-quick-start-tutorial WPF/MVVM Quick Start T ...

  9. AngularJS快速入门指南14:数据验证

    thead>tr>th, table.reference>tbody>tr>th, table.reference>tfoot>tr>th, table ...

随机推荐

  1. [Python WEB开发] 使用WSGI开发类Flask框架 (二)

    WSGI     Web服务器网关接口 WSGI主要规定了Web服务器如何与Web应用程序进行通信,以及如何将Web应用程序链接在一起来处理一个请求. wsgiref Python中的WSGI参考模块 ...

  2. 关于CUDA5之后cutil.h不可用的问题

    用CUDA进行GPU加速开发的同学在移植一些代码片段时应该都有过这样的经历,如果自己用的是高版本的CUDA,系统有时会报错:CANNOT FIND CUTIL.H.查看代码才发现原来是多了CUTIL. ...

  3. [转]DbHelper通用数据库访问帮助类

    之前我一直都是在博客园中查看各位高手的博文,确实学到了不少知识,不过比较少写博客,现在就把我自己在项目实施过程中使用到比较好的技术框架写出来,希望能让更多的人了解和学习. 通常我们在开发使用数据库访问 ...

  4. python 怎样构造字典格式的数据

    #dict()函数的使用 第一种方法l=[('name','xueli'),('age',12)]dd1=dict(l)print dd1#{'age': 12, 'name': 'xueli'} 第 ...

  5. Linux 嵌入式 开发环境 交叉编译安装

    1.安装 Ubuntu 系统 安装完毕,系统 提示 重启,这个时候 请拔掉U盘,进行重启 OK. 2.安装 NFS 服务 3.安装 openssh服务 4.开启openSSH服务 5.就可以使用 Wi ...

  6. EJB到底是什么?---通俗易懂,简单明了

    EJB到底是什么?   1. 我们不禁要问,什么是"服务集群"?什么是"企业级开发"? 既然说了EJB 是为了"服务集群"和"企业 ...

  7. objc单例的两种安全实现方案

    所有转出博客园,请您注明出处:http://www.cnblogs.com/xiaobajiu/p/4122034.html objc的单例的两种安全实现方案 首先应该知道单例的实现有两大类,一个是懒 ...

  8. Vue packages version conflicts 错误修复

    我们在使用Vue作为weex中的前端框架的开发过程中,某次 npm start 遇到了如下的错误: Vue packages version mismatch: - vue@2.5.16 - vue- ...

  9. OO第三次博客作业——规格

    OO第三次博客作业——规格 一.调研结果: 规格的历史: 引自博文链接:http://blog.sina.com.cn/s/blog_473d5bba010001x9.html 传统科学的特点是发现世 ...

  10. RandomAccessFile类——高效快捷地读写文件

    RandomAceessFile类 RandomAccessFile类是一个专门读写文件的类,封装了基本的IO流,在读写文件内容方面比常规IO流更方便.更灵活.但也仅限于读写文件,无法像IO流一样,可 ...