Archive

Posts Tagged ‘TypeValidationRuleSetter’

Checking Property Types Automatically

May 6, 2012 2 comments

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

Validating Objects with ValidationRules

December 31, 2010 4 comments

In examples about writing WPF applications with the MVVM pattern, normally one doesn’t find any reference to ValidationRules. The normal way to do things there is to wrap the business object into the ViewModel and to implement IDataErrorInfo on the ViewModel. However, there are several points about this approach which I don’t like:

  • I don’t like to wrap classes into other classes. The kind of code this produces is exactly the kind of useless boilerplate code I want to get rid of by using a flexible data binding system like WPF’s. If it can’t be helped, this kind of code should be generated, but that is not the topic of this blog.
  • Validation on the ViewModel or Business level removes validation logic and error messages from the domain of the user interface definition to the domain of business logic. Now I like my business logic to be language independent, and it is absolutely valid for a user interface to implement different (more restrictive) checks on a property value than the business object it accesses. So, even if validation logic is implemented on the business level, I want to be able to choose how to use it and what error messages to display on the UI level, i.e. in XAML.

That said, ValidationRules have another significant advantage: they can check values before they are passed to the properties of the bound object. This means that I don’t have to implement rollback capabilities on the business object, but can make sure on the UI level that only valid values are set on the business object. This will be covered in a later post.

With all these advantages, there is a serious drawback to ValidationRules: defining them in XAML is rather unwieldy. Done the traditional way, they tend to bloat the XAML code, and sometimes it is difficult to get at all the reference data they need for their checks.

This is where WPFGlue comes into the picture: here, I want to introduce some ideas which allow to define and apply ValidationRules in a more concise manner, and to do some basic checks even without specifying ValidationRules explicitly.

The BindingGroup Class

The BindingGroup class has been introduced in WPF 3.5 SP1. A BindingGroup can be set on every FrameworkElement. If set, it can define ValidationRules which are able to access multiple properties of the DataContext in order to validate them against each other. In order to afford this possibility, the BindingGroup class provides methods which can access all Bindings in the subtree under the element which defines the BindingGroup, as long as these Bindings have the same DataContext as that element.

The idea here is to use this ability of the BindingGroup class in order to set up ValidationRules for all bindings which share a given DataContext.

So, in order to validate objects of a given class, first of all one would use a container like a StackPanel, Grid or GroupBox to combine a group of controls which share an instance of that class as their DataContext. Then, one would create a BindingGroup object and set it to the BindingGroup property of that container. Further, one would define a set of ValidationRules for a given class. Some of them would be at the property level, others would access multiple properties, as would be normal for ValidationRules on a BindingGroup object. Then, one would apply this set of ValidationRules to the container, instead of the separate controls which edit the object’s properties. One could reuse the set of ValidationRules wherever an instance of the DataContext class was used.

Using the BindingGroup’s ability for finding Bindings which have the same DataContext, one would add the ValidationRules  to the appropriate Bindings inside the container.

How to Modify Bindings Which are Defined in XAML

The documentation says that Bindings can not be modified after they have been used. However, there is an event that comes right between the time when the Bindings have all been created, and when they are used for the first time: Initialized. If the DataContext is not null and a BindingGroup is present on a FrameworkElement, this BindingGroup can access and modify those Bindings during the FrameworkElement’s Initialized event.

So, one would need a couple of things:

  • A way to define sets of ValidationRules.
  • A Sticky Component which handles distributing the ValidationRules to the Bindings.
  • A Sticky Property which attaches an instance of a set of ValidationRules to a container.

Defining Sets of ValidationRules

First of all, I said that I wanted to be able to define error messages in XAML. Using the techniques from the WPFGlue Localization namespace, I can handle multiple languages quite conveniently in XAML, and there might be other considerations like the intended audience of an application which might induce me to change the text of error messages. So, my first addition to a WPFGlue ValidationRule base class would be a Message property, which allows me to set the error message in XAML. In addition to that, I just added the functionality to format the property value into the error message.

Namespace Validation
    Public MustInherit Class ValidationRule
        Inherits System.Windows.Controls.ValidationRule

        Private _Message As String = String.Empty
        Public Property Message() As String
            Get
                Return _Message
            End Get
            Set(ByVal value As String)
                _Message = value
            End Set
        End Property

        Public Overrides Function Validate(ByVal value As Object, ByVal cultureInfo As System.Globalization.CultureInfo) As System.Windows.Controls.ValidationResult
            Dim result As ValidationResult = ValidationResult.ValidResult
            If Not ValidateCore(value, cultureInfo) Then
                Dim message As String = Me.FormatMessage(value)
                result = New ValidationResult(False, message)
            End If
            Return result
        End Function

        Protected MustOverride Function ValidateCore(ByVal value As Object, ByVal cultureInfo As System.Globalization.CultureInfo) As Boolean

        Protected Overridable Function FormatMessage(ByVal value As Object) As String
            Return String.Format(Me.Message, value)
        End Function
    End Class
