Saturday, July 26, 2014

More about Validation

WPF 4.0 has some powerful validation features which are readily declared in XAML. I have put together a small project that demonstrates some validation techniques and also highlights a curious issue.

Let's start by defining a very simple window with one text box that only accepts letters. If the user enters a non-letter the textbox validation will trigger an error which will cause the textbox to get a red outline. We will write a custom validator to do this.

Start a new WPF Application project in C# and call it "SimpleValidator". Change the XAML and code behind to look like this...

<Window x:Class="SimpleValidator.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:SimpleValidator" Title="MainWindow" Height="350" Width="525"> <Window.Resources> <local:User x:Key="theUser"/> </Window.Resources> <StackPanel Orientation="Horizontal" Height="30"> <Label>First Name:</Label> <TextBox Name="NameTextBox" Width="200"> <TextBox.Text> <Binding Source="{StaticResource theUser}" Path="Name" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged"> <Binding.ValidationRules> <local:OnlyLetterValidation/> </Binding.ValidationRules> </Binding> </TextBox.Text> </TextBox> </StackPanel> </Window> ---------------------------------------------------------------------------------------------- using System; using System.Linq; using System.Text; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Media; namespace SimpleValidator {
public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } } public class User { public string Name { get; set; } } public class OnlyLetterValidation : ValidationRule { public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo) { if (value.ToString().All(char.IsLetter)) return new ValidationResult(true, ""); else return new ValidationResult(false, "Only letters allowed"); } } }
Note the use of the String.All method (a linq extension). This is a cool technique for testing every character in a string. Also check out the Any method.

If you run this project you will see that as soon as you enter a non-letter the textbox border goes red to indicate that it has failed validation. If you put a breakpoint on the OldLetterValidation.Validate method you will see it is called whenever the user changes the value in the textbox. This is because UpdateSourceTrigger is set to "PropertyChanged" in the XAML. Validation occurs when the Source is updated. If you want to only validate when the user exits the field you should set UpdateSourceTrigger to "LostFocus".

At the moment we cannot tell the user why the textbox failed validation. There are at least three ways to do this.
  1. Bind a control to the Textbox's Validation.Errors collection. This is an attached property which exposes a collection of ValidationError objects. You could attach a list box's ItemsSource to the collection or attach a tooltip or textblock to it using a converter to convert the collection to a string.
  2. Mark the Textbox to raise Validation events when validation errors are added or removed and consume those events.
  3. Create a custom ErrorTemplate for the textbox. This is what we are going to do now.
Insert the following XAML after the </TextBox.Text> tag. Note the AdornedElementPlaceholder simply creates a placeholder for the UIElement that is being templated.

<Validation.ErrorTemplate> <ControlTemplate> <StackPanel Orientation="Horizontal"> <AdornedElementPlaceholder/> <TextBlock Text="{Binding [0].ErrorContent}" Foreground="Red"/> </StackPanel> </ControlTemplate> </Validation.ErrorTemplate>

Now run the application again and type a non-letter into the textbox. See how the error message is now displayed to the right of the text box.

Now let's add another validation rule. This will be a minimum length rule and will demonstrate how to create a validation with a parameter and how multiple validations work.

Start by adding a second validation class to the code behind. Add this underneath the existing OnlyLettersValidation class.

public class MinLengthValidation : ValidationRule { public int MinLength { get; set; } public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo) { if (value.ToString().Length < MinLength) return new ValidationResult(false, string.Format("At least {0} letters are required", MinLength)); else return new ValidationResult(true, ""); } }

This class exposes a public property which means the XAML can easily access that property, thus allowing parameters to be passed to validators. Note this is not a DependencyProperty so you cannot bind to it in XAML, although it can be done with more work. This is outside the scope of this blog entry.

Using the validator in XAML couldn't be much easier. Just add the following line at the top of the list of ValidationRules.

<Binding.ValidationRules> <local:MinLengthValidation MinLength="5"/> <local:OnlyLetterValidation/> </Binding.ValidationRules>

