¿Le resultó útil esta página?
Sus comentarios sobre este contenido son muy importantes. Háganos saber su opinión.
¿Tiene comentarios adicionales?
Caracteres restantes: 1500
Exportar (0) Imprimir
Expandir todo
Expandir Minimizar

Creación dinámica de controles de validación (artículos técnicos sobre ASP.NET)

18 de Julio de 2005

Publicado: Octubre de 2004

Callum Shillan

Microsoft Consulting Service

Este artículo se aplica a:

Microsoft Framework versiones 1.0 y 1.1

Microsoft Visual C#

Microsoft ASP.NET

Resumen: en este artículo se explica cómo utilizar un archivo de configuración XML para permitir la creación dinámica de controles de validación ASP.NET que se aplican automáticamente a los controles de entrada de usuario. Este procedimiento posibilita que el desarrollador de formularios Web se concentre únicamente en la implementación de la lógica empresarial y ofrece una interfaz de usuario más coherente. (22 páginas impresas.)

Descargar el código fuente de este artículo.

En esta página

Introducción y confesiones Introducción y confesiones
El archivo de configuración de la validación dinámica El archivo de configuración de la validación dinámica
Estructuras de datos Estructuras de datos
La clase DynamicValidationManager La clase DynamicValidationManager
Uso de DynamicValidationManager Uso de DynamicValidationManager
Conclusión Conclusión
Bibliografía relacionada Bibliografía relacionada

Introducción y confesiones

Me gustaría poder decir que las ideas que se ofrecen en este artículo provienen de algo tan noble como intentar que los desarrolladores Web se liberen de parte de la carga y se concentren en implementar la lógica empresarial o del deseo de crear un marco que permita ofrecer una interfaz de usuario más coherente, sin embargo, he de confesar que no ha sido así. El concepto del uso en formularios Web de controles de validación creados dinámicamente es el resultado de mi frustración con un equipo de interfaces de usuario.

Como ocurre en la mayoría de los proyectos de aplicaciones Web bien organizados, había implicados varios equipos con responsabilidades claramente definidas: desarrollo, pruebas, experiencia de los usuarios, etc. En este proyecto concreto utilizábamos ejemplos de casos de uso con diseños de pantalla asociados y todo funcionaba bastante bien. Incluso contábamos con un único documento que incluía la definición de (casi) cada campo de entrada de usuario, las pantallas en las que se utilizaban, si el campo era obligatorio, cuáles eran los caracteres válidos y cómo debían ser los mensajes de error de validación. De esta forma nos pusimos a trabajar de inmediato, comenzamos a desarrollar pantallas y todo iba muy bien. Pero de repente comenzaron los problemas...

En una de las reuniones matinales, el equipo de interfaces de usuario comentó que era preciso cambiar el lenguaje utilizado para informar de los errores de validación. No puedo recordar los detalles concretos: quizás se utilizaba una expresión demasiado ampulosa y era necesario concretar más; o quizás era justo al contrario. Fuera lo que fuera, volvimos a pasar por todas las pantallas para cambiar la redacción en las propiedades ErrorMesssage y Text de los controles de validación, a pesar de que esto suponía perder bastante tiempo. Una de las implicaciones de formar parte de un equipo es que se debe estar preparado para acomodarse al resto de los miembros, aunque esto suponga más trabajo para uno mismo.

Entonces volvió a ocurrir. Probablemente en esta ocasión se había consultado más con el cliente. Quizás había alguna otra buena razón para ello. La cuestión es que de nuevo nos vimos implementando cambios que, en esta ocasión, eran más extensos: era preciso cambiar el color de los mensajes, los campos que antes eran obligatorios ahora pasaban a ser opcionales y viceversa, modificamos los conjuntos de caracteres permitidos y así cambios y más cambios.

En ese momento nos dimos cuenta de que era preciso protegernos. De alguna forma debíamos separar el desarrollo del formulario de la validación de las entradas. También advertimos que necesitábamos liberarnos para poder concentrarnos en la implementación de la lógica empresarial sin tener que preocuparnos por cuál debía ser el color o la redacción de un mensaje de error. Más aún, necesitábamos un marco que nos ayudara a crear una validación de entradas coherente que se pudiera modificar con facilidad y que no nos obligara a cambiar cada una de las páginas ASPX.

