Tuesday, June 23, 2015

Setting the Background of a WPF TextBox depending on Validation in a Template/Style using TemplateBinding

Recently I wanted to change the Background of a TextBox, if some own ValidationRule failed. But I couldn't get this to work. I tried several things and I gained more insights into styling and especially into styling wrong validated elements. And finally I get it to work.

If you are not familiar with WPF validation, here you can find excellent descriptions of how to use validation in WPF:

Now, I come to to initial situation.

Using ValidationRule in WPF

I had a ValidationRule, something like that.
namespace ErrorValidation
{
    public class RangeValidationRule : ValidationRule
    {
        public double Min { getset; }
        public double Max { getset; }
 
        public override ValidationResult Validate(object value,
            CultureInfo cultureInfo)
        {
            double enteredValue;
            if (double.TryParse(value.ToString(), out enteredValue))
            {
                if ((enteredValue < Min) || (enteredValue > Max))
                {
                    return new ValidationResult(false,
                      string.Format("Entered value must be in the range [{0};{1}].",
                      Min, Max));
                }
            }
            else
            {
                return new ValidationResult(false,
                    string.Format("Value '{0}' is not of type double.", value));
            }
 
            return new ValidationResult(truenull);
        }
    }
}
To get access to the ValidationRule the XML namespace is defined in the XAML.
xmlns:validation="clr-namespace:ErrorValidation"
I get access to the ValidationRule in the Window/UserControl Resources.
<validation:RangeValidationRule x:Key="RangeValidationRule" />
Now I can use the ValidationRule to validate user input.
        <TextBox Margin="12,0,12,12">
            <TextBox.Text>
                <Binding Path="InputField1"
                         UpdateSourceTrigger="PropertyChanged">
                    <Binding.ValidationRules>
                        <validation:RangeValidationRule Min="5"
                                                        Max="10" />
                    </Binding.ValidationRules>
                </Binding>
            </TextBox.Text>
        </TextBox>
The default style is used to mark user input that violates the ValidationRule.

Validation styles

So far, so good. But now I wanted to extend my existing TextBox style. If the validation failed, I wanted to change the Background of the TextBox. This was one of my not working tries.
        <Style x:Key="MyWrongTextBoxStyle"
               TargetType="{x:Type TextBox}">
            <Setter Property="Margin"
                    Value="4" />
            <Setter Property="Foreground"
                    Value="DarkBlue" />
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type TextBoxBase}">
                        <Border Name="Border"
                                CornerRadius="2"
                                BorderThickness="1"
                                Background="White"
                                BorderBrush="LightBlue">
                            <VisualStateManager.VisualStateGroups>
                                <VisualStateGroup x:Name="CommonStates">
                                    <VisualState x:Name="Normal" />
                                    <VisualState x:Name="Disabled">
                                        <Storyboard>
                                            <ColorAnimationUsingKeyFrames
                                                Storyboard.TargetName="Border"
                                                Storyboard.TargetProperty=
                                                "(Panel.Background).
                                                (SolidColorBrush.Color)">
                                                <EasingColorKeyFrame
                                                    KeyTime="0"
                                                    Value="LightGray" />
                                            </ColorAnimationUsingKeyFrames>
                                        </Storyboard>
                                    </VisualState>
                                    <VisualState x:Name="ReadOnly">
                                        <Storyboard>
                                            <ColorAnimationUsingKeyFrames
                                                Storyboard.TargetName="Border"
                                                Storyboard.TargetProperty=
                                                "(Panel.Background).
                                                (SolidColorBrush.Color)">
                                                <EasingColorKeyFrame
                                                    KeyTime="0"
                                                    Value="LightBlue" />
                                            </ColorAnimationUsingKeyFrames>
                                        </Storyboard>
                                    </VisualState>
                                    <VisualState x:Name="MouseOver" />
                                </VisualStateGroup>
                            </VisualStateManager.VisualStateGroups>
                            <ScrollViewer Margin="0"
                                          x:Name="PART_ContentHost" />
                        </Border>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
            <Setter Property="Validation.ErrorTemplate">
                <Setter.Value>
                    <ControlTemplate>
                        <StackPanel>
                            <DockPanel>
                                <AdornedElementPlaceholder x:Name="Placeholder" />
                                <TextBlock Foreground="Red"
                                           FontSize="18"
                                           Text="!" />
                            </DockPanel>
                            <StackPanel Orientation="Horizontal">
                                <TextBlock Foreground="Red"
                                           Text="The value '" />
                                <TextBlock Foreground="Red"
                                           Text="{Binding ElementName=Placeholder,
                                                      Path=AdornedElement.Text}" />
                                <TextBlock Foreground="Red"
                                           Text="' causes an error (see tooltip)" />
                            </StackPanel>
                        </StackPanel>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
            <Style.Triggers>
                <Trigger Property="Validation.HasError"
                         Value="True">
                    <Setter Property="ToolTip"
                            Value="{Binding RelativeSource=
                              {x:Static RelativeSource.Self},
                              Path=(Validation.Errors).CurrentItem.ErrorContent}" />
                    <Setter Property="Background"
                            Value="Orange" />
                    <Setter Property="BorderThickness"
                            Value="2" />
                    <Setter Property="BorderBrush"
                            Value="Red" />
                </Trigger>
            </Style.Triggers>
        </Style>