Run the project again and you will see that as soon as you start typing you see the minimum length error displayed. It only goes away when you have entered at least five letters.

But how do we tell the user when they first see the text box that it requires at least five characters. It should start in an error state because it is empty. We fix this with one line of code that explicitly initializes the text property of the textbox. Because we have two-way binding this causes the source to be updated. Validation occurs whenever the source is updated. Add the following line of code after the InitializeComponents line in the window's constructor.

public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); NameTextBox.Text = ""; } }

Run the project now and you will see the text box get validated before the page displays.

Now we have two validators. If you recall the textbox has an attached property called Validation.Errors which exposes a collection of ValidationError objects. However, if you put a breakpoint on each of our validators and type a value that fails both rules ie a space, you will see that validation stops at the first error. I cannot actually find a way to get more than one validation error in the Validation.Errors collection. Oh well!

There's one more thing I want to demonstrate in this blog entry. Sometimes we would like to change the color of another field, normally the prompt field, when validation fails. We can do this with binding although we do need a converter to convert from a boolean to a brush.

Let's start by adding a simply converter to our code behind.

public class ValidationConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { if (value is Boolean) { if ((Boolean)value) return new SolidColorBrush(Colors.Red); else return new SolidColorBrush(Colors.Black); } return DependencyProperty.UnsetValue; } public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { throw new NotImplementedException(); } }

Now we need to add a reference to the converter in the Windows.Resources section like this.

<Window.Resources> <local:User x:Key="theUser"/> <local:ValidationConverter x:Key="ValidationConverter"/> </Window.Resources>

And finally we bind the foreground property of our label using this rather intimidating binding clause. Notice the Path attribute has parentheses around it because Validation is an attached property.

<Label Grid.Row="0" Grid.Column="0" Foreground="{Binding ElementName=NameTextBox, Path=(Validation.HasError), Converter={StaticResource ValidationConverter}}">First Name: </Label>

Now run the project one more time and you will see the foreground color of the label turns red whenever the textbox fails validation. You could just have easily have changed the tooltip using a converter that grabbed the text of the first validation error, etc.

Wednesday, July 23, 2014

Bug when styling DatagridCheckBoxColumn

There is a bug in WPF 4.0 that breaks the IsReadOnly property of the DataGridCheckBoxColumn when it is styled. To see the problem, create a new WPF project with the following XAML and code behind.
<Window x:Class="MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="350" Width="525" DataContext="{Binding RelativeSource={RelativeSource Self}}"> <Window.Resources> <Style x:Key="CheckBox" TargetType="{x:Type CheckBox}"> <Setter Property="Background" Value="Aqua"/> </Style> </Window.Resources> <StackPanel Orientation="Vertical"> <CheckBox Name="DisableGridCheckBox" Content="Disable Grid"/> <DataGrid Name="DataGrid" AutoGenerateColumns="False" ItemsSource="{Binding DataSource}" IsReadOnly="{Binding ElementName=DisableGridCheckBox, Path=IsChecked}"> <DataGrid.Columns> <DataGridTextColumn x:Name="TextColumn" Header="Text" Binding="{Binding TextField}"/> <DataGridCheckBoxColumn x:Name="UnstyledCheckboxColumn" Header="Unstyled" Binding="{Binding CheckBox1}"/> <DataGridCheckBoxColumn x:Name="StyledCheckboxColumn" Header="Styled" Binding="{Binding CheckBox2}" ElementStyle="{StaticResource CheckBox}"/> </DataGrid.Columns> </DataGrid> </StackPanel> </Window> --------------------------------------------------------------------------- Imports System.Data Class MainWindow Public Property DataSource As New DataTable Public Sub New() DataSource.Columns.Add(New DataColumn("TextField", GetType(String))) DataSource.Columns.Add(New DataColumn("CheckBox1", GetType(Boolean))) DataSource.Columns.Add(New DataColumn("CheckBox2", GetType(Boolean))) DataSource.Rows.Add("Text", True, False) InitializeComponent() End Sub End Class
When you run this project you will see an editable grid with a text box, an unstyled checkbox and a styled checkbox (with a pretty aqua background). Modify the text and check the checkboxes to verify that they have inherited the grid's IsReadOnly property.