Después de una intensa sesión de análisis, obtuvimos como resultado una versión de la implementación que se presenta más adelante. En esencia, cada campo de entrada dispone de un control PlaceHolder asociado. La propiedad ID de PlaceHolder se indiza en un archivo de configuración para recuperar una colección de validadores creados dinámicamente y que se agregan a PlaceHolder. Cuando el formulario Web se procesa en el cliente, los controles de validación también se incluyen, y las entradas de usuario se validan según el procedimiento habitual. En realidad, bastante simple.

Después de quitar todos los controles de validación de las páginas ASPX y de insertar los controles PlaceHolder, podíamos pasar el archivo de configuración al equipo de interfaces de usuario para que ellos se encargaran de rellenarlo y de implementar los cambios que fueran necesarios.

El concepto es muy fácil de explicar, pero en este artículo también vamos a tratar el acceso a los datos que se incluyen en un archivo XML, explicaremos cómo copiar en caché los datos de configuración dentro de una Hashtable que contiene ArrayLists de StringDictionaries y cómo hacer uso de la reflexión para obtener acceso y establecer las propiedades de los controles de validación creados dinámicamente. Por cierto, aunque no recomiendo el uso de la reflexión en un escenario real porque puede afectar al rendimiento, sí que se ha utilizado en este artículo porque permite mantener la simplicidad del código, como ya veremos más adelante.

El archivo de configuración de la validación dinámica

Lo primero que debemos hacer es decidir cómo vamos a incluir la información de configuración. Existen diversas formas de almacenarla: en una base de datos, en un archivo de recursos, en un archivo XML, etc. En nuestro caso almacenaremos la información en un archivo XML por la sencilla razón de que resulta más fácil de modificar y manipular.

El archivo de configuración incluirá dos secciones: una que definirá los valores de propiedad comunes y predeterminados que se emplean con todos los validadores, y otra que definirá la colección de validadores y de propiedades que se aplicarán a un campo de entrada de usuario concreto.

Sección Defaults

Como lo último que deseamos es tener que especificar continuamente propiedades repetidas para cada control validador, vamos a utilizar una sección Defaults del archivo de configuración para especificarlas todas de una sola vez. Esto significa que, por ejemplo, no deberemos especificar "ForeColor='RED'" en cada control de validación que definamos. Si fuera necesario, estos valores de propiedad predeterminados se pueden reemplazar para un validador específico.

Existe un conjunto de propiedades que son comunes a todos los validadores, sea cual sea su tipo. No voy a mencionar la lista completa, pero algunos de los que me vienen a la memoria son CssClass, ForeColor y Visible. En cuanto a las propiedades de validador comunes, contaremos con una subsección en el archivo de configuración que incluirá los valores comunes que se van a aplicar a todos los validadores creados dinámicamente, independientemente de su tipo.

Cada tipo de control validador también dispone de un conjunto único de propiedades (por ejemplo, la propiedad ShowMessageBox de ValidationSummary), por lo que precisaremos de una subsección que contenga los valores de propiedad predeterminados.

Por último, sería una buena idea poder parametrizar estos valores. Esto nos permitiría establecer un único valor de propiedad predeterminado ErrorMessage del validador RequiredField como algo similar a "Debe escribir algún valor para {FriendlyName}" y definir el nombre descriptivo en algún otro sitio.

Como cabía esperar, la sección Defaults del archivo de configuración es un nodo XML que incluye nodos secundarios para los valores de propiedad comunes y específicos del validador. Se muestra a continuación.

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

Observe que el valor predeterminado de la propiedad ForeColor de ValidationSummary debería reemplazar el valor contenido en el nodo común, ya que se trata del comportamiento esperado. Pero este comportamiento no ocurre por arte de magia; será necesario implementarlo en el código.

Sección ValidatorSets

