Home > Validation > Checking Property Types Automatically

Checking Property Types Automatically


It’s been a long time since I have written in this blog. The reason is: there have been many changes in my life during the past two years, and I simply haven’t found the time until now. Anyway, I’ve been checking out things and learning continuously, and I hope to be able to share these things more regularly in the future.

In the last post, Validating Objects with ValidationRules, I proposed a way to define ValidationRules for an object in one location and to set them on all Bindings that need them through a single attached property.

Today, I want to show how to set up basic type checks automatically. The example code is contained in the ValidationExample project in the source code distribution, which you can access through the downloads page.

How WPF handles Type

Normally, if the value entered in a TextBox cannot be converted into the type of the bound property, e.g. if you enter letters into a TextBox expecting an integer value, first of all there will be a runtime exception. By default, WPF will handle this exception, mark the TextBox as having invalid data, and assign a default “Conversion error” error message to it. Since the value cannot be converted, WPF will not update the bound property, which means that it will keep its value unchanged.

The first level of refinement here would be to allow the Binding to display exception messages by setting ValidatesOnExceptions to true. Now, if changing a property causes an exception, Validation.Errors will contain the exception’s message. Now the user will at least know whether the reason for the exception is a type mismatch or a numerical overflow.

Validation as a User Interface Responsibility

Why is this not enough?

Reusable software components like services or libraries need to check their input and throw exceptions if it is invalid. This is how one piece of software tells another piece of software that it has done something wrong.

User Interfaces, which basically translate between human users and the software they want to use, are responsible for making sure that those users only provide valid input values. The best way to do that is to offer only valid options to the user. Where that isn’t possible, validation steps in by providing explicit and meaningful validation messages which tell the users what they have to change in order to achieve the intended result.

As a rule of thumb, I’d say that one should protect one’s services and libraries by throwing exceptions, and that one should use validation in the UI to make sure that those exceptions never get thrown.

So, while exceptions are directed at technically aware developers debugging their software components, validation messages are directed at end users with possibly no technical background at all. For example, the error message you receive when you enter too many digits into the TextBox that validates with exceptions in the example project, reads: “Value was either too large or too small for an Int32”. This is obviously not what one would display to a user who has never heard of Int32.

Therefore, in the user interface, one would need two functionalities:

  1. a way to provide globalized validation messages independent of the system’s exception messages.
  2. a way to keep WPF controls from trying to update properties with values of non-matching type.

The TypeValidationRuleSetter Class

The TypeValidationRuleSetter class is a subclass of the ValidationRuleSetter class I mentioned in the last post. In addition to adding predefined ValidationRules to specified properties on a bound object, it will inspect the object’s type, find out the primitive CLR-type of a bound property, and will add default validation rules for each type which use XAML-configurable validation messages.

At the heart of the TypeValidationRuleSetter class is the following method:

Protected Overrides Sub AttachRules(ByVal base As Object)
    Dim f As FrameworkElement = TryCast(base, FrameworkElement)
    If f IsNot Nothing And ReferenceType IsNot Nothing Then
        If f IsNot Nothing Then
            Dim bg As BindingGroup = f.BindingGroup
            If bg IsNot Nothing Then
                For Each be As BindingExpression In bg.BindingExpressions.OfType(Of BindingExpression)()
                    Dim propertyName = be.ParentBinding.Path.Path
                    Dim propertyInfo As System.Reflection.PropertyInfo = ReferenceType.GetProperty(propertyName)
                    Select Case propertyInfo.PropertyType
                        Case GetType(DateTime)
                            be.ParentBinding.ValidationRules.Add(New DateValidationRule(Me.DateMessage))
                        Case GetType(Int16)
                            be.ParentBinding.ValidationRules.Add(New IntegerValidationRule(Int16.MinValue, Int16.MaxValue, Me.IntegerMessage))
                        Case GetType(Int32)
                            be.ParentBinding.ValidationRules.Add(New IntegerValidationRule(Int32.MinValue, Int32.MaxValue,Me.IntegerMessage))
                        Case GetType(Int64)
                            be.ParentBinding.ValidationRules.Add(New IntegerValidationRule(Int64.MinValue, Int64.MaxValue, Me.IntegerMessage))
                        Case GetType(Single)
                            be.ParentBinding.ValidationRules.Add(New NumericValidationRule(Single.MinValue, Single.MaxValue,Me.NumericMessage))
                        Case GetType(Double)
                            be.ParentBinding.ValidationRules.Add(New NumericValidationRule(Double.MinValue, Double.MaxValue, Me.NumericMessage))
                        Case GetType(Decimal)
                            be.ParentBinding.ValidationRules.Add(New NumericValidationRule(Decimal.MinValue, Decimal.MaxValue, Me.NumericMessage))
                        Case GetType(Integer)
                            be.ParentBinding.ValidationRules.Add(New IntegerValidationRule(Integer.MinValue, Integer.MaxValue, Me.IntegerMessage))
                    End Select
                    If Setters.Contains(propertyName) Then
                        For Each v As System.Windows.Controls.ValidationRule In Setters.Item(propertyName).ValidationRules
                            be.ParentBinding.ValidationRules.Add(v)
                        Next
                    End If
                Next
            End If
        End If
    End If