Now check the top checkbox labeled "Disable Grid". This will put the grid into ReadOnly mode. Verify you cannot change the text or check the unstyled checkbox. However you can still check the styled checkbox. This is a bug.

Styling the checkbox has broken the IsHitTestVisible property (and others too, such as the Horizontal Alignment). To fix this, you need to alter the style to bind the IsHitTestVisible property to the parent datagrid's IsReadOnly property. The easiest way to do this is with a DataTrigger. Add the following to the style. Note that when the parent DataGrid.IsReadOnly is true we want the Checkbox.IsHitTestVisible to be false.

<Setter Property="IsHitTestVisible" Value="True"/> <Style.Triggers> <DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType=DataGrid}, Path=IsReadOnly}" Value="True"> <Setter Property="IsHitTestVisible" Value="False"/> </DataTrigger> </Style.Triggers>

Wednesday, July 16, 2014

Subtle effect of UpdateSourceTrigger

I'm writing my own numeric edit box by styling a standard textbox. Everything was going well until I notices that removing the first zero from "0.00" was causing problems. On the screen the textbox showed ".00" but the textbox text property during the PreviewKeyDown event was still "0.00" (that's in the keydown event after the one that deleted the zero). After a lot of digging I realized the problem was that the textbox had UpdateSourceTrigger=PropertyChanged.

Try out the following XAML and VB to see the problem. Run the project with the Output window visible. Put the cursor after the first zero in the top textbox and press backspace, then cursor right. The textbox will show ".00" but the TextBox.Text property will be "0.00". Interestingly the TextBox.GetLineText(0) method returns the correct value.

Now do the same thing with the bottom TextBox and you will see the TextBox.text property now shows the correct value. The only difference is the UpdateSourceTrigger value.


<Window x:Class="MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="350" Width="525" DataContext="{Binding RelativeSource={RelativeSource Self}}"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="30"></RowDefinition> <RowDefinition Height="30"></RowDefinition> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="120"></ColumnDefinition> <ColumnDefinition Width="120"></ColumnDefinition> </Grid.ColumnDefinitions> <Label Grid.Row="0" Grid.Column="0" Content="PropertyChanged"></Label> <TextBox Grid.Row="0" Grid.Column="1" Text="{Binding Path=Decimal0, Mode=TwoWay, StringFormat={}{0:0.00}, UpdateSourceTrigger=PropertyChanged}" PreviewKeyDown="BoundTextBox_KeyDown"></TextBox> <Label Grid.Row="1" Grid.Column="0" Content="LostFocus"></Label> <TextBox Grid.Row="1" Grid.Column="1" Text="{Binding Path=Decimal1, Mode=TwoWay, StringFormat={}{0:0.00}, UpdateSourceTrigger=LostFocus}" PreviewKeyDown="BoundTextBox_KeyDown"></TextBox> </Grid> </Window> Class MainWindow Public Property Decimal0 As Decimal = 0 Public Property Decimal1 As Decimal = 0 Private Sub BoundTextBox_KeyDown(sender As System.Object, e As System.Windows.Input.KeyEventArgs) Dim oTextBox As TextBox = DirectCast(e.OriginalSource, TextBox) Trace.WriteLine("Text=[" & oTextBox.Text & "], Line(0)=[" & oTextBox.GetLineText(0) & "]") End Sub End Class
The problem is that the Textbox is attempting to update the backing store Decimal value every time the value changes. When the content of the textbox is a valid decimal it gets stored and reformatted so that the Text property does not match what's visible on the screen.

Surely this is a bug.

This behavior can be altered by either setting UpdateSourceTrigger=LostFocus (or Explicit) or setting Mode=OneWay.

Thursday, July 10, 2014

