I have created a Custom TextBox control, which overrides TextBox. I have it all working perfectly and I also have validation working with an Override of OnTextChanged.
The issue I have now is that the implementation within the ViewModel has been changed, so validation is now taking place on a button click 'save' at the end of the form (using INotifyDataErrorInfo) rather than on a keystroke basis
My question is, how to I react in my CustomControl to the validation Errors being updated because I want to recolour my textbox border based on the validation result.
Xaml code for where my control is added to the Window:
<ctrl:AppTextBox ButtonCommand="{Binding AddNewWordCommand}"
ButtonLabel="+"
Label="New Word"
Text="{Binding NewWord,
UpdateSourceTrigger=PropertyChanged,
ValidatesOnDataErrors=True}"
x:Name="txtNewWord" />
Xaml code for my CustomControl (AppTextBox):
<Style TargetType="{x:Type ctrl:AppTextBox}"
BasedOn="{StaticResource {x:Type TextBox}}">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="FontFamily" Value="{DynamicResource Inter400}" />
<Setter Property="FontSize" Value="{DynamicResource Font_Medium}" />
<Setter Property="Foreground" Value="{DynamicResource Theme_Foreground}" />
<Setter Property="Margin" Value="20,8" />
<Setter Property="TextWrapping" Value="Wrap" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Validation.ErrorTemplate" Value="{x:Null}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ctrl:AppTextBox}">
<StackPanel Orientation="Vertical">
<Border Background="{TemplateBinding Background}"
BorderBrush="{DynamicResource TextBox_Border_Focus}"
BorderThickness="1"
CornerRadius="{DynamicResource Control_CornerRadius}"
Cursor="IBeam"
Focusable="False"
MinHeight="{DynamicResource TextBox_Height}"
Padding="10,2,5,2"
x:Name="bdrOutline" >
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Background="Transparent"
Grid.Column="0"
Orientation="Vertical"
VerticalAlignment="Center"
x:Name="stpLayout" >
<Label Background="Transparent"
Content="{TemplateBinding Label}"
Focusable="False"
FontFamily="{DynamicResource Inter600}"
FontSize="12"
Foreground="{DynamicResource TextBox_Label_Focus}"
IsTabStop="False"
VerticalAlignment="Center"
Visibility="{TemplateBinding LabelVisibility}"
x:Name="lblHeading" />
<ScrollViewer Background="Transparent"
BorderBrush="Transparent"
Cursor="IBeam"
Foreground="{DynamicResource Theme_Foreground}"
HorizontalAlignment="Stretch"
Margin="3,0,0,0"
VerticalAlignment="Center"
VerticalScrollBarVisibility="Auto"
Visibility="Visible"
x:Name="PART_ContentHost" >
<i:Interaction.Triggers>
<i:KeyTrigger Key="Return">
<i:InvokeCommandAction Command="{Binding ButtonCommand}" />
</i:KeyTrigger>
</i:Interaction.Triggers>
</ScrollViewer>
</StackPanel>
<ContentControl Focusable="False"
Grid.Column="1"
HorizontalAlignment="Center"
Margin="5,0"
Style="{DynamicResource ErrorIcon_Canvas}"
VerticalAlignment="Center"
Visibility="Collapsed"
x:Name="cvsError" />
<Button Command="{TemplateBinding ButtonCommand}"
Content="+"
Cursor="Hand"
Grid.Column="2"
Margin="5,0"
Padding="10,7"
Style="{DynamicResource FilledSquareButton_Base}"
VerticalAlignment="Center"
Visibility="Visible"
x:Name="btnAction" />
</Grid>
</Border>
<!-- List Errors Here -->
<ListBox ItemContainerStyle="{DynamicResource ErrorMessagesListBoxItem}"
ItemsSource="{Binding RelativeSource={x:Static RelativeSource.Self},
Path=(Validation.Errors)/ErrorContent}"
Style="{DynamicResource ErrorMessagesListBox}" />
</StackPanel>
<ControlTemplate.Triggers>
<Trigger Property="ButtonIsActive" Value="False">
<Setter TargetName="btnAction" Property="Visibility" Value="Collapsed" />
</Trigger>
<Trigger Property="HasBorder" Value="False">
<Setter TargetName="bdrOutline" Property="BorderBrush" Value="Transparent" />
</Trigger>
<Trigger Property="Validation.HasError" Value="True">
<Setter TargetName="lblHeading" Property="Foreground" Value="{DynamicResource Theme_ErrorBrush}" />
</Trigger>
<Trigger Property="Layout" Value="Inline">
<Setter TargetName="lblHeading" Property="VerticalAlignment" Value="Top" />
<Setter TargetName="PART_ContentHost" Property="Margin" Value="40,5,0,0" />
<Setter TargetName="stpLayout" Property="Orientation" Value="Horizontal" />
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="HasBorder" Value="True" />
<Condition Property="Validation.HasError" Value="True" />
</MultiTrigger.Conditions>
<Setter TargetName="bdrOutline" Property="BorderBrush" Value="{DynamicResource Theme_ErrorBrush}" />
</MultiTrigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="TextFillState" Value="Empty" />
<Condition Property="IsKeyboardFocusWithin" Value="False" />
<Condition Property="Validation.HasError" Value="False" />
</MultiTrigger.Conditions>
<Setter TargetName="bdrOutline" Property="BorderBrush" Value="{DynamicResource TextBox_Border_Empty}" />
<Setter TargetName="lblHeading" Property="Foreground" Value="{DynamicResource TextBox_Label_Empty}" />
<Setter TargetName="PART_ContentHost" Property="Visibility" Value="Collapsed" />
</MultiTrigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="TextFillState" Value="Empty" />
<Condition Property="IsMouseOver" Value="True" />
<Condition Property="IsKeyboardFocusWithin" Value="False" />
<Condition Property="Validation.HasError" Value="False" />
</MultiTrigger.Conditions>
<Setter TargetName="bdrOutline" Property="BorderBrush" Value="{DynamicResource TextBox_Border_Empty_Hover}" />
<Setter TargetName="lblHeading" Property="Foreground" Value="{DynamicResource TextBox_Label_Empty_Hover}" />
</MultiTrigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="TextFillState" Value="Filled" />
<Condition Property="IsMouseOver" Value="False" />
<Condition Property="IsKeyboardFocusWithin" Value="False" />
<Condition Property="Validation.HasError" Value="False" />
</MultiTrigger.Conditions>
<Setter TargetName="bdrOutline" Property="BorderBrush" Value="{DynamicResource TextBox_Border_Filled}" />
<Setter TargetName="lblHeading" Property="Foreground" Value="{DynamicResource TextBox_Label_Filled}" />
</MultiTrigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="TextFillState" Value="Filled" />
<Condition Property="IsMouseOver" Value="True" />
<Condition Property="Validation.HasError" Value="False" />
</MultiTrigger.Conditions>
<Setter TargetName="bdrOutline" Property="BorderBrush" Value="{DynamicResource TextBox_Border_Filled_Hover}" />
<Setter TargetName="lblHeading" Property="Foreground" Value="{DynamicResource TextBox_Label_Filled_Hover}" />
</MultiTrigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsKeyboardFocusWithin" Value="True" />
<Condition Property="Validation.HasError" Value="False" />
</MultiTrigger.Conditions>
<Setter TargetName="bdrOutline" Property="BorderBrush" Value="{DynamicResource TextBox_Border_Focus}" />
<Setter TargetName="lblHeading" Property="Foreground" Value="{DynamicResource TextBox_Label_Focus}" />
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
UPDATE:
I have managed to get SOME validation working in my user control:
I am able to set the red border and labels by checking
<Condition Property="Validation.HasError" Value="True" />
BUT I am unable to display the errors within the control, instead I have had to add a listbox to the Window, and this is not something I want to do:
<ListBox ItemContainerStyle="{DynamicResource ErrorMessagesListBoxItem}"
ItemsSource="{Binding ElementName=txtEntry,
Path=(Validation.Errors)}"
Style="{DynamicResource ErrorMessagesListBox}" />
I have tried adding this code to my custom Control but it doesnt work for some reason:
<ListBox ItemContainerStyle="{DynamicResource ErrorMessagesListBoxItem}"
ItemsSource="{Binding RelativeSource={x:Static RelativeSource.Self},
Path=(Validation.Errors)}"
Style="{DynamicResource ErrorMessagesListBox}" />
And neither does this:
<ListBox ItemContainerStyle="{DynamicResource ErrorMessagesListBoxItem}"
ItemsSource="{Binding ElementName=PART_ContentHost,
Path=(Validation.Errors)}"
Style="{DynamicResource ErrorMessagesListBox}" />
Binding
RelativeSource.Self
references the current element theBinding
is defined on. But you are inside theControlTemplate
and want to read from the attached property of the parent type that theControlTemplate
is applied to. You must therefore useRelativeSource.TemplatedParent
. And because you want to bind theListBox
to the error collection and not to the current error item you must not bind theListBox.ItemsSource
to the current item'sValidationError.ErrorContent
property.Define the
Binding
on theListBox
inside theControlTemplate
as follows:I highly recommend the use of an error
ControlTemplate
that you assign to theValidation.ErrorTemplate
attached property instead of adding error feedback visuals and logic directly to the visual tree of the defaultControlTemplate
of the control. You can follow and extend this example: How to add validation to view model properties or how to implement INotifyDataErrorInfo.