Cualquier campo de entrada de usuario precisa de una colección de controles de validación para asegurar que la entrada se valida correctamente. Por ejemplo, un campo de contraseña puede necesitar los validadores RequiredFieldValidator y RegularExpressionValidator para poder garantizar que el usuario escribe algún valor en la entrada y que utiliza un conjunto de caracteres definido.

ValidatorCollection dispondrá de un atributo ID y de una colección de nodos secundarios Validator para cada validador que se vaya a crear y a aplicar al campo de entrada de usuario. El atributo ID de ValidatorCollection coincidirá con la propiedad ID de PlaceHolder en el formulario Web para poder conectar el campo de entrada de usuario con la colección de validadores que se deben crear y aplicar.

Por ejemplo, la colección ValidatorCollection del campo de contraseña puede tener una propiedad ID "Password" con dos nodos secundarios Validator: uno para el campo obligatorio y otro para la expresión regular. Los nodos secundarios Validator contienen los valores de propiedad que reemplazan los definidos en la sección Defaults o que no se han definido en ella.

Como habrá varios campos de entrada de usuario, también existirá un conjunto de nodos ValidatorCollection contenido en un único nodo ValidatorSets del archivo de configuración.

Aquí se ofrece un fragmento del archivo de configuración que muestra los campos de dirección de correo electrónico y de contraseña.

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

La colección de la dirección de correo electrónico cuenta con un único nodo secundario Validator que indica que se debe crear dinámicamente un RequiredFieldValidator y aplicarse al control de entrada de usuario TextBoxEmailAddress. Como el nodo Validator no dispone de nodos secundarios, todos los valores de propiedad se derivarán de los definidos en el nodo Defaults. De nuevo, esto no ocurre por arte de magia, es necesario implementar este comportamiento en el código.

La colección de la contraseña cuenta con dos nodos secundarios Validator que indican que se deben crear dinámicamente los validadores RequiredFieldValidator y RegularExpressionValidator y aplicarse al control de entrada de usuario TextBoxEmailPassword. RegularExpressionValidator debe tener la propiedad ValidationExpression establecida en "[A-Za-z0-9]". De nuevo, el validador RequiredFieldValidator debe tomar sus valores de propiedad de los definidos en el nodo Defaults.

Y aquí es donde definimos los valores de nombre de campo. Observe que disponemos de un atributo llamado FriendlyName en el nodo ValidatorCollection. El texto interno de dicho atributo se emplea para reemplazar cualquier instancia de {FriendlyName} que se encuentre en los valores de propiedad contenidos en el archivo de configuración. Esto significa que muchas de las colecciones ValidatorCollection pueden hacer referencia a la misma definición de validador de campo obligatorio predeterminado y que todo lo que deben hacer es identificar el nombre descriptivo para poder personalizar los mensajes que se muestran al usuario final.

Estructuras de datos

Vamos a necesitar algún tipo de estructura de datos en la que incluir toda esta información de configuración. Para no complicarnos demasiado dedicaremos algún tiempo a crearla y después la incluiremos en la caché de la aplicación ASP.NET.

Este tipo de estructura es un buen candidato para incluirse en la caché de la aplicación porque resulta relevante para cada página individual dentro de la misma. También podemos establecer una dependencia de caché en el archivo de configuración para que, si el archivo se modifica, la entrada de caché resulte con el valor "void" y se vuelva a crear en el siguiente acceso.

La estructura de datos que vamos a incluir en la caché de la aplicación se muestra a continuación.

Figura 1. Estructura de datos incluida en la caché de la aplicación

Hashtable contará con una entrada para cada campo de entrada de usuario al que se aplicarán controles validadores creados dinámicamente. La clave (por ejemplo, "email" y "psword" en la figura 1 anterior) coincidirá con la propiedad ID de PlaceHolder, lo que nos permitirá determinar qué lista de validadores debemos crear dinámicamente y agregar.