Problem when two-way binding radio buttons using a converter

In an earlier post I showed how to use a converter to bind radio buttons but I made a subtle error in the ConvertBack code that caused havoc.

Here is the ConvertBack method as I originally wrote it.

Public Function ConvertBack(value As Object, targetType As System.Type, parameter As Object, culture As System.Globalization.CultureInfo) As Object Implements System.Windows.Data.IValueConverter.ConvertBack ' Return the parameter if the button is checked If value Is Nothing Or parameter Is Nothing Then Return Nothing End If Dim IsChecked As Boolean = False Boolean.TryParse(value, IsChecked) If IsChecked Then Return parameter Else Return Nothing End If End Function
The symptoms were that occasionally the source of the binding was being set to DBNull and saved to the database. It should always be "1" or "2".

After much head-scratching and cursing of Microsoft I noticed the problem occurred when I checked the first radio button in the group.

This causes the ConvertBack method to be called for the first radio button with a parameter of "1". Because the radio button is checked the ConvertBack method returns "1" and the source is set to "1".

Then the ConvertBack method is called for the second radio button with a parameter of "2". Because the second radio button is not checked the ConvertBack method returns nothing which causes the source to be set to DBNull.

The solution is to change both "Return Nothing" lines to "Return DependencyProperty.UnsetValue". This tells the framework not to change the source value, which leaves it as "1".

Undoing a LostFocus event

I have a requirement to detect an invalid value as a user tabs off a textbox, display a messagebox, reset the value in the textbox, and move focus back to the textbox.

The obvious solution is to handle the LostFocus event. My first attempt at this is shown below

Private Sub OverallDiscountPercentTextBox_LostFocus(sender As System.Object, e As System.Windows.RoutedEventArgs) Dim OverallDiscountAmount As Decimal = 0 Decimal.TryParse(OverallDiscountPercentTextBox.Text, OverallDiscountPercent) If OverallDiscountPercent > 100 Then MessageBox.Show("Discount cannot be more than 100%.", "Discount Error", MessageBoxButton.OK, MessageBoxImage.Error) OverallDiscountPercentTextBox.Text = 0.ToString("F2") OverallDiscountPercentTextBox.Focus Exit Sub End If ... End Sub
This does not move the focus back to the textbox. Clearly the framework has not finished moving the focus to the next control when the LostFocus event is raised. I suppose this doesn't happen until the next control's Focus event is raised.

The solution is fairly simple once you understand how to write lambda functions in Visual Basic. You defer the Focus method like this...

Dispatcher.BeginInvoke(Function() OverallDiscountPercentTextBox.Focus(), Windows.Threading.DispatcherPriority.Background)

Friday, July 4, 2014

Creating Styles and Binding Converters in code

For WPF 4.0

Recently a colleague of mine has been struggling to write generic code for assigning converters to control the background of a DataGridTextColumn. I pointed him to my April 24th 2014 blog entry entitled "NamedObject Error in Converters" which starts off by showing how to utilize a foreground and background converter in XAML for a DataGridTextColumn (and then goes on to break them).

He got that working but could not determine the code for implementing those same converters in code. While I don't normally feel the need to do something in code that can be done in XAML, I recognize that there are times when you don't know everything at design time and things have to be delayed to run time.

It really isn't too difficult to do this. You start by assigning an ElementStyle to the DataGridColumn, then creating a binding for the background and a multibinding for the foreground and binding them to the ElementStyle. The correlation between the code and the XAML it replaces is easy to see.

Here's the XAML with the style, bindings, and converters removed...


<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:WpfApplication6"
        Title="MainWindow" Height="350" Width="525" 
        
        DataContext="{Binding RelativeSource={RelativeSource Self}}">
    <Window.Resources>
        <local:ForegroundConverter x:Key="ForegroundConverter"></local:ForegroundConverter>
        <local:BackgroundConverter x:Key="BackgroundConverter"></local:BackgroundConverter>
    </Window.Resources>
    <Grid>
        <DataGrid Name="DG" ItemsSource="{Binding Path=DT.DefaultView}" AutoGenerateColumns="False" CanUserAddRows="False">
            <DataGrid.Columns>
                <DataGridTextColumn x:Name="Description" Header="Description" Binding="{Binding Path=Description}" IsReadOnly="true"/>
            </DataGrid.Columns>
        </DataGrid>
    </Grid>