I tried several combinations with Validation.ErrorTemplate and Validation.HasError. But when should I use Validation.ErrorTemplate and when Validation.HasError? I have not thought much about it, until now. But the answer is not very surprising.

Validation.ErrorTemplate is used to add elements that decorate an existing element. So I cannot use it to change the Background of an existing element. But it can be used to add an image, an exclamation mark, TextBlock or something else next to the evaluated element.


Validation.HasError can be used to add a ToolTip or to change properties of an existing element.
So I should use it to change the color of the Background. But why is it not working?

The problem is how the Template style is defined. If I remove the Setter of the Template property in the Style, it is working. So I have to change the Template.

TemplateBinding

To allow changing a property of a defined Template, TemplateBinding is used. TemplateBinding is a markup extension. The properties that are set by TemplateBinding in the Template Setter can be referrenced outside the Setter of the Template. The default property values of the properties that are set by TemplateBinding can be set by other Setters. It is also possible to set the values in XAML parts that are using the Style. And now it is also possible to set the values by a Trigger, like the Background as I wanted. This is the working code that changes the Background of a TextBox in case of error.
        <Style x:Key="MyTextBoxStyle"
               TargetType="{x:Type TextBox}">
            <Setter Property="Margin"
                    Value="4" />
            <Setter Property="Foreground"
                    Value="DarkBlue" />
            <Setter Property="Background"
                    Value="White" />
            <Setter Property="BorderBrush"
                    Value="LightBlue" />
            <Setter Property="BorderThickness"
                    Value="1" />
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type TextBoxBase}">
                        <Border Name="Border"
                                CornerRadius="2"
                                BorderThickness="{TemplateBinding BorderThickness}"
                                Background="{TemplateBinding Background}"
                                BorderBrush="{TemplateBinding BorderBrush}">
                            <VisualStateManager.VisualStateGroups>
                                <VisualStateGroup x:Name="CommonStates">
                                    <VisualState x:Name="Normal" />
                                    <VisualState x:Name="Disabled">
                                        <Storyboard>
                                            <ColorAnimationUsingKeyFrames
                                                Storyboard.TargetName="Border"
                                                Storyboard.TargetProperty=
                                                "(Panel.Background).
                                                (SolidColorBrush.Color)">
                                                <EasingColorKeyFrame
                                                    KeyTime="0"
                                                    Value="LightGray" />
                                            </ColorAnimationUsingKeyFrames>
                                        </Storyboard>
                                    </VisualState>
                                    <VisualState x:Name="ReadOnly">
                                        <Storyboard>
                                            <ColorAnimationUsingKeyFrames
                                                Storyboard.TargetName="Border"
                                                Storyboard.TargetProperty=
                                                "(Panel.Background).
                                                (SolidColorBrush.Color)">
                                                <EasingColorKeyFrame
                                                    KeyTime="0"
                                                    Value="LightBlue" />
                                            </ColorAnimationUsingKeyFrames>
                                        </Storyboard>
                                    </VisualState>
                                    <VisualState x:Name="MouseOver" />
                                </VisualStateGroup>
                            </VisualStateManager.VisualStateGroups>
                            <ScrollViewer Margin="0"
                                          x:Name="PART_ContentHost" />
                        </Border>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
            <Setter Property="Validation.ErrorTemplate">
                <Setter.Value>
                    <ControlTemplate>
                        <StackPanel>
                            <DockPanel>
                                <AdornedElementPlaceholder x:Name="Placeholder" />
                                <TextBlock Foreground="Red"
                                           FontSize="18"
                                           Text="!" />
                            </DockPanel>
                            <StackPanel Orientation="Horizontal">
                                <TextBlock Foreground="Red"
                                           Text="The value '" />
                                <TextBlock Foreground="Red"
                                           Text="{Binding ElementName=Placeholder,
                                                      Path=AdornedElement.Text}" />
                                <TextBlock Foreground="Red"
                                           Text="' causes an error (see tooltip)" />
                            </StackPanel>
                        </StackPanel>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
            <Style.Triggers>
                <Trigger Property="Validation.HasError"
                         Value="True">
                    <Setter Property="ToolTip"
                            Value="{Binding RelativeSource=
                              {x:Static RelativeSource.Self},
                              Path=(Validation.Errors).CurrentItem.ErrorContent}" />
                    <Setter Property="Background"
                            Value="Orange" />
                    <Setter Property="BorderThickness"
                            Value="2" />
                    <Setter Property="BorderBrush"
                            Value="Red" />
                </Trigger>
            </Style.Triggers>
        </Style>
And this is the result.


5 comments:

  1. Nursing jobs in Germany present a fantastic opportunity for Indian nurses eager to advance their careers in a highly regarded healthcare system. With a significant demand for skilled professionals, numerous positions are available across hospitals, clinics, and rehabilitation centres. Salaries are competitive, typically ranging from EUR 2,500 to EUR 4,000 per month, along with attractive benefits to ensure a rewarding employment package. The emphasis on continuous professional development offers Indian nurses access to comprehensive training programs and workshops. Fluency in the German language is crucial for effective communication and successful integration into the workplace environment. Additionally, the culturally diverse and welcoming atmosphere enriches the overall expatriate experience. Embracing these opportunities not only promises financial stability but also substantial personal and professional growth in a vibrant community. Ultimately, nursing careers in Germany are a stepping stone towards a fulfilling international experience.
    https://www.dynamichealthstaff.com/nursing-jobs-in-germany-for-indian-nurses

    ReplyDelete