Por tanto, en algún lugar del formulario Web que tiene un campo de entrada de usuario para, por ejemplo, una dirección de correo electrónico, existirá un control PlaceHolder con un ID "email". Este último se utilizará para indizar en Hashtable y obtener acceso a la lista ArrayList del validador que se debe crear dinámicamente y agregarse a PlaceHolder. ArrayList se repetirá y las propiedades del validador creado dinámicamente se incluirán en el StringDictionary asociado.

La clase DynamicValidationManager

Para poder administrar este proceso vamos a crear una clase simple con dos métodos públicos: una construcción y un método que cargará dinámicamente los controles de validación de un formulario Web o un control de usuario determinados.

Para evitar tener que crear el administrador una y otra vez para el PlaceHolder de cada formulario Web, lo colocaremos en la caché de la aplicación. Cada acceso comprobará si es preciso crearlo. Se debe crear cuando se dan dos situaciones: en el primer acceso y si el elemento se ha eliminado. Como el objeto dependerá del archivo de configuración asociado, también podemos establecer una dependencia de caché para que, si el archivo se modifica, el administrador se elimine automáticamente de la caché de la aplicación.

Constructor del administrador de la validación dinámica

El constructor debe crear las estructuras de datos que se mostraban en la figura 1 anterior. Para ello se deben seguir los siguientes pasos:

  1. Definir las propiedades privadas del administrador y la firma del constructor.

  2. Cargar el archivo de configuración en un documento XML.

  3. Cargar los valores de propiedad comunes y predeterminados.

  4. Realizar un bucle por cada colección de validadores
    Cargar la colección de validadores individual
    Realizar un bucle por cada propiedad definida para el validador
    Insertar el valor de propiedad común o predeterminado, si se define, en un diccionario de cadenas
    Sobrescribir o insertar el valor de propiedad específico en el diccionario de cadenas
    Agregar el diccionario de cadenas a la lista de matrices de validadores
    Agregar la lista de matrices a la tabla hash de colecciones de validadores.

  5. El código utilizado para implementar estos pasos se describe a continuación.

Definición de las propiedades privadas y de la firma del constructor

Precisaremos dos propiedades que son privadas para el administrador de la validación dinámica: la lista de tipos de controles validadores que vamos a procesar y la tabla hash de colecciones de validadores. La firma del constructor contará con un parámetro que definirá la ruta al archivo de configuración. Se muestra a continuación.

Se emplea la enumeración ValidatorTypes porque ofrece un mecanismo sencillo para procesar una lista de equivalentes de cadena o para obtener acceso a un índice de enteros. Podemos obtener la lista de valores de cadena con el método estático GetNames() de la clase Enum y asignar una instancia de la enumeración a un entero para poder obtener su índice.

Carga del archivo de configuración en un documento XML

El código utilizado para cargar el archivo de configuración es bastante simple:

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

Empleamos XmlTextReader para poder disponer de un acceso a los datos XML rápido, sin pasar por caché y de solo avance. Procesaremos el archivo de configuración lo más rápidamente posible; no hay necesidad de copiarlo en caché, ya que los datos se incluyen en la estructura de datos.

Carga de los valores de propiedad comunes y predeterminados

Utilizaremos una matriz temporal de StringDictionaries para almacenar los valores comunes y predeterminados. Cada StringDictionary contendrá todos los valores de propiedad comunes y predeterminados definidos en el nodo Defaults del archivo de configuración. De este modo, cuando llegue el momento de establecer los valores específicos de un validador definido en un nodo ValidatorCollections, podremos obtener acceso a la tabla hash de valores predeterminados muy rápidamente.

El código para cargar los valores de propiedad predeterminados es el siguiente:

// 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++;   
}

Definimos la matriz defaultProperties de StringDictionaries y utilizamos la longitud de la matriz devuelta por el método estático GetNames() de la clase Enum para obtener el número correcto de entradas.

A continuación, realizamos un bucle por cada una de las entradas de la matriz de nombres de la enumeración ValidatorTypes y cargamos las propiedades predeterminadas invocando el método LoadDefaultProperties.

Aquí se muestra el código para el método LoadDefaultProperties:

/// <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;   
         }   
      }   
   }   
}

