检测 WPF 验证错误

在 WPF 中,可以使用 ExceptionValidationRuleDataErrorValidationRule根据数据绑定期间在数据层中抛出的错误设置验证。

假设您以这种方式设置了许多控件,并且有一个 Save 按钮。当用户单击 Save 按钮时,需要确保在继续保存之前没有验证错误。如果存在验证错误,您希望对它们大声疾呼。

在 WPF 中,如何查明是否有任何数据绑定控件设置了验证错误?

67498 次浏览

You can iterate over all your controls tree recursively and check the attached property Validation.HasErrorProperty, then focus on the first one you find in it.

你也可以使用许多已经写好的解决方案 你可以检查 这个线程的一个例子和更多的信息

下面的代码(来自 Chris Sell 和 Ian Griffiths 编写的《编程 WPF 》一书)验证了依赖对象及其子对象上的所有绑定规则:

public static class Validator
{


public static bool IsValid(DependencyObject parent)
{
// Validate all the bindings on the parent
bool valid = true;
LocalValueEnumerator localValues = parent.GetLocalValueEnumerator();
while (localValues.MoveNext())
{
LocalValueEntry entry = localValues.Current;
if (BindingOperations.IsDataBound(parent, entry.Property))
{
Binding binding = BindingOperations.GetBinding(parent, entry.Property);
foreach (ValidationRule rule in binding.ValidationRules)
{
ValidationResult result = rule.Validate(parent.GetValue(entry.Property), null);
if (!result.IsValid)
{
BindingExpression expression = BindingOperations.GetBindingExpression(parent, entry.Property);
System.Windows.Controls.Validation.MarkInvalid(expression, new ValidationError(rule, expression, result.ErrorContent, null));
valid = false;
}
}
}
}


// Validate all the bindings on the children
for (int i = 0; i != VisualTreeHelper.GetChildrenCount(parent); ++i)
{
DependencyObject child = VisualTreeHelper.GetChild(parent, i);
if (!IsValid(child)) { valid = false; }
}


return valid;
}


}

您可以在页面/窗口的保存按钮单击事件处理程序中调用这个函数

private void saveButton_Click(object sender, RoutedEventArgs e)
{


if (Validator.IsValid(this)) // is valid
{


....
}
}

当我使用 ListBox 时,发布的代码不起作用。我重写了它,现在它起作用了:

public static bool IsValid(DependencyObject parent)
{
if (Validation.GetHasError(parent))
return false;


// Validate all the bindings on the children
for (int i = 0; i != VisualTreeHelper.GetChildrenCount(parent); ++i)
{
DependencyObject child = VisualTreeHelper.GetChild(parent, i);
if (!IsValid(child)) { return false; }
}


return true;
}

在应答表单 aogan 中,与其通过验证规则显式迭代,不如直接调用 expression.UpdateSource():

if (BindingOperations.IsDataBound(parent, entry.Property))
{
Binding binding = BindingOperations.GetBinding(parent, entry.Property);
if (binding.ValidationRules.Count > 0)
{
BindingExpression expression
= BindingOperations.GetBindingExpression(parent, entry.Property);
expression.UpdateSource();


if (expression.HasError) valid = false;
}
}

Had the same problem and tried the provided solutions. A combination of H-Man2's and skiba_k's solutions worked almost fine for me, for one exception: My Window has a TabControl. And the validation rules only get evaluated for the TabItem that is currently visible. So I replaced VisualTreeHelper by LogicalTreeHelper. Now it works.

    public static bool IsValid(DependencyObject parent)
{
// Validate all the bindings on the parent
bool valid = true;
LocalValueEnumerator localValues = parent.GetLocalValueEnumerator();
while (localValues.MoveNext())
{
LocalValueEntry entry = localValues.Current;
if (BindingOperations.IsDataBound(parent, entry.Property))
{
Binding binding = BindingOperations.GetBinding(parent, entry.Property);
if (binding.ValidationRules.Count > 0)
{
BindingExpression expression = BindingOperations.GetBindingExpression(parent, entry.Property);
expression.UpdateSource();


if (expression.HasError)
{
valid = false;
}
}
}
}


// Validate all the bindings on the children
System.Collections.IEnumerable children = LogicalTreeHelper.GetChildren(parent);
foreach (object obj in children)
{
if (obj is DependencyObject)
{
DependencyObject child = (DependencyObject)obj;
if (!IsValid(child)) { valid = false; }
}
}
return valid;
}

