ASP.NET form validators spelled the end of repetitive validation code. What used to take hours can now be accomplished in a matter of minutes using Visual Studio.NET's drag-and-drop interface. ASP.NET form validators offer flexibility both in the types of validation they support and in displaying error messages to users. Most developers shudder at the thought of returning to the validation techniques of three years ago.
Within the beauty of form validators lurks a hidden beast that only awakens in large web applications. In a 10 page data-entry application, hard-coding error messages and regular expressions into the validators is a fast and easy way to develop. You'll likely wind up with 2 or 3 "First Name" textboxes, each having a "First Name" required field validator and regular expression validator, each with their own hard-coded error message. Though not an ideal situation, it is certainly manageable. As the page count increases, however, maintaining these strings becomes a headache, especially if the product development department is choosy about error message syntax. When they ask to change "First Name is a required field" to "Please enter a first name" how many lines of code are touched?
The Solution
The requirements for the solution are as follows:
The solution must provide common storage for multiple values based on a key. In our example above, "FirstName" would be the key and the values stored would be RequiredErrorMessage, RegularExpressionErrorMessage, and RegularExpression.
The solution must be fast. These values will be accessed on the first page load of nearly every page in the web application, so they must be held in a screaming fast storage mechanism.
The solution must be easily editable. Developers should be able to view and edit any value with minimal effort.
The solution, as demonstrated in the downloadable code sample, consists of the following:
An XML file used to store the keys and values.
A singleton class that stores the XML data in memory. If you are unfamiliar with the singleton design pattern see the section below titled "A Brief Introduction to Design Patterns."
A method named BindErrorChecking() that appears on every page containing a validator or textbox. The first time a page loads a call is made to BindErrorChecking(), which retrieves the values from the singleton and assigns them to the appropriate validator or textbox. The following syntax is used:
We'll begin by examining a snippet of the XML file, Validation.xml:
<?xml version="1.0" encoding="utf-8" ?>
<Validations>
<Field>
<Name>FirstName</Name>
<RegEx>^[a-zA-Z]{0,50}$</RegEx>
<MaxLength>50</MaxLength>
<RegExErrorMessage>Invalid first name.</RegExErrorMessage>
<RequiredErrorMessage>Please enter a first name.</RequiredErrorMessage>
</Field>
<Field>
<Name>LastName</Name>
<RegEx>^[a-zA-Z']{0,50}$</RegEx>
<MaxLength>50</MaxLength>
<RegExErrorMessage>Invalid last name.</RegExErrorMessage>
<RequiredErrorMessage>Please enter a last name.</RequiredErrorMessage>
</Field>
...
<Name> is the key used in the calling code to retrieve the appropriate values.
<RegEx> is the regular expression that would be assigned to the ValidationExpression property of a RegularExpressionValidator.
<MaxLength> is another property that is typically hard-coded and duplicated throughout code. It corresponds to the MaxLength property of a textbox.
<RegExErrorMessage> is the error message to display if the regular expression validation fails.
<RequiredErrorMessage> is the error message to display if the required field validation fails.
This file will be modified throughout development as new form fields are added. It is very helpful to keep the elements ordered alphabetically by their <Name>.
A Brief Introduction to Design Patterns
If you are familiar with design patterns feel free to skip down to the Singleton section, below.
After many years of using software to solve business problems, computer scientists began to notice that they faced
a similar set of repeated problems from one application to the next. In 1995 the book
Design Patterns: Elements of Reusable Object-Oriented Software coined the term "Design Patterns" to
describe standard solutions to common problems in software design. There are now dozens of books written on
the subject, many of which provide language-specific examples. Microsoft has a section of their site dedicated
to .NET Design Patterns: http://www.microsoft.com/patterns/.
One of the most often-used design patterns is the Singleton. The Singleton pattern ensures that only one
instance of a class can be instantiated. Any calling code that uses an instance of a Singleton accesses
the same portion of memory. The benefits of this pattern are obvious in the current scenario, where having
a copy of the Validation.xml file in memory for each user on the site is unnecessary and would adversely
affect scalability.
With this in mind, let's examine the Validation class, which implements the Singleton design pattern.
The Validation Class
The following are the first few lines from the Validation class, defined in Validation.vb:
Public NotInheritable Class Validation
Private Shared _instance As Validation = New Validation
Public Shared ReadOnly Property instance() As Validation
Get
Return _instance
End Get
End Property
First notice that the class is NotInheritable, meaning it cannot be extended through inheritance. This class was not designed to be a base class and inheriting from it would not make much sense from an object oriented perspective.
The core of a singleton is its ability to maintain a single instance of itself no matter how many users instantiate it. This is possible because instantiation of the object is controlled by the object itself. This is achieved by making its constructor private, which ensures that calling code cannot "accidentally" instantiate the Validation class. When separate instances of calling code retrieve values from this class, they all indirectly use the shared _instance variable.
Public Class ValidationField
Public Name As String = String.Empty
Public RegEx As String = String.Empty
Public MaxLength As Integer = 0
Public RegExErrorMessage As String = String.Empty
Public RequiredErrorMessage As String = String.Empty
End Class
Private _cachedValidationFields As New Collections.Specialized.HybridDictionary
Public Shared ReadOnly Property Field(ByVal fieldName As String) As ValidationField
Get
Return CType(instance._cachedValidationFields(fieldName), ValidationField)
End Get
End Property
The code continues by declaring a nested class named ValidationField that serves as a data container for a single Validation.xml <Field> tag.
The Private HybridDictionary _cachedValidationFields, populated in Validation's constructor (see below), is the core of this class, as it holds the collection of ValidationField objects. _cachedValidationFields can only be accessed through the Field property, which retrieves a particular ValidationField object from the collection based on a string key.
When calling code accesses a specific field using the syntax Validation.Field("FirstName").MaxLength, the shared peroperty named instance is used to retrieve the appropriate value.
The next section of code is the constructor:
'**************************************************************
' New
' Parses the Validation.xml file
'**************************************************************
Private Sub New()
Dim ds As New DataSet
ds.ReadXml(Current.Server.MapPath("Validation.xml"))
Dim dr As DataRow
For Each dr In ds.Tables(0).Rows
Dim Field As New ValidationField
Field.Name = CStr(dr.Item("Name"))
Field.RegEx = CStr(dr.Item("RegEx"))
Field.MaxLength = CInt(dr.Item("MaxLength"))
Field.RegExErrorMessage = CStr(dr.Item("RegExErrorMessage"))
Field.RequiredErrorMessage = CStr(dr.Item("RequiredErrorMessage"))
_cachedValidationFields.Add(Field.Name, Field)
Next
End Sub
Begin by noting that the constructor is Private, the hallmark of a singleton. This constructor is only called once, the first time this class is instantiated. Every time an instance of this object is called after the first time, the "in memory" copy contained in _instance is used. In the accompanying test application this first access happens as soon as ValidationTest.aspx retrieves a value from the Validation class. A line could be added to the Application_Start method in Global.asax to ensure the values are read on application start-up.
The constructor performs the basics discussed earlier:
It reads Validation.xml into memory, in this case a DataSet.
It reads through each row of the DataSet and populates a new ValidationField object.
Finally, it adds the ValidationField object to the _cachedValidationFields collection.
Once the DataSet is populated it is not changed until it is reloaded from the XML when the application is restarted, either by modifying the Web.config or recycling the aspnet worker process.
The last two bits of code are two "bonus" subroutines not used in this sample project, but included for use in larger applications:
Public Shared Sub Reset()
_instance = New Validation
End Sub
The Reset method allows for dynamic reloading of Validation.xml during runtime. A web page could very easily call this method in a button event handler to allow administrators to reload Validation.xml on the fly without restarting the application.
Public Shared Sub AssertValidArgument(ByVal ToValidate As String,
ByVal FieldName As String)
If Not RegularExpressions.Regex.IsMatch(ToValidate,
Validation.Field(FieldName).RegEx) Then
Throw New Exception(Validation.Field(FieldName).RegExErrorMessage)
End If
End Sub
AssertValidArgument is designed to be called from the business tier to ensure the same validation that's performed in the UI is performed in the business tier.
Accessing Validation Fields in an ASP.NET Page
Accessing the values contained in the Validation object is trivial, and can be seen in the code-behind of ValidationTest.aspx in BindErrorChecking(), a method that is called on the first page load:
Protected Sub BindErrorChecking()
' Set textbox maxlengths
txtFirstName.MaxLength = Validation.Field("FirstName").MaxLength
...
' Set regular expressions
revFirstName.ValidationExpression = Validation.Field("FirstName").RegEx
...
' Set regular expression error messages
revFirstName.ErrorMessage = Validation.Field("FirstName").RegExErrorMessage
...
' Set required field error messages
rfvFirstName.ErrorMessage = Validation.Field("FirstName").RequiredErrorMessage
...
End Sub
Pros and Cons of this Solution
The main advantage of this approach is the maintainability provided by storing the myriad of string values in a single repository. In addition, using XML means only a text editor is required to modify these values, eliminating the need for recompilation. Finally, this approach allows the web and business tier to easily share the same regular expressions and error messages.
One disadvantage of this approach is that the ViewState is slightly larger than when hard-coding the values into the validators. When the values are hard-coded they are compiled into the dll, but when they are set dynamically at runtime they must be passed in the ViewState. For intranet or broadband users this is not an issue but for dial-up connections this could contribute to increased page load times.
Further Work
One opportunity to extend this class would be to add the capability to handle range validators, compare validators, and custom validators.
Conclusion
Large applications come with many challenges that, given the wrong approach, can turn into maintenance nightmares. As applications grow they tend to contain large quantities of repetitive information. The Validation class offers an easy to maintain, XML-based solution to an otherwise difficult problem.
A special thanks to Chris Mullins of Winfessor (www.winfessor.com) for providing the idea this article is based on. Thanks a lot!
About The Author
Rob Walling is a Microsoft Certified Application Developer with five years of development experience. His areas of expertise
include ASP.NET, VB.NET, and web application architecture. His technical articles have been published on various websites.
You can reach him at rwalling *=* thenumagroup.com (substitue '@' for '*=*').
Stonebroom.ASP2XML(c) is an interface component designed to make building
applications that transport data in XML format much easier. It can be used
to automatically pass updates back to the original data source.
Right now the latest buzzword around town is AJAX. AJAX is an acronym for Asynchronous JavaScript and XML and is a method used to implement remote calling. The problem is that AJAX is only implemented in ASP.NET 2.0. This article will show you one way to implement remote calling without using AJAX or the XMLHttpRequest object. The technique outlined can even be used from classic ASP and is sufficient for most remote calling needs. [Read This Article][Top]
This article is the third and final installment of Alex Homer's series covering the new XML support in Microsoft SQL Server 2005. In it he covers updating the contents of xml columns, comparing traditional XML update techniques with XQuery, and using XQuery in a managed code stored procedure. [Read This Article][Top]
In the second part of his series on SQL Server 2005's new XML support, Alex Homer looks at extracting data from XML columns, comparing traditional XML data access approaches with XQuery, and combining XQuery and XSL-T.
[Read This Article][Top]
Microsoft SQL Server 2005 now offers great support for and close integration with XML as a data persistence format. In the first article of his series examining this new support, Alex Homer offers an overview of how SQL Server 2005 stores XML documents and schemas, examines how it supports querying and manipulating XML documents, and provides a simple test application that allows you to experiment with XQuery. [Read This Article][Top]
In the final article of his series on reading and writing XML in .NET 2.0, Alex Homer looks at how the updated XML document store objects XmlDocument, XmlDataDocument and PathDocument can be used to read, persist and write XML documents and fragments more easily and more efficiently than in .NET 1.x. [Read This Article][Top]
In the final article of his series on reading and writing XML in .NET 2.0, Alex Homer looks at how the updated XML document store objects XmlDocument, XmlDataDocument and PathDocument can be used to read, persist and write XML documents and fragments more easily and more efficiently than in .NET 1.x. [Read This Article][Top]
Alex Homer continues his series on reading and writing XML in .NET 2.0. In part one, we focused on the reading side of things, examining the XmlReader and XmlReaderSettings classes. In this article, we move on to look at the XmlWriter and XmlWriterSettings classes, and how they can be used to write XML documents and fragments more easily and more efficiently than in version 1.x of .NET.
[Read This Article][Top]
Alex Homer continues his series on reading and writing XML in .NET 2.0. In part one, we focused on the reading side of things, examining the XmlReader and XmlReaderSettings classes. In this article, we move on to look at the XmlWriter and XmlWriterSettings classes, and how they can be used to write XML documents and fragments more easily and more efficiently than in version 1.x of .NET. [Read This Article][Top]
In the first part of his series on reading and writing XML in .NET 2.0, Alex Homer discusses the XmlReader and XmlReaderSettings classes. The XmlReader exposes several useful new features and the all new XmlReaderSettings class makes it easy to generate single or multiple instances of an XmlReader with a range of useful properties. [Read This Article][Top]
In the first part of his series on reading and writing XML in .NET 2.0, Alex Homer discusses the XmlReader and XmlReaderSettings classes. The XmlReader exposes several useful new features and the all new XmlReaderSettings class makes it easy to generate single or multiple instances of an XmlReader with a range of useful properties. [Read This Article][Top]
Mailing List
Want to receive email when the next article is published? Just Click Here to sign up.