Sunday, January 19, 2014

Binding validation rules and styles in code behind

Normally I prefer to do things in XAML because it's a little easier and there's less chance of a support programmer having to figure out how I did something. I have a requirement to make a textbox required. When a required textbox is empty I want both the textbox and its associated label to be highlighted.

This is fairly readily handled in XAML with a validation rule and a converter. We put a validation rule on the textbox and bind the color of the label to the text of the textbox using a converter. The XAML looks like this.


<Window x:Class="MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:WpfApplication13"
    Title="MainWindow" Height="350" Width="525"
    DataContext="{Binding RelativeSource={RelativeSource Self}}">
    <Window.Resources>
        <local:RequiredValidator x:Key="RequiredValidator"/>
        <local:RequiredColorConverter x:Key="RequiredColorConverter"/>
    </Window.Resources>
    <StackPanel Orientation="Horizontal" Height="30">
        <Label Name="DescriptionLabel" Content="Description:">
            <Label.Style>
                <Style TargetType="{x:Type Label}">
                    <Setter Property="Foreground" Value="{Binding ElementName=DescriptionTextBox, Path=(Validation.HasError), Converter={StaticResource RequiredColorConverter}}"/>
                </Style>
            </Label.Style>
        </Label>
        <TextBox Name="DescriptionTextBox" Width="200">
            <TextBox.Text>
                <Binding Path="Description" UpdateSourceTrigger="PropertyChanged" Mode="TwoWay">
                    <Binding.ValidationRules>
                        <StaticResource ResourceKey="RequiredValidator"/>
                    </Binding.ValidationRules>
                </Binding>
            </TextBox.Text>
        </TextBox>
    </StackPanel>
</Window>


--------------------------------------------------------

Class MainWindow 

    Public Property Description As String
    Public Sub New()
        InitializeComponent()
    End Sub
End Class

As you can see will still need to define our validation rule and converter, both of which are fairly trivial classes.


Public Class RequiredValidator
    Inherits ValidationRule

    Public Overloads Overrides Function Validate(value As Object, cultureInfo As Globalization.CultureInfo) As ValidationResult

        If String.IsNullOrWhiteSpace(value) Then
            Return New ValidationResult(False, "Error")
        Else
            Return New ValidationResult(True, Nothing)
        End If
    End Function
End Class

Public Class RequiredColorConverter
    Implements IValueConverter

    Public Function Convert(value As Object, targetType As Type, parameter As Object, culture As Globalization.CultureInfo) As Object Implements IValueConverter.Convert
        If CBool(value) Then
            Return New SolidColorBrush(System.Windows.Media.Colors.Red)
        Else
            Return New SolidColorBrush(System.Windows.Media.Colors.Black)
        End If
    End Function

    Public Function ConvertBack(value As Object, targetType As Type, parameter As Object, culture As Globalization.CultureInfo) As Object Implements IValueConverter.ConvertBack
        Throw New NotImplementedException("RequiredColorConverter")
    End Function
End Class

As you can see when you run the project, both the textbox and the label turn red when the textbox is empty. As a side note, I tried to bind the label to the (Validation.Errors) collection but when I did that Visual Studio kept crashing (bug?) I know you can use this in a trigger without a converter so I think for some reason you can't pass the Validation.Errors to a converter.

Well this all works fine but it's very repetitive if you have several mandatory fields in an application. Even if I created a Style I would need two styles, one for the label and one for the textbox. So I wondered how difficult it would be to do this in code instead. The idea would be to write a utility method that took a textbox and an optional label and add the validator and style in code.

    MakeTextboxRequired(EditDescriptionTextBox, EditDescriptionLabel)

It turns out that creating validation rules and styles in code is quite difficult and verbose.

Change the XAML to...
        <Label Name="DescriptionLabel" Content="Description:"></Label>
        <TextBox Name="DescriptionTextBox" Width="200" Text="{Binding Description}"></TextBox>


This is what the code looks like...


    Public Sub MakeTextboxRequired(oTextBox As TextBox, oLabel As Label)
        ' Attach a mandatory validation rule to the control
        ' Attach a binding to the color of the label

        Dim sElementName As String = oTextBox.Name
        Dim ValidationBinding As New Binding
        Dim RequiredValidator As ValidationRule = Me.FindResource("RequiredValidator")

        Try
            ValidationBinding.Path = New PropertyPath("Description")
            ValidationBinding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged
            ValidationBinding.ValidatesOnDataErrors = True
            ValidationBinding.NotifyOnValidationError = True
            ValidationBinding.ValidationRules.Add(RequiredValidator)
            oTextBox.SetBinding(TextBox.TextProperty, ValidationBinding)

            If oLabel IsNot Nothing Then
                Dim Style As New Style(GetType(Label))
                Dim ColorConverter As IValueConverter = Me.FindResource("RequiredColorConverter")
                Dim ColorBinding As New Binding
                ColorBinding.ElementName = sElementName
                ColorBinding.Path = New PropertyPath("(Validation.HasError)")
                ColorBinding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged
                ColorBinding.ValidatesOnDataErrors = True
                ColorBinding.NotifyOnValidationError = True
                ColorBinding.Converter = ColorConverter
                Style.Setters.Add(New Setter(Label.ForegroundProperty, ColorBinding))
                oLabel.Style = Style
            End If
        Catch ex As Exception
            Throw New Exception("MakeTextboxRequired: " & ex.Message)
        End Try
    End Sub

Now we can get rid of the styles in XAML and let the code do all the hard work.

I'm not saying this is a better approach, but it was an interesting exercise.

No comments:

Post a Comment