我会提供一个小的优化。

如果在相同的控件上多次执行此操作,则可以添加上述代码以保留实际具有验证规则的控件的列表。然后,当您需要检查有效性时,只检查那些控件,而不是整个可视化树。 如果您有许多这样的控件,这将被证明是更好的。

You might be interested in the 图书馆 sample application of the WPF 应用程序框架(WAF). It shows how to use validation in WPF and how to control the Save button when validation errors exists.

这篇文章非常有帮助。感谢所有的贡献者。这里有一个 LINQ 版本,你要么喜欢,要么讨厌。

private void CanExecute(object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute = IsValid(sender as DependencyObject);
}


private bool IsValid(DependencyObject obj)
{
// The dependency object is valid if it has no errors and all
// of its children (that are dependency objects) are error-free.
return !Validation.GetHasError(obj) &&
LogicalTreeHelper.GetChildren(obj)
.OfType<DependencyObject>()
.All(IsValid);
}

除了 Dean 伟大的 LINQ 实现之外,我还乐于将代码包装成 DependencyObjects 的扩展:

public static bool IsValid(this DependencyObject instance)
{
// Validate recursivly
return !Validation.GetHasError(instance) &&  LogicalTreeHelper.GetChildren(instance).OfType<DependencyObject>().All(child => child.IsValid());
}

考虑到可重用性,这使得它非常漂亮。

Here is a 图书馆 for form validation in WPF. 这里是纽奇包裹.

样本:

<Border BorderBrush="{Binding Path=(validationScope:Scope.HasErrors),
Converter={local:BoolToBrushConverter},
ElementName=Form}"
BorderThickness="1">
<StackPanel x:Name="Form" validationScope:Scope.ForInputTypes="{x:Static validationScope:InputTypeCollection.Default}">
<TextBox Text="{Binding SomeProperty}" />
<TextBox Text="{Binding SomeOtherProperty}" />
</StackPanel>
</Border>

其思想是,我们通过附加的属性定义一个验证范围,告诉它要跟踪什么输入控件。 然后我们可以做:

<ItemsControl ItemsSource="{Binding Path=(validationScope:Scope.Errors),
ElementName=Form}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type ValidationError}">
<TextBlock Foreground="Red"
Text="{Binding ErrorContent}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>

我正在使用 DataGrid,上面的正常代码直到 DataGrid 本身失去焦点才发现错误。即使使用下面的代码,它仍然不会“看到”错误,直到行失去焦点,但这至少比等待网格失去焦点好。

此版本还跟踪字符串列表中的所有错误。这篇文章中的大多数其他版本没有这样做,所以他们可以停止在第一个错误。

public static List<string> Errors { get; set; } = new();


public static bool IsValid(this DependencyObject parent)
{
Errors.Clear();


return IsValidInternal(parent);
}


private static bool IsValidInternal(DependencyObject parent)
{
// Validate all the bindings on this instance
bool valid = true;


if (Validation.GetHasError(parent) ||
GetRowsHasError(parent))
{
valid = false;


/*
* Find the error message and log it in the Errors list.
*/
foreach (var error in Validation.GetErrors(parent))
{
if (error.ErrorContent is string errorMessage)
{
Errors.Add(errorMessage);
}
else
{
if (parent is Control control)
{
Errors.Add($"<unknow error> on field `{control.Name}`");
}
else
{
Errors.Add("<unknow error>");
}
}
}
}


// Validate all the bindings on the children
for (int i = 0; i != VisualTreeHelper.GetChildrenCount(parent); i++)
{
var child = VisualTreeHelper.GetChild(parent, i);
if (IsValidInternal(child) == false)
{
valid = false;
}
}


return valid;
}


private static bool GetRowsHasError(DependencyObject parent)
{
DataGridRow dataGridRow;


if (parent is not DataGrid dataGrid)
{
/*
* This is not a DataGrid, so return and say we do not have an error.
* Errors for this object will be checked by the normal check instead.
*/
return false;
}


foreach (var item in dataGrid.Items)
{
/*
* Not sure why, but under some conditions I was returned a null dataGridRow
* so I had to test for it.
*/
dataGridRow = (DataGridRow)dataGrid.ItemContainerGenerator.ContainerFromItem(item);
if (dataGridRow != null &&
Validation.GetHasError(dataGridRow))
{
return true;
}
}
return false;
}