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