De nuevo, el código es bastante simple. Al método se le facilita el documento de configuración XML, el tipo de validador que debe procesar y el StringDictionary en el que se deben cargar los valores.

Localizamos el nodo de validador predeterminado en el documento de configuración mediante el método SelectSingleNode y pasando una expresión XPath formada adecuadamente. Si devuelve un nodo de validador predeterminado, obtendremos algo similar a lo siguiente:

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

El nodo predeterminado de muestra RequiredField indicado en el ejemplo anterior muestra que existen varios nodos secundarios que contienen el valor y el nombre de propiedad reales y que son estos los que representan los valores de propiedad que nos interesan.

Llegamos a la colección de nodos secundarios obteniendo acceso a la propiedad ChildNodes de defaultValidatorNode. En cada uno de los nodos secundarios, utilizamos el método GetAttribute para extraer los atributos de nombre y valor e insertarlos en StringDictionary como par de clave y valor.

Merece la pena destacar en este punto que también se pueden incluir parámetros en los valores de propiedad, como se demuestra con la propiedad ErrorMessage; se definirá el valor real del parámetro FriendlyName para un validador específico definido en el nodo ValidatorSets del documento de configuración.

Realización de un bucle por cada colección de validadores

Después de haber cargado todos los valores comunes y predeterminados especificados en el documento de configuración, podemos cargar los conjuntos de colecciones de validadores que incluye el nodo ValidatorSets.

Básicamente para simplificar la lectura del código, el constructor invocará un método LoadAllValidatorCollections y pasará el documento de configuración XML y la matriz de tablas hash que contienen los valores predeterminados.

A continuación se muestra el método LoadAllValidatorCollections.

/// <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 );   
         }   
      }   
   }   
}

Este método resulta bastante similar al método LoadDefaultProperties al procesar también una serie de nodos secundarios. Se invoca el método LoadIndividualValidatorCollection para poder cargar una colección individual de validadores desde el documento de configuración XML.

Carga de la colección individual de validadores

Aquí se incluye una muestra de una colección de validadores del documento de configuración sobre el campo de contraseña.

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

Es preciso comprender dos cosas sobre este nodo XML para poder procesarlo correctamente. En primer lugar, que el atributo id coincidirá con la propiedad ID de un control PlaceHolder en un formulario Web. Esto nos indica dónde debemos agregar los controles de validación que hemos creado dinámicamente de forma que se puedan procesar correctamente y mostrarse como parte del formulario Web.

En segundo, que el atributo ControlToValidate coincidirá con la propiedad ID de cierto control de entrada de usuario al que se aplicarán todos los controles validadores creados dinámicamente que se han identificado en la colección. Por último, el nodo ValidatorCollection definirá los parámetros como atributos, como se mostraba anteriormente para los parámetros FriendlyName y LegalValues.

El método LoadIndividualValidatorCollection primero creará una ArrayList que se utiliza para contener los datos de los nodos secundarios Validator individuales. A continuación, crea para cada nodo secundario de validador un StringDictionary que contendrá los valores y los nombres de propiedad reales. Se obtendrá acceso a los atributos del nodo secundario para poder recordar información como el identificador y el control que se deben validar, etc. Se muestra a continuación.

/// <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" );

Hasta este punto hemos registrado cierta información de control utilizada para definir valores de propiedad específicos de un validador concreto. Por ejemplo, hemos registrado qué control de entrada de usuario se debe validar, qué tipo de control validador se debe crear y, si se proporciona, el nombre del método que se debe invocar en los sucesos ServerValidate. Ahora llega el turno de cargar los valores y nombres de propiedad específicos de este validador en el StringDictionary de validatorProperties.

Lo primero que debemos hacer es cargar los valores de propiedad comunes y, a continuación, los predeterminados en el StringDictionary de validatorProperties, antes de cargar los valores específicos desde el documento de configuración. Controlamos la asignación de los valores comunes y predeterminados mediante la invocación de un método AssignDefaultValues. El método privado acepta dos parámetros: el StringDictionary que contiene los valores y los nombres de propiedad del validador y otro StringDictionary que incluye los valores y propiedades comunes opredeterminados.

