I'm trying to generate visual feedback on the result of validating a TextBox
input control with the following requirements:
The related validation code seems to be working: the function that performs validation is running, the error messages are accumulated, and OnErrorsChanged
is being invoked and WPF is doing ... something... a red box appears around my entire UserControl
.
After reading many related SO questions/answers and considering the fact that a red box is being drawn, my initial instinct was that this is related to adorner placement. However, I have tried placing AdornerDecorator
at every layer with no effect. I also have checked at runtime if the textbox controls have an adorner layer accessible to them, and they do. It is being created from the ContentControl
hosting my view (View1.xaml
). This seems to explain why I'm seeing a red box around the entire ContentControl
, but I'm confused as to why the TextBox
's Validation.ErrorTemplate
is not controlling the placement of the adorner.
View1.xaml
UserControl
Grid
GroupBox
StackPanel
(vertical layout)
StackPanel
(horizontal layout - repeated for all textbox controls)
Label
Textbox
(validated control - ErrorTemplate
here)<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>
With a ControlTemplate
taken from BionicCode's answer here:
<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>
Here is a sample input control to be validated in this view:
<TextBox Name="Name1"
Width="350"
Validation.ErrorTemplate="{DynamicResource ValidationErrorTemplate}"
Text="{Binding Name, Mode=OneWayToSource, ValidatesOnNotifyDataErrors=True,UpdateSourceTrigger=Explicit}"/>
Your code has several flaws and it does not honor certain details of the binding engine's validation behavior. I also have some suggestions to make that should improve your overall code.
TextBox
As I had already mentioned in the comments you must set the Binding.Mode
to BindingMode.TwoWay
if you want the binding target to show validation errors.
If only want plain property validation, then
BindingMode.OneWayToSource
is sufficient. The binding engine sets
attatched properties like the Validation.Errors
property on the
target element. It does this in the wake of updating the target
property. The target is only updated if the Binding.Mode
is
OneWay
, OneTime
or TwoWay
. Because you want to trigger the
data validation (which requires to update the binding source), the
only feasible mode is TwoWay
.
SessionFromView.xaml
Set the Binding.Mode
of the TextBox
binding to BindingMode.TwoWay
<TextBox Name="SessionName"
Validation.ErrorTemplate="{DynamicResource ValidationErrorTemplate}"
Text="{Binding Name, Mode=TwoWay, ValidatesOnNotifyDataErrors=True}" />
You missed raising the INotifyDataErrorInfo.ErrorsChanged
event for the case that the parameter propertyName
is null
or empty.
The binding engine subscribes to the INotifyDataErrorInfo.ErrorsChanged
event to update the binding (based on the configured Binding.Mode
). If you call ViewModelbase.ClearErrors
without providing a property name, as you are doing, then clearing the errors will not affect the UI as the binding engine is never triggered to re-evaluate the binding that is associated with the event. As a result, the error template is not removed once the errors have been cleared.
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
You have three possible options to fix this:
Binding.ValidatesOnNotifyDataErrors
to false
(recommended).Validation.ErrorTemplate
to null
(not recommended). Although I wouldn't recommend this solution (because it will still keep the binding busy with handling validation errors, which introduces overhead and performance costs), this solution is more convenient as you can use global implicit styles to set Validation.ErrorTemplate
to {x:Null}
.INotifyDataErrorInfo.GetErrors
implementation (not recommended). This will eleimintae the possibility to get all errors of the binding source. WPF is using this feature heavily (which in your case causes the red border around the UserControl
(or the parent hosting ContentControl
to be precise).The default value for Binding.ValidatesOnNotifyDataErrors
is true
. In addition, the Validation.ErrorTemplate
attached property returns a default template that simply draws a red border around the target element.
If a control binds to the data source object and not to a property on this object, the binding engine passes an empty string instead of a specific property name to the INotifyDataErrorInfo.GetErrors
method. If this method is implemented to return all current errors of the instance, like your ViewModelBase
does (and which is the recommended implementation),
then the target has errors to display:
<!--
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>();