我正在尝试针对验证
TextBox
输入控件的结果生成视觉反馈,满足以下要求:
相关的验证代码似乎正在工作:执行验证的函数正在运行,错误消息正在累积,
OnErrorsChanged
正在被调用,WPF正在做......某事......我的整个周围出现一个红色框UserControl
。
在阅读了许多相关的SO问题/答案并考虑到正在绘制红色框的事实后,我最初的直觉是这与装饰器的放置有关。但是,我尝试在每一层放置
AdornerDecorator
,但没有效果。我还在运行时检查了文本框控件是否有可访问的装饰层,它们确实如此。它是从托管我的视图 (ContentControl
) 的View1.xaml
创建的。这似乎解释了为什么我在整个 ContentControl
周围看到一个红色框,但我很困惑为什么 TextBox
的 Validation.ErrorTemplate
没有控制装饰器的位置。
View1.xaml
UserControl
Grid
GroupBox
StackPanel
(垂直布局)
StackPanel
(水平布局 - 对所有文本框控件重复)
Label
Textbox
(已验证的控制 - ErrorTemplate
此处)<UserControl x:Class="View1"
....namespaces....
xmlns:Fluent="urn:fluent-ribbon"
xmlns:vm="clr-namespace:MyNamespace.ViewModel"
xmlns:view="clr-namespace:MyNamespace.View"
Name="MyView1"
DataContext="{StaticResource g_MainWindowViewModel}"
Validation.ErrorTemplate="{x:Null}">
<DockPanel LastChildFill="True">
<ContentControl Name="NewSessionFormContentControl"
Content="{Binding m_SessionFormViewModel}"
DockPanel.Dock="Left"/>
</DockPanel>
ControlTemplate
取自BionicCode在这里的答案:
<ControlTemplate x:Key="ValidationErrorTemplate">
<DockPanel LastChildFill="True">
<Border BorderBrush="Red" BorderThickness="1">
<AdornedElementPlaceholder x:Name="AdornedElement"/>
</Border>
<Border Background="White"
BorderBrush="Red"
Padding="4"
BorderThickness="1,0,1,1"
HorizontalAlignment="Left">
<ItemsControl ItemsSource="{Binding}"
HorizontalAlignment="Left"
DockPanel.Dock="Right">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding ErrorContent}"
Foreground="Red"
DockPanel.Dock="Right"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Border>
</DockPanel>
</ControlTemplate>
这是要在此视图中验证的示例输入控件:
<TextBox Name="Name1"
Width="350"
Validation.ErrorTemplate="{DynamicResource ValidationErrorTemplate}"
Text="{Binding Name, Mode=OneWayToSource, ValidatesOnNotifyDataErrors=True,UpdateSourceTrigger=Explicit}"/>
您的代码有几个缺陷,并且它不遵守绑定引擎验证行为的某些细节。我还有一些建议可以改进您的整体代码。
TextBox
正如我在评论中已经提到的,如果您希望绑定目标显示验证错误,则必须将
Binding.Mode
设置为 BindingMode.TwoWay
。
如果只想简单的属性验证,那么
BindingMode.OneWayToSource
就足够了。绑定引擎集
附加属性,例如 上的 Validation.Errors
属性
目标元素。它在更新目标后执行此操作
财产。仅当 Binding.Mode
为
OneWay
、OneTime
或 TwoWay
。因为你想触发
数据验证(需要更新绑定源),
唯一可行的模式是TwoWay
。
SessionFromView.xaml
将
Binding.Mode
绑定的 TextBox
设置为 BindingMode.TwoWay
<TextBox Name="SessionName"
Validation.ErrorTemplate="{DynamicResource ValidationErrorTemplate}"
Text="{Binding Name, Mode=TwoWay, ValidatesOnNotifyDataErrors=True}" />
您错过了在参数
INotifyDataErrorInfo.ErrorsChanged
为 propertyName
或空的情况下引发 null
事件。
绑定引擎订阅
INotifyDataErrorInfo.ErrorsChanged
事件来更新绑定(基于配置的 Binding.Mode
)。如果您在不提供属性名称的情况下调用 ViewModelbase.ClearErrors
(如您所做的那样),那么清除错误不会影响 UI,因为永远不会触发绑定引擎来重新评估与事件关联的绑定。因此,一旦错误被清除,错误模板就不会被删除。
ViewModelBase.cs
protected bool ClearErrors(string propertyName = "")
{
if (string.IsNullOrEmpty(propertyName))
{
Errors.Clear();
// FIX::Raise ErrrorsChanged event
OnErrorsChanged(propertyName);
return true;
}
if (Errors.Remove(propertyName))
{
OnErrorsChanged(propertyName);
return true;
}
return false;
}
TextBox
您有三种可能的选择来解决此问题:
Binding.ValidatesOnNotifyDataErrors
设置为 false
(推荐)。Validation.ErrorTemplate
设置为 null
(不推荐)。尽管我不推荐此解决方案(因为它仍然会使绑定忙于处理验证错误,从而引入开销和性能成本),但此解决方案更方便,因为您可以使用全局隐式样式将 Validation.ErrorTemplate
设置为 {x:Null}
.INotifyDataErrorInfo.GetErrors
实现(不推荐)。这将消除获取绑定源的所有错误的可能性。 WPF 大量使用此功能(在您的情况下,这会导致 UserControl
周围出现红色边框(或者准确地说是父托管 ContentControl
)。Binding.ValidatesOnNotifyDataErrors
的默认值为true
。此外,附加属性 Validation.ErrorTemplate
返回一个默认模板,该模板仅在目标元素周围绘制红色边框。
如果控件绑定到数据源对象而不是该对象上的属性,则绑定引擎会将空字符串而不是特定属性名称传递给
INotifyDataErrorInfo.GetErrors
方法。如果实现此方法以返回实例的所有当前错误,就像您的 ViewModelBase
所做的那样(这是推荐的实现),<!--
The Content property binding targets the whole data source object
and not a property of the data source. As a result, the binding engine can't query the errors
for a particular property and instead tries to get all errors of the source object.
And because ValidatesOnNotifyDataErrors is TRUE by default, the binding on the Content property
will be validated.
The following two bindings are equivalaent
-->
<ContentControl Name="NewSessionFormContentControl"
Content="{Binding m_SessionFormViewModel}" />
<ContentControl Name="NewSessionFormContentControl"
Content="{Binding m_SessionFormViewModel, ValidatesOnNotifyDataErrors=True}" />
// ViewModelBase:
// This is were all errors of the source object are returned
// in case the binding engine passes in an empty string
public System.Collections.IEnumerable GetErrors(string propertyName)
=> string.IsNullOrWhiteSpace(propertyName)
? Errors.SelectMany(entry => entry.Value) // Return all errors
: Errors.TryGetValue(propertyName, out IList<object> errors)
? (IEnumerable<object>)errors
: new List<object>();
SessionView.xaml
<!-- Disable validation for the particular binding and avoid related overhead -->
<ContentControl Name="NewSessionFormContentControl"
Content="{Binding m_SessionFormViewModel, ValidatesOnNotifyDataErrors=False}" />
SessionView.xaml
<!--
Set Validation.ErrorTemplate to NULL.
The binding will still perform all the validation related operations
but won't show the visual error feedback .
-->
<ContentControl Name="NewSessionFormContentControl"
Validation.ErrorTemplate="{x:Null}"
Content="{Binding m_SessionFormViewModel}" />
ViewModelBase.xaml
// Don't support NULL and empty string queries
public System.Collections.IEnumerable GetErrors(string propertyName)
=> string.IsNullOrWhiteSpace(propertyName)
? new List<object>() // Return no errors
: Errors.TryGetValue(propertyName, out IList<object> errors)
? (IEnumerable<object>)errors
: new List<object>();