Export (0) Print
Expand All

Dynamic Creation of Validation Controls

 

Callum Shillan
Microsoft Consulting Service

October 2004

Applies To:
   Microsoft Framework versions 1.0 and 1.1
   Microsoft Visual C#
   Microsoft ASP.NET

Summary: Explains how to use an XML configuration file to control the dynamic creation of ASP.NET validation controls that are automatically applied to user input controls. This allows the Web form developer to focus on implementing business logic, and also provides for a more consistent user interface. (22 printed pages)

Download the source code for this article.

Contents

Introduction and Confession
The Dynamic Validation Configuration file
Data Structures
The DynamicValidationManager Class
Using the DynamicValidationManager
Conclusion
Related Books

Introduction and Confession

I'd like to say that the ideas in this article were based on something noble, such as the hope of freeing the Web developer to focus on implementing business logic, or maybe a desire to create a framework that helps provide a more consistent user interface, but it just wasn't that way. The concept of using dynamically created validation controls on Web forms was born out of frustration with a User Interface team.

As with most well-run Web application projects, there were several teams with clearly defined responsibilities: We had the usual suspects of development, test, user experience, and so on. On this project we were using use case scenarios with associated screen designs, and it worked quite well. There was even a single document that held the definition of (almost) each user input field, the screens on which it was used, whether it was a required field, what the valid characters were, and what the validation error messages should be. So, we all got to work, started developing our screens, and everything was going quite well. And then the problems started...

In one of the morning meetings, the User Interface team said they needed to change the language used to report validation errors. I can't remember the details: perhaps they were too verbose and needed to be tightened up; perhaps it was the other way round. Whatever it was, we lost quite some time as we went back through all our screens and changed the wording on the ErrorMesssage and Text properties of the validation controls—but, we did it. Part of being a team is that you have to be prepared to accommodate other people, even if this means more work for yourself.

Then it happened again. Maybe this time there had been further consultation with the customer. Maybe there was some other good reason for it. But we found ourselves implementing changes again. This time, however, the changes were more extensive: the color of the messages needed to be changed; fields that were previously required were now optional, and vice versa; allowed character sets were changed; and the changes went on and on.

We realized we needed a way to protect ourselves. Somehow, we had to separate the form development from the input validation. We realized we needed a way of freeing ourselves to focus on implementing the business logic without worrying about what color or wording an error message should be. Further, we needed a framework that helped create a consistent input validation that could be easily changed and modified without needing to change each and every ASPX page.

After a concentrated whiteboard session, we ended up with a version of the implementation presented below. In essence, each input field has an associated PlaceHolder control. The PlaceHolder's ID property indexes into a configuration file to retrieve a collection of validators dynamically created and added to the PlaceHolder. When the Web form gets rendered to the client, the validation controls are also included and the user input gets validated as usual. Quite simple, really.

After removing all validator controls from the ASPX pages and inserting the PlaceHolder controls, we could hand over population of the configuration file to the User Interface team, and they could make changes as and when they needed.

It's a fairly simple concept to explain, but on the way (in this article) we will deal with accessing data held in an XML file, we'll cache the configuration data in a Hashtable that holds ArrayLists of String dictionaries, and we'll make use of reflection to access and set the properties of the dynamically created validation controls. I wouldn't recommend the use of reflection in a real-world scenario, by the way, as it will impact throughput and performance, but using reflection here allows me to keep the code a little simpler, as I'll discuss below.

The Dynamic Validation Configuration file

The first thing we need to do is to decide how to hold the configuration information. We have a number of ways of storing this information: in a database, in a resource file, in an XML file, and so on. We will store the information in an XML file for the simple reason that it is easy to edit and manipulate.

There will be two sections to the configuration file: one that defines the common and default property values used for all validators, and one that defines a collection of validators and properties to be applied to a given user input field.

Defaults Section

The last thing we want to do is have to continually specify repeated properties for every validator control, so we use a Defaults section of the configuration file to specify them once. This means we don't have to specify, for example, "ForeColor='RED'" on every validation control we define. If necessary, these default property values can be overridden for a given validator.

There are a set of properties common to every validator, irrespective of their type. I'm not going to list them all, but ones that spring to mind are CssClass, ForeColor, and Visible. For the set of common validator properties, we will have a subsection of the configuration file that holds the common values to be applied to all dynamically created validators, irrespective of their type.

