Problem
I’ve spent the last two days reviewing SO questions on this topic – there are many. Some seemed like hopeful solutions but have not fixed my problem.
I’m trying to validate some textbox input controls with the following requirements:
- A red box is displayed around the textbox border
- Any error messages are displayed just below the textbox
Other considerations:
- The textbox controls are located inside a
UserControl
, nested several levels deep (see code below) - I want
UpdateSourceTrigger=Explicit
on all validated controls, so that the user can click a button to perform validation on the entire form. - Bound textboxes use
OneWayToSource
type - I am using
Fluent:RibbonWindow
control for my main window - I am not using
ValidationRule
because my form elements have complex interdependencies mapping to differentModel
constraints, and from my investigations, it doesn’t seem likeValidationRule
supports it. - I am trying my best to stick to MVVM
Status
The validation code seems to be working: the function that performs validation is called when the button is clicked, 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 all the SO related questions/answers, and the fact that a red box is being drawn, my instinct is 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.
Digging deeper to see where the adorner is being placed in the OnErrorsChanged
invocation path, because I’m not specifying an adorner site as an attribute in the XAML, it’s pulling one from the ContentControl
hosting my view (View1.xaml
):
This seems to explain what I’m seeing (red box around the entire view), but if I specify an adorner site via the ContentControl
‘s Validation.ValidationAdornerSite
property, it appears to make no difference.
Finally, in ShowValidationAdornerHelper
routine, the full picture:
adornerLayer
is anAdornerLayer
that has no parent and appears to be linked to nothing, but it was derived fromSiteUIElement
(theContentControl
hosting my view)- no
ValidationAdornerProperty
property was set on theContentControl
, so it creates a new one invalidationAdorner
- it adds the new
validationAdorner
to theadornerLayer
- it sets the
ValidationAdornerProperty
property of theContentControl
to this newly createdvalidationAdorner
These clues seem to suggest I should be setting a value for Validation.ValidationAdornerSite
(e.g., here, here, here and many others), but I’m confused as to why the TextBox’s Validation.ErrorTemplate
is not controlling the placement of the adorner.
Control layout
MainWindow.xaml
Fluent:RibbonWindow
DockPanel
Fluent:Ribbon
ContentControl
(views are switched here)StatusBar
View1.xaml
UserControl
Grid
GroupBox
StackPanel
(vertical layout)StackPanel
(horizontal layout – repeated for all textbox controls)Label
Textbox
(validated control –ErrorTemplate
here)
Code
View1.xaml
The template is 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}"/>
ViewModel.cs
The validation code is practically identical to BionicCode’s answer above, so I’ve not pasted it here. That code resides in the ViewModelBase
which my viewmodel inherits from. Then my viewmodel validates the controls whenever the button is clicked:
public bool ValidateForm()
{
ClearErrors();
if (string.IsNullOrEmpty(Name))
{
AddError(nameof(Name), $"Name cannot be empty.");
}
....
And the field defined in the viewmodel:
private string _Name;
public string Name
{
get => _Name;
set
{
if (_Name != value)
{
_Name = value;
OnPropertyChanged("Name");
}
}
}