</Window>


The converters stay the same as you move from XAML to code...


Public Class ForegroundConverter
    Implements IMultiValueConverter

    Public Function Convert(values() As Object, targetType As System.Type, parameter As Object, culture As System.Globalization.CultureInfo) As Object Implements System.Windows.Data.IMultiValueConverter.Convert

        Dim IsError As Boolean = values(0)
        Dim IsWarning As Boolean = values(1)

        If IsError Then
            Return New SolidColorBrush(System.Windows.Media.Colors.Red)
        End If

        If IsWarning Then
            Return New SolidColorBrush(System.Windows.Media.Colors.Blue)
        End If

        Return New SolidColorBrush(System.Windows.Media.Colors.Black)

    End Function

    Public Function ConvertBack(value As Object, targetTypes() As System.Type, parameter As Object, culture As System.Globalization.CultureInfo) As Object() Implements System.Windows.Data.IMultiValueConverter.ConvertBack
        Throw New NotImplementedException()
    End Function
End Class

Public Class BackgroundConverter
    Implements IValueConverter

    Public Function Convert(value As Object, targetType As System.Type, parameter As Object, culture As System.Globalization.CultureInfo) As Object Implements System.Windows.Data.IValueConverter.Convert

        If CBool(value) Then
            Return New SolidColorBrush(System.Windows.Media.Colors.Yellow)
        Else
            Return New SolidColorBrush(System.Windows.Media.Colors.White)
        End If
    End Function

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


And lastly here is the code that replaces the XAML. Note that about eight lines of simple XAML has been replaced by 13 lines of difficult to read code. And for anyone who might point out that code is reusable, I would point out that styles make XAML just as reusable.

Notice I do not attempt to set the Source property of the bindings. The binding will use the ItemsSource property of the column's DataGrid which itself makes use of the DataContext of the window. If we were trying to control properties of UIElements such as a Label we would need to set the binding's Source property ie Binding.Source = DT.Rows(0).

Imports System.Data

Class MainWindow

    Public Property DT As DataTable

    Public Sub New()

        ' Create and populate the DataContext
        DT = New DataTable

        DT.Columns.Add(New DataColumn("Description", Type.GetType("System.String")))
        DT.Columns.Add(New DataColumn("IsError", Type.GetType("System.Boolean")))
        DT.Columns.Add(New DataColumn("IsWarning", Type.GetType("System.Boolean")))
        DT.Columns.Add(New DataColumn("IsImportant", Type.GetType("System.Boolean")))

        DT.Rows.Add("Error", True, False, True)
        DT.Rows.Add("Warning", False, True, False)
        DT.Rows.Add("Info", False, False, True)

        InitializeComponent()

        ' Create ElementStyle
        Dim s As New Style(GetType(TextBlock))
        Description.ElementStyle = s

        ' Create background setter
        Dim bBinding As New Binding()
        Dim BackgroundConverter As Object = FindResource("BackgroundConverter")
        bBinding.Path = New System.Windows.PropertyPath("IsImportant")
        bBinding.Converter = BackgroundConverter
        s.Setters.Add(New Setter(TextBlock.BackgroundProperty, bBinding))

        ' Create Foreground setter
        Dim fBinding As New MultiBinding()
        Dim ForegroundConverter As Object = FindResource("ForegroundConverter")
        fBinding.Bindings.Add(New Binding("IsError"))
        fBinding.Bindings.Add(New Binding("IsWarning"))
        fBinding.Converter = ForegroundConverter
        s.Setters.Add(New Setter(TextBlock.ForegroundProperty, fBinding))

    End Sub
End Class