Each type of validator control also has a unique set of properties (for example, the ValidationSummary's ShowMessageBox property), so we will need a subsection that holds the default property values.

Lastly, it would be good to be able to parameterize these values. This would enable us to set a single RequiredField's ErrorMessage default property value to something like, "Please enter something for {FriendlyName}," and define the friendly name elsewhere.

Not surprisingly, the Defaults section of the configuration file will be an XML node with child nodes for the common and validator-specific property values. This is shown below.

<!--
This section contains default values for validator controls
-->
   <Defaults>
      <!--
      These are default property values that are common to all validator controls
      -->
      <Common>
         <Property name="ForeColor" value="Red" />
         <Property name="Display" value="Dynamic" />
         <Property name="EnableViewState" value="False" />
      </Common>
      <!--
      These are default property values specific to the ValidationSummary controls
      -->
      <ValidationSummary>
         <Property name="EnableClientScript" value="True" />
         <Property name="Enabled" value="True" />
         <Property name="HeaderText" value="Please correct the following errors" />
         <Property name="ShowMessageBox" value="False" />
         <Property name="ShowSummary" value="True" />
         <Property name="DisplayMode" value="BulletList" />
      </ValidationSummary>
      <!--
      These are default property values specific to the Compare validator
      -->
      <Compare />
      <!--
      These are default property values specific to the RequiredField validator
      -->
      <RegularExpression>
         <Property name="Text" value="Allowed chracters are {LegalValues}" />
         <Property name="ErrorMessage" value="{FriendlyName} can only consist of {LegalValues}" />
      </RegularExpression>
      <!--
      These are default property values specific to the RequiredField validator
      -->
      <RequiredField>
         <Property name="InitialValue" value="" />
         <Property name="Text" value="This is a required field" />
         <Property name="ErrorMessage" value="You must enter something for the {FriendlyName}" />
      </RequiredField>
      <!--
      These are default property values specific to the Custom validator
      -->
      <Custom>
         <Property name="EnableClientScript" value="False" />
      </Custom>
   </Defaults>

Note that the default value for the ValidationSummary's ForeColor property should override the value held in the Common node as this is the intuitive understanding. This behavior doesn't happen by magic; we will have to implement it in our code.

ValidatorSets Section

Any user input field will need a collection of validation controls in order to ensure that the input is correctly validated. For example, a password field might need a RequiredFieldValidator and a RegularExpressionValidator in order to ensure that the user entered input and that it was from a defined character set.

A ValidatorCollection will have an ID attribute and a collection of Validator child nodes for each validator that should be created and applied to the user input field. The ID attribute of the ValidatorCollection will match the PlaceHolder's ID property on the Web form in order to connect the user input field with the collection of validators that should to be created and applied.

For example, the ValidatorCollection for the password field might have an ID of "Password" with two Validator child nodes: one for the required field and one for the regular expression. The Validator child nodes hold the property values that either override those defined in the Defaults section or are not defined in the Defaults section.

As there will be a number of user input fields, there will be a set of ValidatorCollection nodes, and these will be held in a single ValidatorSets node of the configuration file.

A portion of the configuration file for e-mail address and password fields is given below.

<!--
This section defines the validator groups
A validator group defines a collection of validators and their properties
-->
<ValidatorSets>
   <!--
   This is the collection of validator controls to be used for the EmailAddress
   -->
   <ValidatorCollection id="EmailAddress" 
     FriendlyName="Email address" 
     ControlToValidate="TextBoxEmailAddress">
      <Validator type="RequiredField" />
   </ValidatorCollection>
   <!--
   This is the collection of validator controls to be used for the Passsword
   -->
   <ValidatorCollection id="Password" 
     FriendlyName="Password" 
     LegalValues="alphabetic characters and numbers" 
     ControlToValidate="TextBoxPassword">
      <Validator type="RequiredField" />
      <Validator type="RegularExpression">
         <Property name="ValidationExpression" value="[A-Za-z0-9]*" />
      </Validator>
   </ValidatorCollection>
</ValidatorSets>

The e-mail address collection has a single Validator child node indicating that a RequiredFieldValidator should be dynamically created and applied to the TextBoxEmailAddress user input control. As the Validator node has no child nodes, all the property values will be derived from those defined in the Defaults node. Again, this doesn't happen by magic and we will need to implement this behavior in our code.

The password collection has two Validator child nodes indicating that a RequiredFieldValidator and a RegularExpressionValidator should be dynamically created and applied to the TextBoxEmailPassword user input control. The RegularExpressionValidator should have the ValidationExpression property set to "[A-Za-z0-9]". Again, the RequiredFieldValidator validator should take its property values from those defined in the Defaults node.

And this is where we define the field name values. Note that we have an attribute called FriendlyName on the ValidatorCollection node. The inner text of this attribute is used to replace any instances of {FriendlyName} found in property values held in the configuration file. This means that lots of ValidatorCollections can refer to the same default required field validator definition, and all they need to do is identify the user-friendly name in order to customize the messages displayed to the end user.

Data Structures

We are going to need some form of data structure to keep all this configuration information in. In order to keep things simple, we will allow ourselves to spend quite some time building this data structure and then hold it in the ASP.NET application cache.

This type of structure is a valid candidate to be held in the application cache, as it is relevant to the every single page within the application. We can also set a cache dependency on the configuration file, so that if the file is modified, the cache entry will be made void and then recreated on its next access.

The data structure that will be held in the application cache is shown below.

Aa478956.datastructures_fig1(en-us,MSDN.10).gif

Figure 1. The data structure held in application cache

The Hashtable will have an entry for each user input field that is to have dynamically created validator controls applied to it. The key (for instance, "email" and "psword" in figure 1, above) will match the PlaceHolder's ID property enabling us to determine which list of validators to dynamically create and add.

So, somewhere on the Web form that has a user input field for, say, an e-mail address, there will be a PlaceHolder control with an ID of "email". This will be used to index into the Hashtable and access the ArrayList of validator's that need to be dynamically created and added to the PlaceHolder. The ArrayList will be iterated and the properties for the dynamically created validator will be held in the associated StringDictionary.

The DynamicValidationManager Class

In order to manage this process, we are going to create a simple class with two public methods: a construct and one that will dynamically load the validation controls for a given Web form or user control.

In order to avoid the manager being created time and again for every PlaceHolder on every Web form, we place it in the application cache. Each access will check to see whether it needs to be created. The need for creation occurs in two situations: on first access and if the item has been flushed. As the object will depend on the associated configuration file, we can also set a cache dependency so that if the file is modified, the manager will be automatically flushed from the application cache.

Dynamic Validation Manager Constructor

The constructor needs to create the data structures shown in Figure 1, above. In order to do this, the following steps will need to be performed:

  1. Define the manager's private properties and the constructor signature.
  2. Load the configuration file into an XML document.
  3. Load the common and default property values.
  4. Loop through each validator collection and
    Load the individual validator collection
    Loop through each defined property for the validator
    Insert the common/default property value, if defined, in a string dictionary
    Overwrite/Insert the specific property value in the string dictionary
    Add the string dictionary to the array list of validators
    Add the array list into the hash table of validator collections.
  5. The code to implement these steps is discussed below.

Define private properties and constructor signature

We will need two properties that are private to the dynamic validation manager: the list of validator control types we will process, and the hash table of validator collections. The constructor signature will have a parameter defining the path to the configuration file. This is shown below.

// These are the validator types that we will cater for
private enum ValidatorTypes {Common, Compare, 
  Custom, Range, RegularExpression, RequiredField, 
  ValidationSummary};

// Holds the collection of validator controls defined 
// in the <ValidatorCollections> node of the configuration file
// This will hold array lists of string dictionaries of property/values 
// for each defined validator
private Hashtable validatorCollections = new Hashtable();

/// <summary>
/// Builds the Dynamic Validation object
/// </summary>
/// <param name="validatorsConfigFile">The configuration file 
/// for the Dynamic Validator Controls</param>
public DynamicValidationManager( string validatorsConfigFile )
{

The ValidatorTypes enumeration will be used as it gives us an easy way to either process a list of string equivalents or access an integer index. We can get the list of string values by using the GetNames() static method of the Enum class and we can simply cast an instance of the enumeration to an integer in order to get its index.

Load the configuration file into an XML document

The code to load the configuration file is quite simple and is shown below.

// Load the configuration file into an XML document
XmlTextReader xmlTextReader = new XmlTextReader( validatorsConfigFile );
XmlDocument configurationDocument = new XmlDocument();
configurationDocument.Load( xmlTextReader );

We use an XmlTextReader in order to have a fast, non-cached, forward-only access to XML data. We're going to process this configuration file as quickly as possible; there is no need to cache it, as we are holding the values in our data structure.

Load the common and default property values

We will use a temporary array of StringDictionaries to store the common and default values. Each StringDictionary will hold all the common and default property values as defined in the Defaults node of the configuration file. In this way, when we come to setting the specific values of a validator defined in a ValidatorCollections node, we can quickly access the correct "default values" hash table.

The code to load the default property values is shown below.

// Holds the default properties defined in the 
// <Defaults> node of the configuration file
// The array will hold one StringDictionary of 
// default properties and values for each type of validator
StringDictionary[] defaultProperties = 
new StringDictionary[Enum.GetNames(typeof(ValidatorTypes)).Length];

// Loop through each ValidatorType
int iCnt = 0;
foreach ( string validatorType in Enum.GetNames( typeof(ValidatorTypes) ) )
{
   // Create a new hashtable to hold the 
   // property/value pairs for the current validator type
   defaultProperties[iCnt] = new StringDictionary();

   // Load the default settings from the configuration document
   LoadDefaultProperties( configurationDocument, 
     validatorType, defaultProperties[iCnt] );

   // Increment the counter
   iCnt++;
}

We define the defaultProperties array of StringDictionaries and use the length of the array returned by the GetNames() static method of the Enum class to get the correct number of entries.

We then loop through each of the entries in the array of names from the ValidatorTypes enumeration and load in the default properties by invoking the LoadDefaultProperties method.

The code for the LoadDefaultProperties method is shown below.

/// <summary>
/// Loads default settings from the configuration document into a property store
/// </summary>
/// <param name="configurationDocument">The XML document 
/// that holds the configuration information</param>
/// <param name="validatorType">The validator type to load</param>
/// <param name="propertyStore">The store to hold the 
/// retrieved default properties and values</param>
private void LoadDefaultProperties( XmlDocument configurationDocument, 
  string validatorType, StringDictionary defaultPropertiesStore )
{
   // Select the node that holds the default 
   // properties for the specified validator
   XmlNode defaultValidatorNode = 
     configurationDocument.SelectSingleNode( "//Defaults/" + 
     validatorType );

   // If there was a node containing default validator properties
   if ( defaultValidatorNode != null )
   {
      // For each validator property
      foreach( XmlNode defaultValidatorProperty in 
        defaultValidatorNode.ChildNodes )
      {
         // Only process XML elements and ignore comments, etc
         if ( defaultValidatorProperty is XmlElement )
         {
            // Insert the property name and the 
            // default value into the store of default 
            // properties store
            string propertyName = GetAttribute( defaultValidatorProperty, 
              "name" );
            string propertyValue = GetAttribute( defaultValidatorProperty, 
              "value" );
            defaultPropertiesStore[ propertyName ] = propertyValue;
         }
      }
   }
}

Again, this code is quite simple. The method is given the XML configuration document, the type of validator to process, and the StringDictionary into which the values should be loaded.

We get hold of the default validator node in the configuration document by using the SelectSingleNode method and passing an appropriately formed XPath expression. If this returns a default validator node, we will have something of the form indicated below.

<RequiredField>
   <Property name="InitialValue" value="" />
   <Property name="Text" value="This is a required field" />
   <Property name="ErrorMessage" 
      value="You must enter something for the {FriendlyName}" />
</RequiredField>

The sample default RequiredField node indicated above shows that there are a number of child nodes that hold the actual property name and value, and it is these that represent the property values we are interested in.

We get to the collection of child nodes by accessing the ChildNodes property of the defaultValidatorNode. For each of the child nodes, we use the GetAttribute method to extract the name/value attributes and insert them into the StringDictionary as a key/value pair.

It is worth noticing at this point that it is also possible to include parameters in the property values, and this is seen in the ErrorMessage property; the actual value for the FriendlyName parameter will be defined for a specific validator defined in the ValidatorSets node of the configuration document.

Loop through each validator collection

Now that we have loaded all the common and default values specified in the configuration document, we can load the sets of validator collections held in the ValidatorSets node.

Purely to aid readability of the code, the constructor will invoke a LoadAllValidatorCollections method and pass in the XML configuration document and the array of hash tables that hold the default values.

The LoadAllValidatorCollections method is shown below.

/// <summary>
/// Loads all of the validator collections
/// </summary>
/// <param name="configurationDocument">The XML document 
///  that holds the configuration information</param>
private void LoadAllValidatorCollections( XmlDocument 
  configurationDocument, StringDictionary[] defaultProperties )
{
   // Select the node that holds all of the 
   // validator collections for a given user input field
   XmlNode allValidatorCollections = 
     configurationDocument.SelectSingleNode( "//ValidatorSets" );

   // If we got the node that holds the validator collections
   if ( allValidatorCollections != null )
   {
      // Iterate through the validator collections
      foreach ( XmlNode validatorCollection in 
        allValidatorCollections.ChildNodes )
      {
         // Load the validator collection for the user input field
         if ( validatorCollection is XmlElement )
         {
            LoadIndividualValidatorCollection( validatorCollection, 
              defaultProperties );
         }
      }
   }
}

This method is remarkably similar to the LoadDefaultProperties method in that it processes a series of child nodes. The LoadIndividualValidatorCollection method is invoked in order to load an individual validator collection from the XML configuration document.

Load the individual validator collection

A sample configuration document validator collection for a password field is given below.

<ValidatorCollection id="Password" FriendlyName="Password" 
  LegalValues="alphabetic characters and numbers" 
  ControlToValidate="TextBoxPassword">
   <Validator type="RequiredField" />
   <Validator type="RegularExpression">
      <Property name="ValidationExpression" value="[A-Za-z0-9]*" />
   </Validator>
</ValidatorCollection>

There are two things that we need to understand about this XML node in order to process it correctly. Firstly, the id attribute will match the ID property of a PlaceHolder control on a Web form. This tells us where we should add our dynamically created validation controls so they can be rendered correctly and displayed as part of the Web form.

Secondly, the ControlToValidate attribute will match the ID property of some user input control to which all the dynamically created validator controls identified in the collection will be applied. Lastly, the ValidatorCollection node will define parameters as attributes, and this is shown above for the FriendlyName and LegalValues parameters.

The LoadIndividualValidatorCollection method will first create an ArrayList used to hold the data from the individual Validator child nodes. Then, for each validator child node, it will create a StringDictionary to hold the actual property names and values. The attributes of the child node will be accessed in order to remember information such as the identifier and the control to validate, and so on. This is shown below.

/// <summary>
/// Load a collection of validators to be applied to a given user input field
/// </summary>
/// <param name="validatorCollection">The validator collection</param>
/// <param name="defaultProperties">The default property values</param>
private void LoadIndividualValidatorCollection( XmlNode 
  validatorCollection, StringDictionary[] defaultProperties )
{
   // The list of validators to be applied to the given field
   ArrayList validatorList = new ArrayList();

   // Remember the control to validate
   string controlToValidate = GetAttribute( validatorCollection, 
     "ControlToValidate" );

   // Iterate through each validator in the collection
   foreach( XmlNode validatorNode in validatorCollection.ChildNodes )
   {
      // Only process XML elements and ignore comments, etc
      if ( validatorNode is XmlElement )
      {
         // Use a new string dictionary to hold the validator's 
         // properties and values
         StringDictionary validatorProperties = new StringDictionary();

         // Remember which control this validator should validate
         validatorProperties[ "ControlToValidate" ] = controlToValidate;

         // Remember the type of validator
         string typeofValidator = GetAttribute( validatorNode, "type" );
         validatorProperties["ValidatorType"] = typeofValidator;

         // Add the ServerValidate event handler (only used on Custom validators)
         validatorProperties[ "ServerValidate" ] = 
           GetAttribute( validatorNode, "ServerValidate" );

At this point, we have recorded some control information used to define property values specific to an individual validator. For example, we have recorded which user input control should be validated, what type of validator control should be created, and, if given, the name of the method to invoke on ServerValidate events. It is now time to load the property names and values specific to this validator into the validatorProperties StringDictionary.

We first load the common property values and then the default property values into the validatorProperties StringDictionary before loading the specific values from the configuration document. We handle the assigning of common and default values by invoking an AssignDefaultValues method. The private method accepts two parameters: the StringDictionary that will hold the property names and values for this validator and another StringDictionary that contains the common or default property and values.

In this way, default property values overwrite, and therefore take precedence over the common values. This is shown below.

// Assign the default property values common to all validators
AssignDefaultValues( validatorProperties, 
  defaultProperties[(int) ValidatorTypes.Common] );

// Assign the default property values specific to this type of validator
ValidatorTypes validatorType = (ValidatorTypes) Enum.Parse( 
  typeof(ValidatorTypes), typeofValidator );
AssignDefaultValues( validatorProperties, 
  defaultProperties[(int) validatorType] );

We've used a little trick to determine which of the StringDictionarys held in the defaultProperties array should be used. Remember the declaration of the ValidatorTypes enumeration earlier?

// These are the validator types that we will cater for
private enum ValidatorTypes {Common, Compare, 
  Custom, Range, RegularExpression, RequiredField, 
  ValidationSummary};

The typeOfValidator variable is a string picked up from the "type" attribute of the validatorNode and it will have one of the values: "Compare", "Custom", and so on. As this is a string representation of the name of one of the enumerated constants, we can use the static Parse method of the Enum class to get an equivalently enumerated object.

As we have not specified an underlying type for the enumeration, the default Int32 type is used. As such, casting the validatorType enumeration to an Int32 gives us the integer value from the enumeration. We can then index into the defaultProperties array of StringDictionarys for the correct validator type's default values.

All that remains is for us to iterate through each child node of the validator to access the properties and values specifically defined for this validator. After this, we can replace any field name parameters with the values derived from the equivalently named attributes of the ValidatorCollection node. Finally, we can add the StringDictionary of specific property names and values to the ArrayList.

This is shown below.

// Iterate through each property node
foreach ( XmlNode propertyNode in validatorNode.ChildNodes )
{
   // Only process XML elements and ignore comments, etc
   if ( propertyNode is XmlElement )
   {
      // Add property names/values explicitly given for this validator
      string propertyName = GetAttribute( propertyNode, "name" );
      string propertyValue = GetAttribute( propertyNode, "value" );
      validatorProperties[ propertyName ] = propertyValue;
   }
}

// Now we have the string dictionary, make any fieldname 
//  replacements that might have been specified
ReplaceFieldnamesWithValues( validatorProperties, validatorCollection );

// Finally, add the string dictionary containing the 
// validator property values to the list of validators 
// for this group
validatorList.Add( validatorProperties );

I'm not going to go into the fine detail of how the fieldnames are replaced with the appropriate values. Simply, we have to loop through property values held in the string dictionary looking for an open brace. When we find one, we get the string up to the close brace, and this is our parameter name. We then expect to find an attribute of this name on the validator collection node, and we take its InnerText property as the parameter value. It is then a simple matter of string replacement to substitute the parameter name with the parameter value.

Dynamically Load Validation Controls

We now have the configuration file loaded into a runtime data structure as defined in Figure 1, above, and we can turn our attention to the fun part: to dynamically create the validator controls and add them to the PlaceHolder.

The LoadDynamicValidators method will take a single input parameter: the UserControl that hosts the placeholders. The basic idea is that this method will iterate through the collection of controls hosted by the user control. If a PlaceHolder control is found, we will index into the validatorCollections hash table to see if there is set of validator controls that should be dynamically created for this PlaceHolder. If there is, we simply iterate through them, create the appropriate control, set its properties according to the values found in the StringDictionary, and add it to the PlaceHolder's controls. When the Web page is rendered to the client browser, the validation controls we have dynamically created will be included and the user's input will be validated as defined by the configuration file.

Note that for this implementation, I have restricted user input controls to being hosted within a user control. If you have user input controls on the actual ASPX Web page, you will need to modify or overload this method accordingly.

The LoadDynamicValidators' method signature, iterating through the user control's child controls and detecting whether we have a PlaceHolder control is shown below.

/// <summary>
/// Dynamically load validators into a placeholder
/// </summary>
/// <param name="placeHolder">The place holder to load the 
/// validators into</param>
/// <param name="userControl">The user control that 
/// hosts the user input fields and validation controls</param>
public void LoadDynamicValidators( UserControl userControl )
{
   foreach ( Control childControl in userControl.Controls )
   {
      if ( childControl is PlaceHolder )
      {
         // Assign a place holder control, purely for readability
         PlaceHolder placeHolderControl = (PlaceHolder) childControl;

Note that we make use of a placeHolderControl variable. This is purely for readability as we could have directly referenced the childControl identifier within the body of the foreach statement. However, using the placeHolderControl variable makes the code a little easier to read and understand at the expense of a overhead of a few extra clock cycles and a miniscule memory consumption.

We now need to determine whether the placeHolderControl has an entry in the hash table of the validatorCollections hash table. This is shown below.

// Get the list of validators to be dynamically 
// added to this userControlChildControl
ArrayList validatorList = (ArrayList) 
  validatorCollections[ placeholderControl.ID ];

// Only process controls that have been configured 
// to contain dynamically created validator controls
if ( validatorList != null )
{

If validatorList is not null, that means that we have a list of validators that should be applied. We will need to dynamically create each specified validation control and set their properties accordingly. This is shown below.

// Loop through each validator in the list
for ( int iCnt = 0; iCnt < validatorList.Count; iCnt++ )
{
   // Get the string dictionary of property name/values for the validator
   StringDictionary validatorProperties = 
    (StringDictionary) validatorList[iCnt];

   // Create and add a spacer to go between each 
   // dynamically created placeholderControl
   // Note that whether this is done (and what is added) 
   // could be driven from the configuration file
   Literal spacer = new Literal();
   spacer.Text = "&nbsp;";
   userControl.Controls.Add( spacer );

   // Dynamically create and populate the validator type 
   // based on configuration information held in the string 
   // dictionary
   switch( validatorProperties["ValidatorType"].ToLower() )
   {
      // Each case statement has the same form:
      //    (1) create the correct type of validator,
      //    (2) set the properties of the validator
      //    (3) add it to the placeholderControl placeholderControl
      case "range":
         RangeValidator rangeValidator = new RangeValidator();
         SetProperties( rangeValidator, validatorProperties );
         placeholderControl.Controls.Add( rangeValidator );
         break;

      // The requiredfield, regularexpression, compare, 
      // validationsummary are omitted from this code snippet 
      // in the interests of brevity.  However, 
      // they are similar to that for the range validator

      // Custom validators also need the event handler to be set
      case "custom":
         CustomValidator customValidator = new CustomValidator();
         SetProperties( (Control) customValidator, validatorProperties );
         SetEventHandler( (Control) customValidator, 
           validatorProperties, userControl );
         placeholderControl.Controls.Add( customValidator );
         break;
   }
}

We get the string dictionary that holds the property names and values specified in the configuration file by indexing into the validatorList and casting appropriately. In the string dictionary, the keys map to the various property names and the associated values map to a string representation of the property value.

In the code above, we automatically add in a literal holding an HTML non-breaking space. This could (and probably should) come from the configuration file. It is easy to imagine additional attributes, "HTMLPrefix" say, on the various Defaults nodes as well as the ValidatorCollection and Validator nodes that would enable common, default, and specific HTML to be added. And there's no reason why we couldn't cater for an "HTMLPostfix" attribute as well.

The ValidatorType key of the string dictionary tells us which type of validator control should be dynamically created. The switch statement is used to direct flow control to the appropriate case statement, and we can finally create the validator control and set its properties.

Apart from CustomValidator controls, each case statement has the same pattern: create the validation control, set its properties as defined by the string dictionary, and add it to the PlaceHolder control.

We won't look at the detail of the private SetProperties method that gets invoked, as it is a fairly simple routine. It iterates through each key in the string dictionary and interprets this as a property name. Using reflection, we get a PropertyInfo object that will allow us to set the value of the control's property. We then create an object of the correct type and finally use the PropertyInfo's SetValue method to set its value.

Although we have used reflection to set the control's properties, this wouldn't be my recommended option for a real-world scenario. The cost of reflection from a throughput and performance perspective is too great. In practice, there would be a series of switch statements that would first determine the type of validator being used and the property being set. Then another switch statement would determine and set the property directly. However, in order to keep the demonstration code relatively simple and easy to read, I have opted for reflection.

In the code snippet above, we had to cater for setting an event handler for CustomValidator controls. The CustomValidator control is used to provide a user-defined (that is, developer-defined) validation function for an input control. CustomValidator controls always have a server-side validation function and they may have a client-side validation function. The client-side validation function is easily handled in our code, as it is just a string property and can be specified, and it is accessed and set just like any other string type property (for instance, ErrorMessage or Text).

Normally, the Web form developer would create a method in the code-behind of the Web form or user control and specify that should be invoked in response to the ServerValidate event. As part of the build process, Visual Studio would take care of doing all the difficult work behind the scenes in order to implement this. Unfortunately, there is no such luxury for us, and we have to use reflection in order to get this done.

The SetEventHandler method is used to set an event handler and is shown below.

/// <summary>
/// Set an event handler on a validation control to 
/// invoke a emthod in the user control
/// </summary>
/// <param name="validationControl">The validation 
/// control that wil raise the event</param>
/// <param name="eventName">The name of the event</param>
/// <param name="methodName">The method to invoke</param>
/// <param name="userControl">The user control on 
/// which the method is declared</param>
private void SetEventHandler( Control validationControl, 
  string eventName, string methodName, UserControl userControl)
{
   if ( methodName != null && eventName != null )
   {
      // Get the type object for the validation control
      Type childControlType = validationControl.GetType();

      // Get information on the event
      EventInfo eventInfo = childControlType.GetEvent( eventName );

      // Create a delegate of the correct type that will 
      // invoke the specified method on the class instance 
      // of the user control
      Delegate delegateEventHandler = 
        (Delegate) Delegate.CreateDelegate( eventInfo.EventHandlerType, 
         userControl, methodName);

      // Add the delegate as the eventhandler for the child control
      eventInfo.AddEventHandler( validationControl, delegateEventHandler );
   }
}

Assuming we have been given a non-null event name and method name, the first thing to do is to get the Type object of the validation control. This forms the root of reflection functionality and is the primary way to access metadata, such as event information.

The event model of the Microsoft .NET Framework is based on having a delegate that connects an event with its handler (and this is what we are trying to do—connect an event with its handler).

The delegate class is able to hold a reference to a method. Unlike other classes, a delegate class can hold references only to methods that match its own signature. As such, a delegate is equivalent a type-safe function pointer.

We invoke GetEvent on the validation control's Type object to get information about the specified event. This allows us to access the Type object of the handler associated with this event. From this we are able to dynamically create a delegate of the correct type.

There are a number of overloads of the static CreateDelegate method of the Delegate class. The one that we use allows us to create a delegate for (or you can think of it as a function pointer to) a specified instance method on a given class. As such, we create a delegate of the correct type that will invoke the method specified by the methodName parameter implemented in the user control object.

Once we have our delegate event handler, we simply add it to the validation control by invoking the eventInfo's AddEventHandler. Once this is complete, whenever the event is raised on the validation control, it will invoke the specified method from the instance of the user control.

Using the DynamicValidationManager

So, we now have a class that, given a configuration file, implements a mechanism to dynamically create validation controls and apply them to various user input controls.

But how do we use this class in a Web application? It couldn't be simpler, really. In the Web application project that accompanies this article, I have created a very few forms that collect information from the user. The idea is that they are registering information for a free e-mail account. Once this information is entered, the user can log in to their account and, were it a real application, be able to send and receive e-mails.

I have implemented a simple form of "one page Web application". This single page loads various user controls as specified by the "page" Request parameter. This means that the whole of the business logic of the application is actually implemented in a series of user controls.

The code-behind for the user controls is modified so that they derive from a helper class called DVCUserControl (which itself derives from the System.Web.UI.UserControl). We override the OnInit method so that we can take the additional steps necessary to create an instance of the user control. As we want to dynamically load validator controls into the various placeholders on the user control, we simply invoke the LoadDynamicValidators method of the dynamic validation manager.

This is shown below.

/// <summary>
/// Used to perform any initialization steps required 
/// to create and set up this instance
/// </summary>
/// <param name="e">The event arguments</param>
protected override void OnInit( EventArgs e)
{
   // Load all dynamically created validators for this user control
   DynamicValidationManager.LoadDynamicValidators( this );
}

Conclusion

This article has described a mechanism to implement the dynamic creation of validation controls for Web applications. This mechanism is most effective when there are either user input controls for similar information spread across many pages or if there are frequent changes to the underlying properties of the validation controls.

The mechanism frees Web developers from having to concentrate on user input validation and allows them to concentrate on implementing business logic. As such, this mechanism will simultaneously improve the consistency of the end user experience whilst also increasing developer productivity.

Related Books

 

About the author

Callum Shillan is a Principal Consultant working for Microsoft in the UK, specializing in Internet Business. He's been working with C# and ASP.NET on large Internet Web sites for the last few years. Callum can be reached at callums@microsoft.com.

Show:
© 2015 Microsoft