WPF MVVM從入門到精通8:數據驗證

原文: WPF MVVM從入門到精通8:數據驗證

WPF MVVM從入門到精通1:MVVM模式簡介html

WPF MVVM從入門到精通2:實現一個登陸窗口canvas

WPF MVVM從入門到精通3:數據綁定緩存

WPF MVVM從入門到精通4:命令和事件ide

WPF MVVM從入門到精通5:PasswordBox的綁定函數

WPF MVVM從入門到精通6:RadioButton等一對多控件的綁定this

WPF MVVM從入門到精通7:關閉窗口和打開新窗口spa

WPF MVVM從入門到精通8:數據驗證.net

完整示例代碼下載LoginDemocode

到目前爲止,登陸窗口的基本功能彷佛都完成了。但咱們知道,不少時候用戶名的格式是有要求的,例如是隻有字母數字下劃線,或者字數有限制。這要求咱們在登陸以前,驗證輸入內容的正確性。在這一節,咱們須要驗證用戶名和密碼的正確性,若是上面兩個框的輸入非法,禁用登陸按鈕。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模式了。

相關文章
相關標籤/搜索