End Sub

 

This method will be called during the Initialized event, when Bindings have been established but still can be modified. It inspects the type of the property and adds one of a choice of default ValidationRules, which basically make a difference only between date, integer and other numerical values, providing different value ranges as feedback to the end user. So, the class translates from the technical, physical representation, which is determined by the CLR type of the property, to the logical limitations that type entails, which is what is relevant for the user.

So, although there are different checks for different allowed value ranges, there are only three configurable, i.e. localizable, validation messages:

Public Property DateMessage As String = "Must be a date value."

Public Property IntegerMessage As String = "Must be an integer between {1} and {2}."

Public Property NumericMessage As String = "Must be a number between {1} and {2}."

 

The default ValidationRules use basic .Net functions in order to determine whether .Net can convert the value:

Protected Overrides Function ValidateCore(ByVal value As Object, ByVal cultureInfo As System.Globalization.CultureInfo) As Boolean
    Dim result As Boolean = IsNumeric(value)
    If result And MinValue.HasValue Then
        result = CDbl(value) >= MinValue
    End If
    If result And MaxValue.HasValue Then
        result = CDbl(value) <= MaxValue
    End If
    Return result
End Function

 

Finally, the allowed minimum and maximum values are formatted into the validation message, to give the users complete feedback on what they have to enter in order to make it a valid value:

Protected Overrides Function FormatMessage(ByVal value As Object) As String
    Return String.Format(Me.Message, value, MinValue, MaxValue)
End Function

 

Conclusion

The TypeValidationRuleSetter class allows it to include meaningful and localizable validation messages for type checks with minimal effort. At the same time, it protects the application from exceptions which would otherwise be thrown because of type mismatches between what is entered into a TextBox and the type of the bound property.

Advertisements
  1. June 2, 2012 at 06:12

    The problem is that a user can enter valid data into the TextBox and this will be set in the property UserName. If the user then decides to change the text in the TextBox and the data becomes invalid, the setter of the property UserName is not called after the failed validation.

  2. June 3, 2012 at 08:56

    Hi, actually this is not a problem, but the purpose of validation: if the data is invalid, it should not be passed to the bound object, which means the setter will not be called. Normally, the bound object shouldn’t need to know about the invalid value, since it couldn’t use it anyway. If you need to know the invalid value, you should probably use IDataErrorInfo and a property that accepts all kinds of values instead.

    If you are worried about the object being different from what the users see on the screen: If you are using a BindingGroup and UpdateSourceTrigger=Explicit, as proposed in https://wpfglue.wordpress.com/2010/12/31/validating-objects-with-validationrules/, the BindingGroup can only commit all values together, and only if all of them are valid. So your object will not enter a state where some properties have changed their values and others still have their different, old values.

    So, strictly speaking, even if the user enters a valid UserName, this isn’t set on the property UserName until the data is submitted, e.g. by pressing a Login Button. Then, there will be a valid set of properties, and if the user does something on the same form after submitting the data, that will be a new, independent action.

  1. No trackbacks yet.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: