[WPF] 讓第一個資料驗證出錯(Validation.HasError)的控制元件自動獲得焦點
阿新 • • 發佈:2020-12-28
## 1. 需求
在上一篇文章 《[在 ViewModel 中讓資料驗證出錯(Validation.HasError)的控制元件獲得焦點](https://www.cnblogs.com/dino623/p/focus_controls_in_ViewModel.html)》中介紹瞭如何讓 Validation.HasError 的控制元件自動獲得焦點,之後引申了另一個問題:如果有多個 HasError 的控制元件,如何只讓第一個自動獲得焦點。
這需求比較常見,所以我試著解決這個問題,最終完成了一個 Demo,XAML 如下:
``` XML
```
為了實現這個功能用到了幾個入門知識,這篇文章講解如何組合這幾個入門知識實現需求:
- [Validation.Error 附加事件](https://docs.microsoft.com/zh-cn/dotnet/api/system.windows.controls.validation.error?view=net-5.0&WT.mc_id=WD-MVP-5003763)
- [WPF 中的樹](https://docs.microsoft.com/en-us/dotnet/desktop/wpf/advanced/trees-in-wpf?WT.mc_id=WD-MVP-5003763&view=netframeworkdesktop-4.8&WT.mc_id=WD-MVP-5003763)
- [附加屬性](https://docs.microsoft.com/zh-cn/dotnet/desktop/wpf/advanced/attached-properties-overview?view=netframeworkdesktop-4.8&WT.mc_id=WD-MVP-5003763)
## 2. Validation.Error 附加事件
為了實現自動獲得焦點這個需求,我們首先需要一個和資料驗證錯誤相關的事件通知。[Validation 類](https://docs.microsoft.com/zh-cn/dotnet/api/system.windows.controls.validation?view=net-5.0&WT.mc_id=WD-MVP-5003763) 提供了很多支援資料驗證的方法和附加屬性,其中這次用到的是 [Validation.Error 附加事件](https://docs.microsoft.com/zh-cn/dotnet/api/system.windows.controls.validation.error?view=net-5.0&WT.mc_id=WD-MVP-5003763),它在繫結元素遇到驗證錯誤時觸發。使用方式如下:
``` CS
Validation.AddErrorHandler(target, (s, e) =>
{
//some code
});
```
注意,為了使用這個事件,資料繫結中的 [NotifyOnValidationError](https://docs.microsoft.com/zh-cn/dotnet/api/system.windows.data.binding.notifyonvalidationerror?view=net-5.0&WT.mc_id=WD-MVP-5003763) 必須設定為 `true`:
``` XML
Text="{Binding Name, Mode=TwoWay, NotifyOnValidationError=True}"
```
## 3. WPF 中的樹
使用 [VisualTreeHelper](https://docs.microsoft.com/en-us/dotnet/api/system.windows.media.visualtreehelper?view=net-5.0&WT.mc_id=WD-MVP-5003763) 遍歷 VisualTree,再通過 [Validation.GetHasError](https://docs.microsoft.com/zh-cn/dotnet/api/system.windows.controls.validation.gethaserror?view=net-5.0&WT.mc_id=WD-MVP-5003763) 判斷元素是否具有 [ValidationError](https://docs.microsoft.com/zh-cn/dotnet/api/system.windows.controls.validationerror?view=net-5.0&WT.mc_id=WD-MVP-5003763),這樣就可以找出所有資料驗證錯誤的元素。我在以前的文章中提供了一個用於遍歷 VisualTree 的擴充套件方法類 [VisualTreeExtensions](https://www.cnblogs.com/dino623/p/VisualTreeExtensions.html),這次我直接使用它找出第一次資料驗證出錯的元素:
``` CS
var root = Window.GetWindow(target).Content as UIElement;
var errorElement = root.GetVisualDescendants().OfType().FirstOrDefault(u => Validation.GetHasError(u));
```
## 4. 附加屬性
附加屬性是由 XAML 定義的概念。 附加屬性旨在用作可在任何物件上設定的一類全域性屬性。通常來說附加屬性有兩種用法:純粹作為屬性值,或者在屬性值改變的回撥函式裡執行程式碼。而這次我兩種方式都有用到。
在上面的程式碼中,我先獲得要獲得焦點的控制元件的根節點元素,然後再找到第一次資料驗證出錯的元素。如果在結構複雜的 UI 中這個操作稍微有點耗時,而且說不定找到的是別的表單中的控制元件。這篇文章提到的“讓第一個 HasError 的元素獲得焦點”這個需求,通常還有一個隱含的條件:**同一個表單以內**。一般業務來說,同一個表單裡的輸入控制元件並不會太多,起碼 VisualTree 會比一整個 Window 的 VisualTree 簡單很多。所以需要用一個附加屬性,將表單的根節點標記出來。在這裡我參考 [Grid.IsSharedSizeScope 附加屬性](https://docs.microsoft.com/zh-cn/dotnet/api/system.windows.controls.grid.issharedsizescope?view=net-5.0&WT.mc_id=WD-MVP-5003763) 自定義了一個 `IsValidationScope` 屬性作為標識:
``` CS
public static bool GetIsValidationScope(DependencyObject obj) => (bool)obj.GetValue(IsValidationScopeProperty);
public static void SetIsValidationScope(DependencyObject obj, bool value) => obj.SetValue(IsValidationScopeProperty, value);
public static readonly DependencyProperty IsValidationScopeProperty =
DependencyProperty.RegisterAttached("IsValidationScope", typeof(bool), typeof(ValidationService), new PropertyMetadata(default(bool)));
```
在 XAML 中,將 StackPanel 標識為 ValidationScope:
``` XML
```
然後查詢表單根節點的程式碼修改成這樣:
``` CS
var root = target.GetVisualAncestors().OfType().FirstOrDefault(d => GetIsValidationScope(d));
if (root == null)
root = Window.GetWindow(target).Content as UIElement;
```
`IsValidationScope` 是純粹作為屬性值的附加屬性,我還需要定義另一個暑假屬性, 並在它的屬性值改變的回撥函式中執行上面的邏輯。完整程式碼如下:
``` CS
public static bool GetAutoFocusWhenValidationError(DependencyObject obj) => (bool)obj.GetValue(AutoFocusWhenValidationErrorProperty);
public static void SetAutoFocusWhenValidationError(DependencyObject obj, bool value) => obj.SetValue(AutoFocusWhenValidationErrorProperty, value);
public static readonly DependencyProperty AutoFocusWhenValidationErrorProperty =
DependencyProperty.RegisterAttached("AutoFocusWhenValidationError", typeof(bool), typeof(ValidationService), new PropertyMetadata(default(bool), OnAutoFocusWhenValidationErrorChanged));
private static void OnAutoFocusWhenValidationErrorChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
var oldValue = (bool)args.OldValue;
var newValue = (bool)args.NewValue;
if (newValue == oldValue || newValue == false)
return;
var target = obj as UIElement;
Validation.AddErrorHandler(target, (s, e) =>
{
var root = target.GetVisualAncestors().OfType().FirstOrDefault(d => GetIsValidationScope(d));
if (root == null)
root = Window.GetWindow(target).Content as UIElement;
var errorElement = root.GetVisualDescendants().OfType().FirstOrDefault(u => Validation.GetHasError(u));
if (errorElement != null && errorElement.IsKeyboardFocused == false)
errorElement.Focus();
});
}
```
在 `OnAutoFocusWhenValidationErrorChanged` 這個回撥函式裡面,我們可以拿到被 “附加”的元素 `target`,以及附加屬性的值。如果這個值為 `true` (在這種用法裡通常都是 `true`,類似一個簡單的 `Behavior`),則通過 `Validation.AddErrorHandler` 為 `target` 新增事件處理程式,當資料驗證出錯時找到表單範圍內第一個出錯的元素,如果它還沒有獲得焦點就執行 [Focus](https://docs.microsoft.com/zh-cn/dotnet/api/system.windows.uielement.focus?view=net-5.0&WT.mc_id=WD-MVP-5003763) 函式。
在 XAML 中,為了讓表單中所有元素都附加上這個行為,可以通過全域性樣式:
``` XML
```
## 5. 最後
這種做法需要每個資料繫結中的 [NotifyOnValidationError](https://docs.microsoft.com/zh-cn/dotnet/api/system.windows.data.binding.notifyonvalidationerror?view=net-5.0&WT.mc_id=WD-MVP-5003763) 必須設定為 `true`,在實際業務中比較麻煩。還有一種方法是主動遍歷所有元素並使用 `Validation.GetHasError` 找到目標元素,這樣做法簡單很多,但不夠自動,而且和本文的方法大同小異,就不另外寫出來了。
## 6. 原始碼