繁体中文
设为首页
加入收藏
当前位置:.Net技术首页 >> Asp.Net开发 >> Using Custom Attributes for Validation

Using Custom Attributes for Validation

2007-09-15 08:00:00  作者:  来源:互联网  浏览次数:0  文字大小:【】【】【
简介:Jan Tielens Ordina Euregio NV Applies to: Visual Studio.NET 2003 Level: Intermediates - Advanced Summary: Attributes can be used to decorate language elements with additional information that can ...

Jan Tielens

Ordina Euregio NV

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

CustomValidationSource

Contents

Attributes?

Custom Attributes

Why Use Attributes?

Building the ValidatorAttribute Base Class

Building Custom Validator Classes

Building the Validation Logic

Using the Validation

Conclusion

Further Reading

About the author

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:

_

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.

_

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:

_

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.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

_

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

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.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

_

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

'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!

Further Reading

Extending Metadata Using Attributes

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.

The demo code is hosted by

http://www.microsoft.com/belux/nl/msdn/community/columns/jtielens/attributes.mspx

责任编辑:admin
相关文章