Numeric data binding validation #3

Let’s enhance our Validation Behavior to make it ready for use in real-life applications!

Styling:

The first thing we should get rid of is the fixed color values defined within the behavior’s TextChanged event handler method. Instead, we can define two new visual state identifiers:

public const string VisualStateValid = "DataValid";
public const string VisualStateInvalid = "DataInvalid";

…let the users of our behavior specify these visual states if they want to, and just activate these from within the behavior code:

private void TextBoxOnTextChanged(object sender, TextChangedEventArgs args)
{
	var tb = sender as TextBox;
	if (tb != null)
	{
		try
		{
			var dummy = Convert.ChangeType(tb.Text, ValidationType);
		}
		catch (Exception)
		{
			VisualStateManager.GoToState(tb, VisualStateInvalid, true);
			return;
		}
	}
	VisualStateManager.GoToState(tb, VisualStateValid, true);
}

To make use of this new feature, we would retrieve the TextBox default style using either Blend or MSDN, and adapt it to, for example, change its border color:

<VisualStateGroup x:Name="ValidationStates">
	<VisualState x:Name="DataValid" />
	<VisualState x:Name="DataInvalid">
		<Storyboard>
			<ObjectAnimationUsingKeyFrames Storyboard.TargetName="BorderElement" Storyboard.TargetProperty="BorderBrush">
				<DiscreteObjectKeyFrame KeyTime="0" Value="Red" />
			</ObjectAnimationUsingKeyFrames>
		</Storyboard>
	</VisualState>
</VisualStateGroup>

Note that the big advantage of this approach is that the DataValid state need not explicitly be defined in XAML, therefore the tex box will return to its default state when containing valid data.

Generic types:

To make our behavior work with decimal numeric types, two issues need to be resolved:

  • Let users (meaning developers using the behavior) define the desired validation data type, and
  • get rid of the int.Parse(tb.Text) code line.

The first issue can be handled by defining a dependency property of type Type that can be set in XAML while attaching the behavior:

public static readonly DependencyProperty ValidationTypeProperty = DependencyProperty.Register(
	"ValidationType", typeof(Type), typeof(ValidationBehavior), new PropertyMetadata(default(Type)));

public Type ValidationType
{
	get { return (Type) GetValue(ValidationTypeProperty); }
	set { SetValue(ValidationTypeProperty, value); }
}

This allows us to change the target type directly in XAML:

<TextBox Text="{Binding Number, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" x:Name="Box">
	<interactivity:Interaction.Behaviors>
		<behaviors:ValidationBehavior ValidationType="x:Double" />
	</interactivity:Interaction.Behaviors>
</TextBox>

Of course, we also need to process this type setting within the TextChanged event handler. This is easily done by replacing int.Parse(...) with the generic Convert.ChangeType() method:

try
{
	var dummy = Convert.ChangeType(tb.Text, ValidationType);
}
catch (Exception)
{
	VisualStateManager.GoToState(tb, VisualStateInvalid, true);
	return;
}

Business logic validation:

Notifying the user about invalid input is one thing, but at the same time you might wish to forward the current validation state to the ViewModel layer, to integrate it into business layer validation – for example, a Command might be disabled as long as a numeric text box contains invalid data. This can be done by defining a second bindable dependency property:

public static readonly DependencyProperty IsInvalidProperty = DependencyProperty.Register(
	"IsInvalid", typeof(bool), typeof(ValidationBehavior), new PropertyMetadata(default(bool)));

public bool IsInvalid
{
	get { return (bool) GetValue(IsInvalidProperty); }
	set { SetValue(IsInvalidProperty, value); }
}

private void TextBoxOnTextChanged(object sender, TextChangedEventArgs args)
{
	var tb = sender as TextBox;
	if (tb != null)
	{
		try
		{
			var dummy = Convert.ChangeType(tb.Text, ValidationType);
		}
		catch (Exception)
		{
			IsInvalid = true;
			VisualStateManager.GoToState(tb, VisualStateInvalid, true);
			return;
		}
	}
	IsInvalid = false;
	VisualStateManager.GoToState(tb, VisualStateValid, true);
}

The following XAML sample assumes that the ViewModel contains a public bindable IsNumberInvalid property of type bool. The Mode=TwoWay attribute is necessary to ensure that the value is passed towards the ViewModel (a default one-way binding would try to update the View from the ViewModel):

<TextBox Text="{Binding Number, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" x:Name="Box">
	<interactivity:Interaction.Behaviors>
		<behaviors:ValidationBehavior ValidationType="x:Double" IsInvalid="{Binding IsNumberInvalid, Mode=TwoWay}"/>
	</interactivity:Interaction.Behaviors>
</TextBox>

Within the page’s ViewModel, we can now reference IsNumberInvalid from a Command’s CanExecute condition, for example.

Let’s play!

By now, the behavior should be ready for use. Start the sample app and watch the TextBox as you enter text!

Valid numeric TextBox content

Invalid numeric TextBox content