WPF MVVM从入门到精通2:实现一个登陆窗口canvas
WPF MVVM从入门到精通5:PasswordBox的绑定函数
WPF MVVM从入门到精通6:RadioButton等一对多控件的绑定this
到目前为止,登陆窗口的基本功能彷佛都完成了。但咱们知道,不少时候用户名的格式是有要求的,例如是只有字母数字下划线,或者字数有限制。这要求咱们在登陆以前,验证输入内容的正确性。在这一节,咱们须要验证用户名和密码的正确性,若是上面两个框的输入非法,禁用登陆按钮。orm
在数据验证错误的时候,咱们显示一个叹号在输入框的旁边,以下图所示:
数据验证的方法有不少,咱们使用了一种比较优雅的。
首先定义一些验证属性:
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模式了。