Framework 4.0
I recently got a requirement to apply validation to a textbox based on the content of the textbox and
another textbox. This was way more difficult to implement than I thought it would be when I foolishly said it wouldn't take long.
I reduced the requirement to its simplest form for this blog. I have two text boxes. If the text of the second textbox is not included in the text of the first textbox the second textbox will fail validation.
Let's start with a simple validator that works with an unbound value for "String Value"...
Start a new WPF Application (VB today), call the project "ContainsDemo" and add a new class called Validators. Here is the code for the first cut at our validator.
Public Class ContainsValidator
Inherits ValidationRule
Public Property StringValue As String
Public Overloads Overrides Function Validate(value As Object, cultureInfo As Globalization.CultureInfo) As ValidationResult
Try
If StringValue.Contains(value.ToString) Then
Return New ValidationResult(True, "")
Else
Return New ValidationResult(False, "Substring not in string value")
End If
Catch e As Exception
Return New ValidationResult(False, e.Message)
End Try
End Function
End Class
Build the project before you continue.
As you can see, the validator has a property called StringValue which can be set in MainWindow.xaml as shown below.
<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:ContainsDemo" Title="MainWindow" Height="350" Width="525"
DataContext="{Binding RelativeSource={RelativeSource Self}}">
<Window.Resources>
<local:ContainsValidator x:Key="ContainsValidator" ></local:ContainsValidator>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto"></RowDefinition>
<RowDefinition Height="auto"></RowDefinition>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"></ColumnDefinition>
<ColumnDefinition Width="auto"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.Column="0" Content="String Value"></Label>
<TextBox Grid.Row="0" Grid.Column="1" Name="txtStringValue" Text="{Binding Path=StringValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Width="200"></TextBox>
<Label Grid.Row="1" Grid.Column="0" Content="Substring"></Label>
<TextBox Grid.Row="1" Grid.Column="1" Name="txtSubstring" Width="200">
<TextBox.Text>
<Binding Path="Substring" UpdateSourceTrigger="PropertyChanged" Mode="TwoWay">
<Binding.ValidationRules>
<local:ContainsValidator StringValue="ABCDEFG"/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
</Grid>
</Window>
Here is the XAML.VB file
Class MainWindow
Public Property StringValue As String = "ABCDEFG"
Public Property Substring As String = "CDE"
End Class
If you run the project you can see that the validation works but we haven't met the requirement of validating against a bound property.
At this point, if we want to bind the ContainsValidator StringValue we might be tempted to try something like...
<local:ContainsValidator StringValue="{Binding Path=StringValue}"/>
but if you do that you will see an error message saying that you can only bind to a DependencyProperty of a DependencyObject. Our validator already inherits from ValidationRule so it can't also inherit from DependencyObject - VB and C# do not allow multiple inheritance.
So we have to alter the validator to implement a public property that is a class that inherits DependencyObject. We can bind to that property but it gets complicated.
Let's start by adding a new class to the validator that implements DependencyObject and registering a DependencyProperty. They we can add a public property that instantiates the new class. The Validator code ends up looking like this...
Imports System.Globalization
Public Class ContainsValidator
Inherits ValidationRule
Public Property oStringValue As cStringValue
Public Sub New()
_oStringValue = New cStringValue()
End Sub
Public Overrides Function Validate(value As Object, cultureInfo As CultureInfo) As ValidationResult
Try
If _oStringValue.mStringValue.contains(value.ToString) Then
Return New ValidationResult(True, "")
Else
Return New ValidationResult(False, "Substring not in string value")
End If
Catch e As Exception
Return New ValidationResult(False, e.Message)
End Try
End Function
End Class
Public Class cStringValue
Inherits DependencyObject
Public Shared ReadOnly mStringValueProperty As DependencyProperty = DependencyProperty.Register("mStringValue", GetType(String), GetType(cStringValue))
Public Property mStringValue As String
Get
Return DirectCast(GetValue(mStringValueProperty), String)
End Get
Set(value As String)
SetValue(mStringValueProperty, value)
End Set
End Property
End Class
Now we can modify our XAML to bind to the new property. Change the txtSubstring textbox definition to look like this...
<TextBox Grid.Row="1" Grid.Column="1" Name="txtSubstring" Width="200">
<TextBox.Text>
<Binding Path="Substring" UpdateSourceTrigger="PropertyChanged" Mode="TwoWay">
<Binding.ValidationRules>
<local:ContainsValidator>
<local:ContainsValidator.oStringValue>
<local:cStringValue mStringValue="{Binding Path=StringValue}"></local:cStringValue>
</local:ContainsValidator.oStringValue>
</local:ContainsValidator>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
But if you try this you will see it doesn't really work. If you put a breakpoint in the Validate function of the validator you can see the value of mStringValue never gets set. Also, if you look in the output window you will see the familiar error message "Cannot find governing FrameworkElement or FrameworkContentElement for target element." It's complaining about the StringValue binding. So we fix it with a proxy element.
Add another window resource.
<FrameworkElement x:Key="ProxyElement" DataContext="{Binding}"></FrameworkElement>
Add a ContentControl at the bottom of the Grid.
<ContentControl Visibility="Collapsed" Content="{StaticResource ProxyElement}"></ContentControl>
Now we can bind StringValue via the datacontext of the proxy element by changing the binding to this.
<local:cStringValue mStringValue="{Binding Path=DataContext.StringValue, Source={StaticResource ProxyElement}}"></local:cStringValue>
Run it again and you will see it works pretty well and the error message is gone. However there is still one scenario that is a problem. If you invalidate the substring by appending an "X" and then make it valid by inserting an "X" into the string value after CDE so that it looks like "ABCDEXFG" the substring is still flagged as invalid. The problem is that the substring is not being revalidated when the string value changes. To do that, we need to implement INotifyPropertyChanged in the code behind so that it looks like this.
Imports System.ComponentModel
Class MainWindow
Implements INotifyPropertyChanged
Private _StringValue As String = "ABCDEFG"
Private _SubString As String = "CDE"
Public Property StringValue As String
Get
Return _StringValue
End Get
Set(value As String)
If _StringValue <> value Then
_StringValue = value
NotifyPropertyChanged("StringValue")
If txtSubstring.GetBindingExpression(TextBox.TextProperty).ValidateWithoutUpdate() Then
txtSubstring.GetBindingExpression(TextBox.TextProperty).UpdateSource()
End If
End If
End Set
End Property
Public Property Substring As String
Get
Return _SubString
End Get
Set(value As String)
If _SubString <> value Then
_SubString = value
NotifyPropertyChanged("Substring")
End If
End Set
End Property
Public Event PropertyChanged(sender As Object, e As PropertyChangedEventArgs) Implements INotifyPropertyChanged.PropertyChanged
Public Sub NotifyPropertyChanged(PropertyName As String)
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(PropertyName))
End Sub
End Class
Take a look at the setter for the StringValue property. After it calls NotifyPropertyChanged it re-validates the Substring property and updates the source if it is valid. It has to do this because the source is not updated if the target is invalid and if some other change causes the target to become value, the source will never get updated.
Now the substring is revalidated when the string value changes and, if it is valid, the txtSubstring text is saved to the Substring property.