背景:
阅读新闻

Using Custom Attributes for Validation

[日期:2003-10-21] 来源:MSDN  作者: [字体: ]

Applies to:
  • Visual Studio.NET 2003
  • Level: Intermediates - Advanced
Summary:

Attributes can be used to decorate language elements with additional information that can be retrieved at run time by using Reflection. You can easily build your own custom attributes to create powerful generic solutions. At first sight they might to seem a little bit useless. But this article shows how they can be used to create a simple extensible framework to provide validation of property values.

Downloads

Attributes?

First of all, what are attributes? In the MSDN Library the following description can be found: The common language runtime allows you to add keyword-like descriptive declarations, called attributes, to annotate programming elements such as types, fields, methods, and properties. Attributes are saved with the metadata of a Microsoft .NET Framework file and can be used to describe your code to the runtime or to affect application behavior at run time. While the .NET Framework supplies many useful attributes, you can also design and deploy your own. (Extending Metadata Using Attributes)
So you can add attributes to programming elements, in which you can provide some extra information. This information can be retrieved from those programming elements at run time by using Reflection. For example, we can use the System.ComponentModel.DescriptionAttribute to provide some information about a the class:
<System.ComponentModel.Description("This is the Customer class.")> _
Public Class Customer
    Private _name As String

    Public Property Name() As String
        Get
            Return _name
        End Get
        Set(ByVal Value As String)
            _name = Value
        End Set
    End Property
End Class

As you can see, the Description attribute is attached to the Customer class. The value for the Description attribute is set to the String value between the parentheses. Notice that you can use DescriptionAttribute or Description as the name of the attribute; you can omit the Attribute suffix. Also notice that this attribute is attached to a class. Some attributes can attach to any programming element (property, class, parameter, .) while others can only attach to specific programming elements.
So what can you do with these attributes? You can retrieve the values of the attributes at run time using Reflection. Reflection can examine a type and retrieve all or some of the attributes.
Dim customerType As Type = GetType(Customer)
Dim descr As System.ComponentModel.DescriptionAttribute

For Each descr In customerType.GetCustomAttributes( _
    GetType(System.ComponentModel.DescriptionAttribute), True)

    MsgBox(descr.Description)
Next

The example shows that we first need to get the Type of the Customer class, which is achieved using the GetType function. Then the GetCustomAttributes function is used to get all the attributes of the type DescriptionAttribute. For each of these attributes, in the example only one, the Description is showed in a message box.

Custom Attributes

There are many attributes provided by the .NET Framework, but it gets really interesting when you build your own attributes. To do so, the Attribute class can be used to inherit from.
<System.AttributeUsage(AttributeTargets.Property, AllowMultiple:=False)> _
Public Class ExtendeDescriptionAttribute
    Inherits System.Attribute

    Private _description As String
    Private _displayOrder As Integer

    Public Sub New(ByVal description As String, ByVal displayOrder As Integer)
        MyBase.New()
        _description = description
        _displayOrder = displayOrder
    End Sub

    Public Property Description() As String
        Get
            Return _description
        End Get
        Set(ByVal Value As String)
            _description = Value
        End Set
    End Property

    Public Property DisplayOrder() As Integer
        Get
            Return _displayOrder
        End Get
        Set(ByVal Value As Integer)
            _displayOrder = Value
        End Set
    End Property
End Class

The example above shows a custom attribute class that has two properties: description and display order. As you can see the ExtendedDescriptionAttribute class inherits from the Attribute class provided in the .NET Framework. It's a guideline to use the "Attribute" suffix for classes that are custom attributes, so make sure always to use this suffix. The AttributeUsage attribute is attached to the class to specify for which type of programming elements the custom attribute can be used and if the attribute can be repeated. In this example the ExtendedDescriptionAttribute can be used for properties and can be repeated. Custom attributes can be retrieved at runtime using Reflection, like the first example. Also the use of the attribute does not differ from the attributes provided in the .NET Framework:
<ExtendeDescription("The is the name of the customer.", 0)> _
Public Property Name() As String
    Get
        Return _name
    End Get
    Set(ByVal Value As String)
        _name = Value
    End Set
End Property
 

