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


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

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

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

    首先定义一些验证属性:

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

  • 相关阅读:
    RedHat 7.0及CentOS 7.0禁止Ping的三种方法
    修改WordPress后台默认登陆地址提高网站安全性
    解决WordPress用户名密码都正确但点击登陆就清空密码的问题
    Windows上使用Git托管代码到Coding
    使用Coding Pages托管网站
    Windows上设置Mozilla Thunderbird邮件客户端后台运行
    在VirtualBox中安装BlackArch Linux
    关于XML学习
    软件工程课程设计团队项目总结与项目报告
    详细设计
  • 原文地址:https://www.cnblogs.com/ljdong7/p/12058068.html
Copyright © 2020-2023  润新知