WPF – A sample of a TextBox Style for mandatory fields with validation error management
In this blog I will illustrate how to create a style that apply to standard WPF Textbox to achieve these results:
- be highlighted when focused
- possibility to select all the text when focused
- possibility to show a little red triangle on the Top-Right, in case it is a mandatory field
- possibility to show an inside explanatory label (for example: description of data to insert)
- show an allert on the right if the value will not be validated
- report the validation error inside the tooltip
In our code, the style is contained by a ResourceDictionary, so that it can be easy used in all the App.
You can find a full example in c# on github.
Prerequisites
To better understand this post, you should have:
- a basic knowledge of styling and templating processes of WPF.
- knowledge of WPF Binding process.
- knowledge of WPF Validation process.
- knowledge of Attacched Properties
Implementation
The first thing to do is to define the Style:
<Style x:Key="TextBoxBase" TargetType="{x:Type TextBox}" BasedOn="{StaticResource {x:Type TextBox}}"> </Style>
In the code, you can see:
- x:Key: while it is in a ResourceDictionary, we use this parameter to recall the Style
- TargetType: defines the Control the Style will be applied to
- BasedOn: recalls a Style on which this Style should base. In this case is the same of the TextBox (it sould be omitted).
Then, it’s possible to add some simple properties definitions (borders, fonts, …):
<Style x:Key="TextBoxBase" TargetType="{x:Type TextBox}" BasedOn="{StaticResource {x:Type TextBox}}"> ... <Setter Property="BorderThickness" Value="1"/> <Setter Property="FontFamily" Value="Segoe UI"/> <Setter Property="FontSize" Value="13"/> <Setter Property="Height" Value="Auto"/> <Setter Property="Margin" Value="1,3,3,3"/> ... </Style>
And… here is ended the simple part!
Highlight when focused
No… I was kidding… This is still simple! To achive this function, we’ve used a simple Trigger on the “IsFocused” property, setting a Background if the Property is true:
<Trigger Property="IsFocused" Value="true"> <Setter Property="Background" Value="LightYellow"/> </Trigger>
Select all when focused
The idea is that when the Textbox is focused, all the contained text is automatically selected. While we want to be able to disable this function, the best way is to use an attacched property.
Here we will not explain attached roperties (it is not the scope of this post), but you can find a very good tutorial here.
To setup the Attached Property, we created a new class named “Attacched_Properties” where we inserted all the attached properties needed nby the Style.
The first (and more complex) is “SelectAllOnEntry”:
public static readonly DependencyProperty SelectAllOnEntryProperty = DependencyProperty.RegisterAttached("SelectAllOnEntry", typeof(bool), typeof(AttacchedProperties), new PropertyMetadata(default(bool), SelectAllOnEntryChanged)); public static bool GetSelectAllOnEntry(DependencyObject d) { return (bool)d.GetValue(SelectAllOnEntryProperty); } public static void SetSelectAllOnEntry(DependencyObject d, bool value) { d.SetValue(SelectAllOnEntryProperty, value); } private static void SelectAllOnEntryChanged(DependencyObject d, DependencyPropertyChangedEventArgs args) { if (!(bool)args.NewValue) return; var text = d as TextBox; if (text == null) return; text.GotFocus += (s, e) => { text.SelectionStart = 0; text.SelectionLength = text.Text.Length; }; }
As you can see from the code above, after declaring the Attached_Property with its modifiers get and set, we declared a method called “SelectAllOnEntryChanged” that will run when Control is focused. This is the method that allows the text selection:
- First it checks if the attached property is true or false. If flase, it will return.
- Second it parses the dependency object (passed as argument) as TextBox. This point is critical: due to this, this attached property will work only with TextBox, even if it could be attached to other controls without errors.
- Finally it will manage the “GotFocus” event, forcing to select all the content.
The method is referenced in the PropertyMetadata argument of the RegisterAttached method.
Once the Attacched Property is set, we must reference in the ResourceDictionary:
xmlns:apt="clr-namespace:Styles.Attacched_Properties"
and then add the property to the style:
<Setter Property="apt:AttacchedProperties.SelectAllOnEntry" Value="True" />
As you can see, the option is enabled by defult.
We can disable this function, just by adding the reference to the attacched property on the TextBox directly BEFORE declaring the Style:
<TextBox aps:AttacchedProperties.SelectAllOnEntry="False" x:Name="txText" Text="{Binding ExampleText}" Style="{StaticResource ResourceKey=TextBlockBase}"/>
Possibility to show a little red triangle on the Top-Right
We use this function to identify mandatory fields. Again, we use an attached property to define manually which fields are mandatory and which not. The attached property this time is a classic bool property:
public static readonly DependencyProperty IsMandatoryProperty = DependencyProperty.RegisterAttached("IsMandatory", typeof(bool), typeof(AttacchedProperties), new PropertyMetadata(default(bool))); public static bool GetIsMandatory(DependencyObject d) { return (bool)d.GetValue(IsMandatoryProperty); } public static void SetIsMandatory(DependencyObject d, bool value) { d.SetValue(IsMandatoryProperty, value); }
The xaml part is a bit more difficult. Indeed we need to modify the Template of the TextBox:
<Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type TextBox}"> <Border x:Name="border" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" SnapsToDevicePixels="True"> <Grid> <Polygon x:Name="IsMandatoryPolygon" Points="0,0 8,0 0,8 0,0" Margin="0,2,2,0" HorizontalAlignment="Right" Fill="Red" FlowDirection="RightToLeft" Visibility="Collapsed"/> <ScrollViewer x:Name="PART_ContentHost" Focusable="false" HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Hidden"/> </Grid> </Border> <ControlTemplate.Triggers> <Trigger Property="apt:AttacchedProperties.IsMandatory" Value="True"> <Setter Property="Visibility" TargetName="IsMandatoryPolygon" Value="Visible"/> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Setter.Value> </Setter>
As you can see, this template consists in a Border that contains a Polygon and a ScrollViewer. ScrollViewer has all the scrollbars hidden and it is used a “container” for it’s “host” (see it’s name set to the special keyword “PART_ContentHost”).
The enabling/disabling job is made by the trigger on the control tamplate that maps the attached property “IsMandatoryPolygon”.
Show an inside explanatory label
We use this function to give a feedback to the user on the type of data should be inserted inside the filed. Again it is a possibility and again we use a simple sting attached property to store the text.
public static readonly DependencyProperty ExampleLabelProperty = DependencyProperty.RegisterAttached("ExampleLabel", typeof(string), typeof(AttacchedProperties), new PropertyMetadata(default(string))); public static string GetExampleLabel(DependencyObject d) { return (string)d.GetValue(ExampleLabelProperty); } public static void SetExampleLabel(DependencyObject d, string value) { d.SetValue(ExampleLabelProperty, value); }
For the xaml part, we just added a Label and a template multitrigger:
<Label Margin="5,0,0,0" x:Name="WaterMarkLabel" Content="{TemplateBinding apt:AttacchedProperties.ExampleLabel}" VerticalAlignment="Center" Visibility="Collapsed" Foreground="Gray" FontFamily="Verdana" FontSize="7" FontWeight="Bold"/> <MultiTrigger> <MultiTrigger.Conditions> <Condition Property="Text" Value=""/> </MultiTrigger.Conditions> <Setter Property="Visibility" TargetName="WaterMarkLabel" Value="Visible"/> </MultiTrigger>
In this case the attached property is used for the Label Content and the Visibility is collapsed. In this way, the visibility of the Label depends directly on the presence of some text in the TextBox (managed by multitrigger): if there is no text is in the Textbox, the visibility of the Label turns to Visible.
Show Validation Allert with error in the ToolTip
In case of incorret data insied a filed, we will show an exclamation mark with the error reported in the TextBox ToolTip. For the Exclamation mark we did not use an image, but a polygon ans a TextBlock.
For this functinality we have to edit the Validation.ErrorTemplate, that is based on an Adorner:
<Setter Property="Validation.ErrorTemplate"> <Setter.Value> <ControlTemplate> <Grid > <AdornedElementPlaceholder/> <Border Background="Red" CornerRadius="7" Height="14" HorizontalAlignment="Right" Margin="0,2,7,2" VerticalAlignment="Center" Width="14"> <TextBlock FontFamily="Verdana" FontSize="11" FontWeight="Bold" Foreground="White" HorizontalAlignment="Center" Text="!" VerticalAlignment="Center" /> </Border> </Grid> </ControlTemplate> </Setter.Value> </Setter> ... <Trigger Property="Validation.HasError" Value="true"> <Setter Property="ToolTip" Value="{Binding Path=(Validation.Errors)[0].ErrorContent, RelativeSource={x:Static RelativeSource.Self}}"/> </Trigger>
AdornedElemntPlaceholder is used as Placeholder that pushes the exclamation point all on the right. Finally we ‘ve added a trigger in the Style triggers to manage the content of the ToolTip.
Put all together
Here the final result of the style:
<Style x:Key="TextBoxBase" TargetType="{x:Type TextBox}" BasedOn="{StaticResource {x:Type TextBox}}"> <Setter Property="apt:AttacchedProperties.SelectAllOnEntry" Value="True" /> <Setter Property="BorderBrush" Value="DarkGray" /> <Setter Property="BorderThickness" Value="1"/> <Setter Property="FontFamily" Value="Segoe UI"/> <Setter Property="FontSize" Value="13"/> <Setter Property="Height" Value="Auto"/> <Setter Property="Margin" Value="1,3,3,3"/> <Setter Property="Padding" Value="3"/> <Setter Property="VerticalAlignment" Value="Center"/> <Setter Property="VerticalContentAlignment" Value="Center"/> <Setter Property="Validation.ErrorTemplate"> <Setter.Value> <ControlTemplate> <Grid > <AdornedElementPlaceholder/> <Border Background="Red" CornerRadius="7" Height="14" HorizontalAlignment="Right" Margin="0,2,7,2" VerticalAlignment="Center" Width="14"> <TextBlock FontFamily="Verdana" FontSize="11" FontWeight="Bold" Foreground="White" HorizontalAlignment="Center" Text="!" VerticalAlignment="Center" /> </Border> </Grid> </ControlTemplate> </Setter.Value> </Setter> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type TextBox}"> <Border x:Name="border" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" SnapsToDevicePixels="True"> <Grid> <Polygon x:Name="IsMandatoryPolygon" Points="0,0 8,0 0,8 0,0" Margin="0,2,2,0" HorizontalAlignment="Right" Fill="Red" FlowDirection="RightToLeft" Visibility="Collapsed"/> <ScrollViewer x:Name="PART_ContentHost" Focusable="false" HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Hidden"/> <Label Margin="5,0,0,0" x:Name="WaterMarkLabel" Content="{TemplateBinding apt:AttacchedProperties.ExampleLabel}" VerticalAlignment="Center" Visibility="Collapsed" Foreground="Gray" FontFamily="Verdana" FontSize="7" FontWeight="Bold"/> </Grid> </Border> <ControlTemplate.Triggers> <MultiTrigger> <MultiTrigger.Conditions> <Condition Property="Text" Value=""/> </MultiTrigger.Conditions> <Setter Property="Visibility" TargetName="WaterMarkLabel" Value="Visible"/> </MultiTrigger> <Trigger Property="IsEnabled" Value="False"> <Setter Property="Foreground" Value="DimGray"/> </Trigger> <Trigger Property="apt:AttacchedProperties.IsMandatory" Value="True"> <Setter Property="Visibility" TargetName="IsMandatoryPolygon" Value="Visible"/> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Setter.Value> </Setter> <Style.Triggers> <Trigger Property="IsFocused" Value="true"> <Setter Property="Background" Value="LightYellow"/> </Trigger> <Trigger Property="Validation.HasError" Value="true"> <Setter Property="ToolTip" Value="{Binding Path=(Validation.Errors)[0].ErrorContent, RelativeSource={x:Static RelativeSource.Self}}"/> </Trigger> </Style.Triggers> </Style>
How to use it
To use the style you have to:
- Reference the dictionary in your container window/control (or you can reference it at App Level, having it public for all the app).
- Referente the class that contains the attached properties in your container window/control.
- Add a TextBox control, set up the desired attached properties and then apply the style, as in the following example:
<TextBox x:Name="txUsername" aps:AttacchedProperties.SelectAllOnEntry="False" aps:AttacchedProperties.ExampleLabel="UserId used for Database connection" aps:AttacchedProperties.IsMandatory="True" Text="{Binding DataModel.User, UpdateSourceTrigger=PropertyChanged}" Style="{StaticResource ResourceKey=TextBoxBase}"/>
Bibliography
–