Why Use Attributes?

At first sight, the usage of attributes seems to be limited. Why would you provide information about the code you write, which can only be used at run time? By using custom attributes you can make more generic functionality. To illustrate the use of custom attributes, this article will describe how to make use of attributes to build a small framework to validate properties. The goal is to build a set of attributes that allows developers to easily add validation, with a minimum of code:
<Validators.NotEmptyStringValidator("Name cannot be empty."), _
Validators.LengthValidator("Max length of name is 30.", MaxLength:=30)> _
Public Property Name() As String
    Get
        Return _name
    End Get
    Set(ByVal Value As String)
        _name = Value
    End Set
End Property

As you can see, two attributes are attached to the Name property. They will enforce two validation rules for that property: the name cannot be empty and the maximum length is 30 characters. These attributes will be the only thing you need to add to your class properties to have validation!

Building the ValidatorAttribute Base Class

The idea is to build a whole set of validator attributes to check various rules, for example length, maximum value, minimum value, . Since there is some basic functionality that every validator attribute should have, this functionality is encapsulated in an abstract base class. The Message property is used to store the message that should showed when the validation for a specific property failed. The UML diagram shows that ValidatorAttribute base class that is inherited by two validator classes that will be build later in this article. The ValidatorAttribute class inherits its base attribute functionality from the System.Attribute class that is provided by the .NET Framework.
Building the ValidatorAttribute Base Class
Building the ValidatorAttribute Base Class

<AttributeUsage(AttributeTargets.Property, AllowMultiple:=True)> _
Public MustInherit Class ValidatorAttribute
    Inherits Attribute

    Private _message As String

    Public Property Message() As String
        Get
            Return _message
        End Get
        Set(ByVal Value As String)
            _message = Value
        End Set
    End Property

    Friend MustOverride Function IsValid(ByVal item As Object) As Boolean

    Public Sub New(ByVal message As String)
        _message = message
    End Sub
End Class

The ValidatorAttribute inherits from the System.Attribute class and can be attached multiple times to properties. Each implementation of a ValidatorAttribute has the Message property that contains the message that should be displayed if the validation of that property failed.
The actual validation of the value of the property is done in the IsValid function. Because each implementation of the ValidatorAttribute class will have its own validation rules, only the definition of this member is given; all inherited classes must implement this function.

Building Custom Validator Classes

Now that the abstract base class is available, we can start building the implementations of this class. Each implementation will have its own validation rule. All the implementations inherit from the ValidatorAttribute class, so they'll all have the Message property for free. In the source code for this example, the custom validator implementations are placed in the Validators namespace.
NotEmptyStringValidatorAttribute Class
One of the most basic validations that can be done is to check the value of the property is not empty. Since this is only applicable to a String value, the validation will check for the String.Empty value.
Public Class NotEmptyStringValidatorAttribute
    Inherits ValidatorAttribute

    Public Sub New(ByVal message As String)
        MyBase.New(message)
    End Sub

    Friend Overrides Function IsValid(ByVal item As Object) As Boolean
        If CType(item, String) = String.Empty Then
            Return False
        Else
            Return True
        End If
    End Function
End Class

In the IsValid function, the item parameter is cast from the Object type to the String type, to be able to compare with the String.Empty value. If the item equals the String.Empty value the validation failed, so False is returned by the IsValid function, otherwise True is returned indicating the validation succeeded.
LengthValidatorAttribute Class
String values can be of any length, but in a database, most of the time string values will have a maximum length. So a validation rule that is very often is used is the maximum length of a property. The code below shows how to build the LengthValidatorAttribute that can validate the minimum and maximum length of a string value.
Public Class LengthValidatorAttribute
    Inherits ValidatorAttribute

    Private _maxLength As Integer
    Private _minLength As Integer

    Public Property MinLength() As Integer
        Get
            Return _minLength
        End Get
        Set(ByVal Value As Integer)
            _minLength = Value
        End Set
    End Property

    Public Property MaxLength() As Integer
        Get
            Return _maxLength
        End Get
        Set(ByVal Value As Integer)
            _maxLength = Value
        End Set
    End Property

    Public Sub New(ByVal message As String)
        MyBase.New(message)
        _maxLength = 0  'Default value
        _minLength = 0  'Default value
    End Sub

    Friend Overrides Function IsValid(ByVal item As Object) As Boolean
        If _maxLength > 0 Then  'If maxLength needs to be checked.
            If Not item Is Nothing Then
                'It item is Nothing, don't check the length.
                If CType(item, String).Length > Me.MaxLength Then
                    Return False
                End If
            End If
        End If

        If _minLength > 0 Then  'If minLength needs ot be checked.
            If Not item Is Nothing Then
                If CType(item, String).Length < Me.MinLength Then
                    Return False
                End If
            Else
                'If item is Nothing the validation will failed.
                Return False
            End If
        End If

        'Otherwise the validation succeeded.
        Return True
    End Function