De esta forma, los valores de propiedad predeterminados sobrescriben y tienen prioridad sobre los comunes. Se muestra a continuación.

// 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] );

Hemos empleado un pequeño truco para determinar cuál de los StringDictionaries contenido en la matriz defaultProperties se debe emplear. ¿Recuerda la declaración de la enumeración ValidatorTypes expuesta anteriormente?

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

La variable typeOfValidator es una cadena recopilada del atributo "type" de validatorNode y tendrá uno de los siguientes valores: "Compare", "Custom", etc. Como se trata de una representación de cadena del nombre de una de las constantes enumeradas, podemos emplear el método estático Parse de la clase Enum para obtener un objeto enumerado equivalente.

No hemos especificado ningún tipo subyacente para la enumeración, por lo que se utiliza el predeterminado Int32. De esta forma, la asignación de la enumeración validatorType al tipo Int32 nos proporciona el valor entero desde la enumeración. En este momento podemos indizar en la matriz defaultProperties de los StringDictionaries para obtener los valores predeterminados del tipo de validador correcto.

Todo lo que queda por hacer es repetir en cada nodo secundario del validador para obtener acceso a los valores y propiedades definidos específicamente para él. Después de ello, podemos reemplazar cualquier parámetro de nombre de campo con los valores derivados de los atributos de nombre equivalentes del nodo ValidatorCollection. Por último, podemos agregar el StringDictionary de los valores y nombres de propiedad específicos a ArrayList.

Se muestra a continuación.

// 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 );

No me voy a detener en los detalles específicos de cómo se reemplazan los campos de nombre con los valores adecuados. Simplemente debemos realizar un bucle por los valores de propiedad contenidos en el diccionario de cadenas para buscar una llave abierta. Cuando la encontremos, debemos subir la cadena a una llave cerrada y obtendremos nuestro nombre de parámetro. A continuación, buscamos un atributo de este nombre en el nodo de la colección de validadores y tomamos su propiedad InnerText como valor de parámetro. Después, es tan sólo cuestión de reemplazar las cadenas para sustituir el nombre de parámetro por el valor de parámetro.

Carga dinámica de los controles de validación

Ahora que el archivo de configuración ya está cargado en una estructura de datos en tiempo de ejecución, como se ha definido anteriormente en la figura 1, podemos concentrar nuestra atención en la parte divertida: crear dinámicamente los controles validadores y agregarlos a PlaceHolder.

El método LoadDynamicValidators sólo toma un parámetro de entrada: el UserControl que aloja los marcadores de posición. La idea principal es que este método se repite en la colección completa de controles que aloja el control de usuario. Si se encuentra un control PlaceHolder, lo indizamos en la tabla hash validatorCollections para comprobar si debemos crear dinámicamente algún conjunto de controles validadores para PlaceHolder. Si es así, simplemente lo repetimos en ellos, creamos el control adecuado, establecemos sus propiedades en función de los valores encontrados en StringDictionary y lo agregamos a los controles PlaceHolder. Cuando la página Web se procesa en el explorador del cliente, los controles de validación que hemos creado dinámicamente se incluirán en ella y las entradas que realice el usuario se validarán como se haya definido en el archivo de configuración.

Tenga en cuenta que para esta implementación he restringido los controles de entrada de usuario de forma que se alojen en un control de usuario. Si dispone de controles de entrada de usuario en la página Web ASPX real, deberá modificar o sobrecargar este método según corresponda.

A continuación se muestra la firma del método de LoadDynamicValidators que se repite en los controles secundarios del control de usuario y que detecta si disponemos de un control PlaceHolder.

/// <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;

Observe que utilizamos una variable placeHolderControl. Lo hacemos únicamente para que su lectura resulte más sencilla, ya que podíamos haber hecho referencia directamente al identificador childControl dentro del cuerpo de la instrucción foreach. Sin embargo, al utilizar la variable placeHolderControl, el código resulta más fácil de leer y de comprender, con los únicos inconvenientes de algunos ciclos de reloj adicionales y un mínimo consumo de memoria.