End Namespace

 

The set of ValidationRules would be defined as a resource in XAML. In order to be able to do that, I create several classes which allow to define collections of ValidationRules grouped by property name:

Namespace Validation
    <System.Windows.Markup.ContentProperty("Setters")>
    Public Class ValidationRuleSetter
        Inherits DependencyObject
        Implements IStickyComponent

        Private Shared _DummyDataContext As Object = New Object
        Protected Overridable ReadOnly Property DummyDataContext As Object
            Get
                Return _DummyDataContext
            End Get
        End Property

        Private _Setters As New PropertyValidationRuleSetterCollection
        Public ReadOnly Property Setters As PropertyValidationRuleSetterCollection
            Get
                Return _Setters
            End Get
        End Property

 

The collection Setters contains collections of ValidationRules, grouped under different property names.

The ValidationRuleSetter class is a Sticky Component: in addition to defining the set of ValidationRules, it also has the ability to set the ValidationRules on the Bindings which belong to the container’s BindingGroup. The following method will be called during the container’s Initialized event:

Protected Overridable Sub AttachRules(ByVal base As Object)

    Dim f As FrameworkElement = TryCast(base, FrameworkElement)
    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)()
                If Setters.Contains(be.ParentBinding.Path.Path) Then
                    For Each v As System.Windows.Controls.ValidationRule In Setters.Item(be.ParentBinding.Path.Path).ValidationRules
                        be.ParentBinding.ValidationRules.Add(v)
                    Next
                End If
                If Setters.Contains(String.Empty) Then
                    For Each v As System.Windows.Controls.ValidationRule In Setters.Item(String.Empty).ValidationRules
                        bg.ValidationRules.Add(v)
                    Next
                End If
            Next
        End If
    End If

End Sub

 

For each Binding that is accessible to the BindingGroup, it looks up the name of the bound property and attaches all ValidationRules which are grouped under that name, if any. By convention, ValidationRules which have an empty String as property name are attached to the BindingGroup itself.

How to Use ValidationRuleSetter on a Container

In order to be able to attach the Sticky Component to a container, one defines a custom attached property like this:

Public Shared ReadOnly RuleSetterProperty As DependencyProperty = DependencyProperty.RegisterAttached("RuleSetter", GetType(ValidationRuleSetter), GetType(Validation), New PropertyMetadata(AddressOf WPFGlue.Framework.StickyComponentManager.OnStickyComponentChanged))
Public Shared Function GetRuleSetter(ByVal d As DependencyObject) As ValidationRuleSetter
    Return d.GetValue(RuleSetterProperty)
End Function
Public Shared Sub SetRuleSetter(ByVal d As DependencyObject, ByVal value As ValidationRuleSetter)
    d.SetValue(RuleSetterProperty, value)
End Sub

 

This is actually the standard way of defining a sticky property in the WPFGlue framework.

One would then define the container in XAML like this:

<StackPanel v:Validation.RuleSetter="{StaticResource vrules}" >
    <StackPanel.BindingGroup>
        <BindingGroup/>
    </StackPanel.BindingGroup>
    <TextBox Text="{Binding StringProperty, UpdateSourceTrigger=PropertyChanged}"
     Validation.ErrorTemplate="{StaticResource ValidationTemplate}"/>
    <TextBox Text="{Binding IntegerProperty, UpdateSourceTrigger=PropertyChanged}"
     Validation.ErrorTemplate="{StaticResource ValidationTemplate}"/>
    <TextBox Text="{Binding DoubleProperty, UpdateSourceTrigger=PropertyChanged}"
     Validation.ErrorTemplate="{StaticResource ValidationTemplate}"/>
</StackPanel>

 

Side Effects of Using BindingGroups

The presence of a BindingGroup on a container has various effects:

  • Any failed ValidationRule on any Binding inside the BindingGroup will make Validation.HasError return true on the container.
  • The BindingGroup’s own ValidationRules will cause the Validation.ErrorTemplate to be displayed on the container. ValidationRules on the Bindings will show the ErrorTemplate on the respective control and also the container’s ErrorTemplate.
  • The UpdateSourceTrigger on all Bindings inside the BindingGroup will be set to Explicit by default, which means that one would have to call BindingGroup.CommitEdit in order to have the Bindings update their sources. However, if the UpdateSourceTrigger on a Binding is set to another value in XAML, this value will be honoured.

Conclusion

Using this technique, it is possible to separate the definition of validation rules both from the model and from the view. This overcomes the main disadvantage of ValidationRules and opens up the way for further applications.

In posts to come, I intend to cover how to extend this technique in order to do type checks on property values before they are committed to the bound properties, and how to connect this with general data entry handling.

On the Downloads page, you can find the full code of the examples, plus some additions which will be covered in a later post.