WPF MVVM(Caliburn.Micro) 數據驗證#
前文中僅是WPF驗證中的一種,我們暫且稱之為View端的驗證(因為其驗證規是寫在Xaml文件中的)。
還有一種我們稱之為Model端驗證,Model通過繼承IDataErrorInfo接口來實現,這個還沒研究透,后面補上。
今天的主要內容是MVVM下的數據驗證,主要使用View端驗證,需求如下:

- 1.對姓名的非空驗證,驗證錯誤控件后邊應該有感嘆號提示,感嘆號的ToolTip應該有具體錯誤的信息
- 2.對姓名的非空驗證不通過的話,確定 按鈕應該禁用
對於1,控件本身驗證不通過會有一個紅色的邊框,后面的感嘆號我們用Adorner來實現,且看這篇
不好處理的是2,為什么呢?在Mvvm中,我們故意分離View和VM,View只負責顯示,VM負責各種交互邏輯,VM應該感知不到View的存在,而各種驗證(不管你是VIew端驗證還是Model端驗證)產生的Validation.ErrorEvent冒泡事件只會沿着邏輯樹上走,我們就是需要監聽這個事件,有了這個事件我們的VM才能知道驗證不通過,從而修改屬性來達到禁用按鈕的目的。也就是說View和VM之間除了傳統的Binding和Command之外,還應該有一條通道,從View通知到VM的通道。
這里補充一下Validation.ErrorEvent,被驗證的控件如下
<TextBox Grid.Row="0" Grid.Column="1" Height="25" VerticalContentAlignment="Center"> <TextBox.Text> <Binding Path="IdentityName" UpdateSourceTrigger="PropertyChanged" NotifyOnValidationError="True"> <Binding.ValidationRules> <validationRules:RequiredRule ValidatesOnTargetUpdated="True"></validationRules:RequiredRule> </Binding.ValidationRules> </Binding> </TextBox.Text> </TextBox>需要把
NotifyOnValidationError設置為True才能夠產生Validation.ErrorEvent事件,我們一般在所有要驗證控件的最外層來注冊監聽這個事件
這是基本思路,實現的方式就很多,就看誰的優雅。最直接的就是在View的后台代碼中直接注冊監聽這個事件,然后驗證事件觸發的時候,將
這個View的DataContext轉成我們對應的ViewModel,就可以直接操作了。這樣做也行,但是后面的項目基本會累死,因為這些都是體力活。
各種百度(百度基本沒什么用),最后使用Bing搜索到一個老外寫的代碼
非常6,參考之后,決定改造一下。
先講一下思路,繼承WPF中的Behavior,取名ValidationExceptionBehavior,這個Behavior負責注冊監聽Validation.ErrorEvent事件,並且將驗證結果通知到ViewModel
,要能夠通用的話,必然ValidationExceptionBehavior不知道ViewModel的具體類型,於是我們設計了一個接口IValidationExceptionHandler,需要接受到來自view的驗證結果
的ViewModel就需要實現這個接口。所以對於View,我們只需要在最外層容器加入下面一行代碼
<i:Interaction.Behaviors>
<behaviors:ValidationExceptionBehavior></behaviors:ValidationExceptionBehavior>
</i:Interaction.Behaviors>
對於ViewModel,我們只需要實現接口
[Export]
public class BaseInfoConfigViewModel : Screen, IValidationExceptionHandler
{
public bool IsValid
{
get
{
return _isValid;
}
set
{
if (value == _isValid)
return;
_isValid = value;
NotifyOfPropertyChange(() => IsValid);
}
}
}
該接口只有一個屬性,就是IsValid,驗證是否有效,通過這個屬性,就可以在ViewModel中為所欲為了。好吧,講這么多不如上代碼,上Demo。
IValidationExceptionHandler.cs##
/// <summary>
/// 驗證異常處理接口,由VM來繼承實現
/// </summary>
public interface IValidationExceptionHandler
{
/// <summary>
/// 是否有效
/// </summary>
bool IsValid
{
get;
set;
}
}
ValidationExceptionBehavior.cs##
/// <summary>
/// 驗證行為類,可以獲得附加到的對象
/// </summary>
public class ValidationExceptionBehavior : Behavior<FrameworkElement>
{
#region 字段
/// <summary>
/// 錯誤計數器
/// </summary>
private int _validationExceptionCount = 0;
private Dictionary<UIElement, NotifyAdorner> _adornerCache;
#endregion
#region 方法
#region 重寫方法
/// <summary>
/// 附加對象時
/// </summary>
protected override void OnAttached()
{
_adornerCache = new Dictionary<UIElement, NotifyAdorner>();
//附加對象時,給對象增加一個監聽驗證錯誤事件的能力,注意該事件是冒泡的
this.AssociatedObject.AddHandler(Validation.ErrorEvent, new EventHandler<ValidationErrorEventArgs>(this.OnValidationError));
}
#endregion
#region 私有方法
#region 獲取實現接口的對象
/// <summary>
/// 獲取對象
/// </summary>
/// <returns></returns>
private IValidationExceptionHandler GetValidationExceptionHandler()
{
if (this.AssociatedObject.DataContext is IValidationExceptionHandler)
{
var handler = this.AssociatedObject.DataContext as IValidationExceptionHandler;
return handler;
}
return null;
}
#endregion
#region 顯示Adorner
/// <summary>
/// 顯示Adorner
/// </summary>
/// <param name="element"></param>
/// <param name="errorMessage"></param>
private void ShowAdorner(UIElement element, string errorMessage)
{
NotifyAdorner adorner = null;
//先去緩存找
if (_adornerCache.ContainsKey(element))
{
adorner = _adornerCache[element];
//找到了,修改提示信息
adorner.ChangeToolTip(errorMessage);
}
//沒有找到,那就New一個,加入到緩存
else
{
adorner = new NotifyAdorner(element, errorMessage);
_adornerCache.Add(element, adorner);
}
//將Adorner加入到
if (adorner != null)
{
var adornerLayer = AdornerLayer.GetAdornerLayer(element);
adornerLayer.Add(adorner);
}
}
#endregion
#region 移除Adorner
/// <summary>
/// 移除Adorner
/// </summary>
/// <param name="element"></param>
private void HideAdorner(UIElement element)
{
//移除Adorner
if (_adornerCache.ContainsKey(element))
{
var adorner = _adornerCache[element];
var adornerLayer = AdornerLayer.GetAdornerLayer(element);
adornerLayer.Remove(adorner);
}
}
#endregion
#region 驗證事件方法
/// <summary>
/// 驗證事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnValidationError(object sender, ValidationErrorEventArgs e)
{
try
{
var handler = GetValidationExceptionHandler();
var element = e.OriginalSource as UIElement;
if (handler == null || element == null)
return;
if (e.Action == ValidationErrorEventAction.Added)
{
_validationExceptionCount++;
ShowAdorner(element, e.Error.ErrorContent.ToString());
}
else if (e.Action == ValidationErrorEventAction.Removed)
{
_validationExceptionCount--;
HideAdorner(element);
}
handler.IsValid = _validationExceptionCount == 0;
}
catch (Exception ex)
{
throw ex;
}
}
#endregion
#endregion
#endregion
}
NotifyAdorner.cs##
/// <summary>
/// 提示Adorner
/// </summary>
public class NotifyAdorner : Adorner
{
private VisualCollection _visuals;
private Canvas _canvas;
private Image _image;
private TextBlock _toolTip;
/// <summary>
/// 構造
/// </summary>
/// <param name="adornedElement"></param>
/// <param name="errorMessage"></param>
public NotifyAdorner(UIElement adornedElement, string errorMessage) : base(adornedElement)
{
_visuals = new VisualCollection(this);
BuildNotifyStyle(errorMessage);
_canvas = new Canvas();
_canvas.Children.Add(_image);
_visuals.Add(_canvas);
}
private void BuildNotifyStyle(string errorMessage)
{
_image = new Image()
{
Width = 20,
Height = 20,
Source = new BitmapImage(new Uri("你的圖片路徑", UriKind.Absolute))
};
_toolTip = new TextBlock() { FontSize = 14, Text = errorMessage };
_image.ToolTip = _toolTip;
}
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 + 2, 0, 0, 0);
return base.ArrangeOverride(finalSize);
}
}
當然,如有錯誤,請大家斧正。