Ahora debemos determinar si placeHolderControl cuenta con una entrada en la tabla hash de la tabla hash validatorCollections. Se muestra a continuación.

// 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 )   
{

Si el valor de validatorList no es nulo, significa que contamos con una lista de validadores que debemos aplicar. Será preciso crear dinámicamente cada control de validación especificado y establecer sus propiedades según corresponda. Se muestra a continuación.

// 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;   
   }   
}

Obtenemos el diccionario de cadenas que contiene los valores y nombres de propiedad especificados en el archivo de configuración mediante la indización en validatorList y la correspondiente asignación. En el diccionario de cadenas, las claves asignan a los distintos nombres de propiedad y los valores asociados asignan a una representación de cadena del valor de propiedad.

En el código anterior, hemos agregado automáticamente un literal que contiene un espacio sin salto HTML que podría (y probablemente debería) proceder del archivo de configuración. Resulta sencillo imaginar atributos adicionales, como "HTMLPrefix", en los distintos nodos Defaults, así como los nodos ValidatorCollection y Validator que permitirían agregar atributos comunes, predeterminados y HTML específicos. Y tampoco existe ninguna razón por la que no podamos optar también por un atributo "HTMLPostfix".

La clave ValidatorType del diccionario de cadenas nos indica qué tipo de control validador se debe crear dinámicamente. Utilizamos la instrucción switch para dirigir el control de flujo a la instrucción case adecuada y así podemos crear el control validador y establecer sus propiedades.

Aparte de los controles CustomValidator, cada instrucción case sigue el mismo patrón: crear el control de validación, establecer sus propiedades tal como se definen en el diccionario de cadenas y agregarlo al control PlaceHolder.

No analizaremos los detalles del método privado SetProperties que se invoca, porque resulta una rutina muy sencilla. Se repite en cada clave del diccionario de cadenas y se interpreta como nombre de propiedad. Gracias a la reflexión, obtenemos un objeto PropertyInfo que nos permite establecer el valor de la propiedad del control. Luego creamos un objeto del tipo correcto y utilizamos el método SetValue de PropertyInfo para establecer su valor.

Aunque en nuestro caso hemos utilizado la reflexión para establecer las propiedades de los controles, no recomiendo esta opción en un escenario real. El coste de la reflexión desde el punto de vista del rendimiento es demasiado alto. En la práctica, existiría una serie de instrucciones switch que primero determinarían el tipo de validador que se va a utilizar y la propiedad que se va a establecer. A continuación, otra instrucción switch determinaría y establecería la propiedad directamente. Sin embargo, para poder mantener la simplicidad del código de demostración, he optado por la reflexión.

En el fragmento de código anterior, tuvimos que optar por establecer un controlador de sucesos para los controles CustomValidator. El control CustomValidator se emplea para proporcionar una función de validación definida por el usuario (es decir, definida por el desarrollador) para un control de entrada. Los controles CustomValidator siempre presentan una función de validación del lado del servidor y pueden presentar una del lado del cliente. La función de validación del lado del cliente se puede controlar muy fácilmente en el código, ya que se trata simplemente de una propiedad de cadena que se puede especificar y a la que se puede obtener acceso y que se puede establecer igual que cualquier otra propiedad de tipo de cadena (por ejemplo, ErrorMessage o Text).

Normalmente, el desarrollador del formulario Web crea un método en el código subyacente del formulario Web o del control de usuario y especifica que se debe invocar en respuesta al suceso ServerValidate. Como parte del proceso de creación, Visual Studio se encargaría de realizar todo el trabajo difícil en un segundo plano para poder implementar todo esto. Por desgracia, nosotros no nos podemos permitir ese lujo y debemos emplear la reflexión para obtener el mismo resultado.

Utilizamos el método SetEventHandler para establecer un controlador de sucesos, como se muestra a continuación.

/// <summary>   
/// Set an event handler on a validation control to   
/// invoke a method in the user control   
/// </summary>   
/// <param name="validationControl">The validation   
/// control that will 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 );   
   }   
}