End Class

The LengthValidatorAttribute class has two additional properties: MaxLength and MinLength. These properties are used to store either the maximum length and/or minimum length of the property to which this attribute is attached. They can both be used alone, or in combination to each other. The IsValid function can be divided into two parts: the first checks for the maximum length of the property, the second part checks for the minimum length of the property, if applicable. As you can see both the MaxLength and MinLength values are set to zero in the constructor of the class. In fact this is not really necessary because Integer have always the default zero value, but doing so you indicate that somewhere in your class you rely on those default values.

Building the Validation Logic

ValidationException Class
Now that the validation attributes that decorate the properties are ready, the building of component that actually does the validation needs to be done. But first of all, lets think about what should happen if the validation of one or more properties fails. In that case an exception should be raised. This exception should contain all the information about the properties that caused the validation to fail. The easy way to accomplish this is to put all this information into a string and throw an ApplicationException. But it's nicer to provide a custom exception not only containing the concatenated message, but also for each property for which the validation failed, the corresponding message separately. This gives you the advantage to either treat the validation exception as a whole, or treating the validation exception into detail for each property that caused the validation to fail; for example when you want to put a message next to the textboxes for the properties that have wrong values. Using a custom exception also enables you to write Catch statements specific for the ValidationException. The UML class diagram shows how the ValidationException will be implemented. For each property for which validation failed, will be a ValidationExceptionDetail object in the ValidationException. The ValidationExceptionDetail class has two properties to store the corresponding message and property name.
ValidationException Class
ValidationException Class

Public Class ValidationException
    Inherits ApplicationException

    Private _details As ValidationExceptionDetail()

    Public Sub New(ByVal message As String, ByVal details As ValidationExceptionDetail())
        MyBase.New(message)
        _details = details
    End Sub

    Public ReadOnly Property Details() As ValidationExceptionDetail()
        Get
            Return _details
        End Get
    End Property
End Class

Public Class ValidationExceptionDetail
    Private _propertyName As String
    Private _message As String

    Public Sub New(ByVal propertyName As String, ByVal message As String)
        _propertyName = propertyName
        _message = message
    End Sub

    Public Sub New(ByVal message As String)
        Me.New(String.Empty, message)
    End Sub

    Public Property PropertyName() As String
        Get
            Return _propertyName
        End Get
        Set(ByVal Value As String)
            _propertyName = Value
        End Set
    End Property

    Public Property Message() As String
        Get
            Return _message
        End Get
        Set(ByVal Value As String)
            _message = Value
        End Set
    End Property
End Class