Suponiendo que se nos han proporcionado un nombre de método y un nombre de suceso no nulos, lo primero que debemos hacer es obtener el objeto Type del control de validación. Este objeto es la raíz de la funcionalidad de reflexión y constituye la forma de acceso primaria a los metadatos, por ejemplo, la información de los sucesos.

El modelo de sucesos de Microsoft .NET Framework se basa en la existencia de un delegado que conecta un suceso con su controlador (justamente lo que estamos tratando de lograr, conectar un suceso con su controlador).

La clase de delegado puede contener una referencia a un método. A diferencia de otras, esta clase puede contener referencias sólo a los métodos que coinciden con su propia firma. De esta forma, un delegado es equivalente a un puntero de función con seguridad de tipos.

Invocamos el método GetEvent en el objeto Type del control de validación para obtener información sobre el suceso especificado. Esto nos permite obtener acceso al objeto Type del controlador asociado con el suceso. A partir de este punto podemos crear dinámicamente un delegado del tipo correcto.

Existen varias sobrecargas del método estático CreateDelegate de la clase Delegate. La que utilizamos en nuestro caso nos permite crear un delegado (o si lo prefiere, un puntero de función) para un método de instancia especificado en una clase determinada. De esta forma podemos crear un delegado del tipo correcto que invocará el método especificado por el parámetro methodName que se ha implementado en el objeto de control de usuario.

Una vez disponemos de nuestro controlador de sucesos delegado, simplemente lo agregamos al control de validación invocando el método AddEventHandler de eventInfo. Cuando se ha completado este proceso, siempre que se genera un suceso en el control de validación, se invoca el método especificado desde la instancia del control de usuario.

Uso de DynamicValidationManager

texto

Por tanto, contamos con una clase que, disponiendo de un archivo de configuración, implementa un mecanismo que permite crear dinámicamente controles de validación y aplicarlos a distintos controles de entrada de usuario.

Pero, ¿cómo se utiliza esta clase en una aplicación Web? La verdad es que no puede ser más simple. En el proyecto de aplicación Web que acompaña a este artículo, he creado muy pocos formularios que recopilen información del usuario. La idea es el registro de información para una cuenta de correo electrónico gratuita. Una vez se ha facilitado la información, el usuario puede iniciar sesión en su cuenta y, si se tratara de una aplicación real, podría enviar y recibir correo electrónico.

He implementado un formulario simple de una aplicación Web de una página. Esta única página carga los distintos controles de usuario como se especifica en el parámetro Request de la página. Esto supone que toda la lógica empresarial de la aplicación en realidad se implementa en una serie de controles de usuario.

El código subyacente de los controles de usuario se modifica de forma que estos últimos deriven de una clase de aplicación auxiliar llamada DVCUserControl (que a su vez deriva de System.Web.UI.UserControl). Reemplazamos el método OnInit para poder realizar los pasos posteriores necesarios para crear una instancia del control de usuario. Como la intención es cargar dinámicamente los controles validadores en los distintos marcadores de posición del control de usuario, simplemente invocamos el método LoadDynamicValidators del administrador de la validación dinámica.

Se muestra a continuación.

/// <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 );   
}

Conclusión

En este artículo se ha descrito un mecanismo que permite implementar la creación dinámica de controles de validación para aplicaciones Web. Este mecanismo resulta más eficaz cuando existen controles de entrada de usuario para información similar repartidos por muchas páginas o cuando se deben realizar cambios frecuentes en las propiedades subyacentes de los controles de validación.

Permite liberar a los desarrolladores Web de la carga que supone la validación de las entradas de usuario para concentrarse en la implementación de la lógica empresarial. De esta forma, se contribuye tanto a mejorar la coherencia de la experiencia del usuario final como a incrementar la productividad del desarrollador.

Bibliografía relacionada

Acerca del autor

Callum Shillan desempeña la labor de asesor principal para Microsoft en el Reino Unido como especialista en negocios en Internet. En los últimos años ha trabajado con C# y ASP.NET en importantes sitios Web. Puede ponerse en contacto con Callum en callums@microsoft.com.

Mostrar:
© 2015 Microsoft