ValidationEngine Class
The ValidationEngine class will have the logic to validate a class based on the attributes that are attached to it. As you can see in the code, there is only one public member; the Validate sub which takes an object as a parameter. This object is the object that will be validated. If the validation succeeds, nothing will happen, but if the validation fails, a ValidationException is throwed. Notice that the Validate member is Shared (static) so no instance of the ValidationEngine class is needed to use that member.
The type of the object that is passed, is retrieved using the GetType function, which returns an instance of the type class for that object. The code shows that first all the properties are traversed in a for each loop, using the GetProperties function of the type class. Then for each property, all the custom attributes of the type ValidatorAttribute are fetched. Now for each ValidatorAttribute, the IsValid function is invoked. This function takes an object as a parameter that should be validated. So the actual value of the current property needs to be retrieved and passed in that parameter. This is accomplished by using some Reflection magic: by using the PropertyInfo object, the value of the corresponding property of the current item is retrieved. If the IsValid function returns False, so when the validation failed, a ValidationExceptionDetail object is put in the details ArrayList.
When all the properties are validated, the details ArrayList is checked. If it contains no items, nothing should happen because all validations were successful. If one ore more items are in the ArrayList, an exception message is build by concatenating all the messages from the ValidationExceptionDetails. This message is used to construct a new ValidationException, to which the details ArrayList is added too.
Imports System.Reflection
Public Class ValidationEngine
    Private Sub New()

    End Sub

    Public Shared Sub Validate(ByVal item As Object)
        Dim message As String
        Dim details As New ArrayList

        For Each propInfo As PropertyInfo In item.GetType.GetProperties
            For Each attr As ValidatorAttribute _
            In propInfo.GetCustomAttributes(GetType(ValidatorAttribute), True)
                If Not attr.IsValid(propInfo.GetValue(item, Nothing)) Then
                    details.Add( _
                        New ValidationExceptionDetail(propInfo.Name, attr.Message))
                End If
            Next
        Next

        If details.Count > 0 Then
            'Construct message
            For Each detail As ValidationExceptionDetail In details
                If message <> String.Empty Then message += vbCrLf
                message += detail.Message
            Next

            Throw New ValidationException(message _
                , details.ToArray(GetType(ValidationExceptionDetail)))
        End If
    End Sub
End Class

Using the Validation

To illustrate how the validations can be used, a very simple business entity example is included; the Customer class with two properties: Name and Street. The Name property cannot be empty and both the Name and Street properties cannot contain more than 30 characters.
Public Class Customer
    Private _name As String
    Private _street As String

    <Validators.NotEmptyStringValidator("Name can not be empty."), _
    Validators.LengthValidator("Max length of name is 30.", MaxLength:=30)> _
    Public Property Name() As String
        Get
            Return _name
        End Get
        Set(ByVal Value As String)
            _name = Value
        End Set
    End Property

    <Validators.LengthValidator("Max. length of street is 30.", MaxLength:=30)> _
    Public Property Street() As String
        Get
            Return _street
        End Get
        Set(ByVal Value As String)
            _street = Value
        End Set
    End Property
End Class

In the source code a small test application is included with a simple user interface to test the validation of a customer object. A new customer object is created and validated in a Try . Catch block where a ValidationException is catched. The Message property of this exception is used to display the information about the properties for which the validation failed. The Details property is used to change the back color of each Textbox causing the validation to fail.


Validation Test
Validation Test

'Reset backcolors
customerName.BackColor = Color.White
customerStreet.BackColor = Color.White

Dim c As New Customer

c.Name = customerName.Text
c.Street = customerStreet.Text

Try
    ValidationEngine.Validate(c)
    validateResult.Text = "Validation succeeded!"
Catch ex As ValidationException
    validateResult.Text = ex.Message

    For Each detail As ValidationExceptionDetail In ex.Details
        Select Case detail.PropertyName
            Case "Name"
                customerName.BackColor = Color.LightYellow
            Case "Street"
                customerStreet.BackColor = Color.LightYellow
        End Select
    Next
End Try

Conclusion

Working with attributes in .NET is great. In combination with Reflection you can create generic and extensible solutions for common problems very easily. As you can see in the example described in this article, once the base functionality is build, extending it is very simple. So you can build more custom ValidatorAttributes to perform more validation rules, for example checking the minimum and maximum value of numeric properties, capitalization, dates, . just use your creativity!

About the author

Jan Tielens is a .NET Architect of Ordina Euregio NV. Already since the early betas of .NET he has spent a lot of time getting familiar with the details of the .NET Framework and the new possibilities and technologies. Now he's not only involved in .NET projects, but he's also evangelizing .NET among his colleagues by giving presentations and writing articles for MSDN Belux.
You can contact him at jan.tielens@ordina-euregio.com or read his weblog at http://weblogs.asp.net/jan.






收藏 推荐 打印 | 录入:木鸟 | 阅读:
相关新闻      
本文评论   [发表评论]   全部评论 (0)
热门评论