Extendiendo CodeDomSerializer
Cómo usar CodeDomSerializer y atributos personalizados para modificar el orden de generación de las instrucciones de deserialización de un componente

Fecha: 20/Abr/2004 (8 de Abril de 2004)
Autor: Néstor Soriano - konamiman@konamiman.com

 


En este artículo se muestra cómo usar una clase derivada de CodeDomSerializer, así como un atributo personalizado, para modificar el orden predeterminado en el que el diseñador de formularios de Visual Studio genera las instrucciones que reconstruyen un componente o control (usaré la palabra "componente" para referirme a ambos) en tiempo de ejecución. Esto resulta muy útil para aquellos componentes que requieren que sus propiedades sean establecidas en un orden determinado.

El problema

Como ya sabrás si has utilizado Visual Studio .NET, el diseñador de formularios de éste no es más que un generador de código que transforma el diseño que tú realizas visualmente en instrucciones que, ejecutadas en el constructor del formulario en cuestión, reconstruirán todos los componentes incluidos en el mismo. Por ejemplo, si añades un control de texto al formulario, puedes comprobar que se ha añadido automáticamente el siguiente código a la rutina de inicialización del formulario:

// 
// textBox1
// 
this.textBox1.Location = new System.Drawing.Point(3, 16);
this.textBox1.Name = "textBox1";
this.textBox1.TabIndex = 1;
this.textBox1.Text = "textBox1";

(También se añaden un par de instrucciones más para crear el control y añadirlo a la colección de controles del formulario). Si modificas las propiedades del control, el código generado también se modifica adecuadamente.

Ahora bien, una interesante cuestión es la siguiente: ¿en qué orden se generan las instrucciones que establecen las propiedades del componente? (o en otras palabras, ¿en qué orden se ejecutarán dichas instrucciones a la hora de reconstruir el componente?) La respuesta es simple: El diseñador de formularios genera las instrucciones siguiendo un orden alfabético según los nombres de las propiedades, y no existe a priori ninguna forma de modificar este comportamiento.

En teoría, esto no debería suponer ningún problema, ya que las propiedades de una clase bien diseñada deben poder establecerse en cualquier orden (tal y como recomienda Microsoft en la documentación MSDN). Sin embargo esto, aún siendo cierto en la mayoría de las ocasiones, puede no serlo en algunos casos. Por ejemplo, recientemente he desarrollado un componente llamado MultiRadioButton que representa un panel de controles RadioButton. Este componente se define por dos propiedades: Strings, que es una matriz de cadenas que representa todos los controles RadioButton que se mostrarán; y SelectedIndex, un entero que indica el índice del control actualmente seleccionado. Intentar establecer la propiedad SelectedIndex con un valor de índice superior al tamaño de Strings provoca, lógicamente, una excepción; por tanto, está claro que al reconstruir el control la propiedad Strings debe establecerse antes que la propiedad SelectedIndex. Pero el diseñador de formularios hace justamente lo contrario, ya que alfabéticamente "SelectedIndex" va antes que "Strings".

La solución

Afortunadamente existe una forma de influir en la forma en la que el diseñador de formularios genera el código de reconstrucción de los componentes. De hecho, podemos especificar exactamente cuáles serán las instrucciones de reconstrucción que serán generadas, aunque nosotros no llegaremos tan lejos (sólo queremos modificar el orden de las instrucciones que son generadas por el mecanismo predeterminado del diseñador de formularios).

La solución pasa por la clase CodeDomSerializer y el atributo DesignerSerializerAttribute. CodeDom es la parte de .NET Framework que permite representar la estructura de un documento de código fuente mediante una serie de objetos independientes de cualquier lenguaje de programación. Así, existe una clase de CodeDom que representa un comentario; otra clase representa una asignación; otra clase representa la llamada a un método, etc.

CodeDomSerializer es una clase abstracta que se usa para definir serializadores personalizados para componentes. Su método Serialize admite como entrada un componente y devuelve una colección de objetos CodeDom que el diseñador de formularios transforma en instrucciones de código fuente. Paralelamente, su método Deserialize admite como entrada una colección de instrucciones CodeDom y devuelve el componente reconstruido en base a dichas instrucciones.

El atributo DesignerSerializerAttribute se puede aplicar a cualquier clase de componente y permite especificar el serializador (cualquier clase derivada de CodeDomSerializer) que el diseñador de formularios usará para serializar y deserializar los componentes de esa clase a y desde instrucciones en código fuente.

Así pues, para resolver nuestro problema y poder elegir el orden en el que se generarán las instrucciones para serializar y deserializar el componente, vamos a hacer lo siguiente:

OrderedCodeDomSerializer "simplemente" reordenará la colección de instrucciones que genera la implementación de la clase base CodeDomSerializer, en base a los atributos DesignSerializationOrderAttribute hallados en las propiedades que definen el componente a serializar. Así, volviendo a mi control MultiRadioButton, para que las propiedades se generen en el orden que yo quiero definiría el control de la siguiente forma:

[DesignerSerializer(typeof(OrderedCodeDomSerializer),typeof(CodeDomSerializer))
public class MultiRadioButton:UserControl
{
    [DesignSerializationOrder(0)]
    public string[] Strings
    {
         get { /* Implementación de la propiedad */}
         set { /* Implementación de la propiedad */}
    }

    [DesignSerializationOrder(1)]
    public int SelectedIndex
    {
         get { /* Implementación de la propiedad */}
         set { /* Implementación de la propiedad */}
    }

    // Otras definiciones de propiedades y métodos
}

Así, el código que generará el diseñador de formularios para los controles MultiRadioButton será algo parecido a esto:

//
// multiRadioButton1
//
this.multiRadioButton1.Strings=new string[]
                                      {"Primer elemento",
                                       "Segundo elemento"};
this.multiRadioButton1.Selectedindex=0;
//Aquí van otras propiedades como Name, Size, etc.

Las reglas para decidir el orden de generación de las instrucciones son las siguientes:

  1. El orden de generación de las instrucciones depende del campo Order del atributo DesignSerializationOrder de cada propiedad: las propiedades con valores inferiores se serializan antes (el valor mínimo es cero).
  2. Si varias propiedades tienen el mismo valor en Order, se serializan en el orden predeterminado del diseñador de formularios; es decir, en orden alfabético según sus nombres.
  3. Si una propiedad no tiene atributo DesignSerializationOrder, se actúa como si tuviera uno con un valor de Int32.MaxValue para Order. Esto implica que las propiedades sin atributo se serializan después de todas las propiedades que sí tienen el atributo (y, por la regla anterior, en orden alfabético).

El código

Lo primero que hacemos es definir la clase DesignSerializationOrderAttribute, que es extremadamente sencilla; simplemente hay que establecer su parámetro Order en el constructor:

[AttributeUsage(AttributeTargets.Property)]
public class DesignSerializationOrderAttribute:Attribute
{
    private int pOrder;

    public DesignSerializationOrderAttribute(int order)
    {
        if(order<0)
         pOrder=Int32.MaxValue;
    else
         pOrder=order;
    }

    public int Order
    {
        get {return pOrder;}
    }
}

Quizá pienses que sería más adecuado iniciar una excepción que asignar Int32.MaxValue si se pasa un número negativo al constructor. Pues bien, yo también lo pensaba, y de hecho eso hice en la primera versión del código. Pero hay un problema: si se genera una excepción en el constructor del atributo aplicado a la propiedad de un componente, el diseñador de Visual Studio no muestra un mensaje de error (como sería lógico), sino que simplemente elimina el componente sin dar más explicaciones. Así las cosas, lo más lógico es asignar el valor Int32.MaxValue, que es como hacer que la propiedad no tenga atributo.

Y ahora vamos con el "hueso": la clase OrderedCodeDomSerializer. Esta clase tendrá la siguiente firma:

public class OrderedCodeDomSerializer:CodeDomSerializer,IComparer

y se compondrá de cuatro métodos:

Empezaremos por Deserialize que es la más sencilla, tan sencilla que podemos limitarnos a usar la implementación de ejemplo que hay en MSDN, que funcionará de maravilla:

public override object Deserialize(IDesignerSerializationManager manager, object codeObject) 
{
    CodeDomSerializer baseClassSerializer = (CodeDomSerializer)manager.
        GetSerializer(typeof(Component), typeof(CodeDomSerializer));

    return baseClassSerializer.Deserialize(manager, codeObject);
}

Tan sólo hemos necesitado un pequeño cambio con respecto al ejemplo de MSDN: El método GetSerializer admite en principio typeof(MyComponent).BaseType como primer parámetro; cambiándolo a typeof(Component) también parece funcionar bien (si no es cierto, admito tirones de orejas electrónicos) y conseguimos un código genérico que no depende de la clase a serializar. Por lo demás, no voy a entrar en detalles sobre lo que hace este método porque creo que no viene al caso; tan sólo recordar que el objeto devuelto es el componente reconstruido en base a la colección de objetos CodeDom pasada en el parámetro codeObject.

Más interesante es el método Serialize, que dejamos de la siguiente forma:

private object SerializedObject=null;
object codeObject;

public override object Serialize(IDesignerSerializationManager manager, object value) 
{
    SerializedObject=value;
    CodeDomSerializer baseClassSerializer = (CodeDomSerializer)manager.
        GetSerializer(typeof(Component), typeof(CodeDomSerializer));
 
    codeObject = baseClassSerializer.Serialize(manager, value);
 
    // Añadido respecto a la implementación base:
    ArrayList.Adapter((CodeStatementCollection)codeObject).Sort(this);

    return codeObject;
}

Aquí, codeObject es una colección de tipo CodeStatementCollection y contiene los objetos CodeStatement (la clase base de todos los objetos de CodeDom que representan instrucciones ejecutables) a partir de los cuales el diseñador de formularios generará el código fuente para reconstruir el componente en cuestión (que, por cierto, es pasado en el parámetro value y guardamos en la variable SerializedObject; definimos estas variables fuera de la función porque serán usadas por los métodos que quedan por explicar).

El punto clave de esta función es la línea añadida con respecto a la implementación base, en la que ordenamos la colección de objetos CodeStatement (ya generada por el código de la implementación base del método) usando el método Sort de ArrayList; para este menester nos viene de perlas el método estático ArrayList.Adapter, que crea un contenedor temporal ArrayList para cualquier objeto que implemente IList (como es el caso de CodeStatementCollection), lo cual nos permite usar el método Sort.

El método Sort admite como entrada un objeto que implemente IComparer, en el que se basa para comparar los objetos de la colección y así tener un criterio de ordenación. Pasamos como comparador this, una referencia al propio objeto, de forma que se usará la función Compare para comparar los objetos CodeStatement de la colección:

public int Compare(object x, object y)
{
    int orderX=GetOrderNumber((CodeStatement)x);
    int orderY=GetOrderNumber((CodeStatement)y);

    if(orderX!=orderY)
        return orderX-orderY;
    else
        return ((CodeStatementCollection)codeObject).IndexOf((CodeStatement)x)-
               ((CodeStatementCollection)codeObject).IndexOf((CodeStatement)y);
}

Por definición, la función Compare de IComparer debe devolver un número negativo si el primero objeto es menor que el segundo, positivo en caso contrario, y cero si son iguales. En este caso, los objetos a comparar son de tipo CodeStatement, y un objeto será "menor" que otro si el valor del parámetro Order del atributo DesignSerializationOrder de su propiedad asociada es menor que el homólogo del otro objeto. Como veremos enseguida, el método GetOrderNumber devuelve este valor; por tanto basta devolver la resta de los valores devueltos por dicha función para ambos objetos.

Oberva sin embargo que si dos objetos tienen el mismo número de orden, no devolvemos cero, porque según MSDN el método Sort realiza una ordenación inestable; es decir, si dos elementos son iguales, no se garantiza que mantengan su orden relativo inicial. Nosotros queremos mantener este orden (para que las propiedades con mismo valor de Order se generen en orden alfabético, que es el orden predeterminado), por tanto lo que hacemos en este caso es simplemente comparar el índice de ambos elementos en la colección original codeObject.

Y vamos con el último método, GetOrderNumber, que nos devuelve el número de orden de generación de la propiedad serializada por el objeto CodeStatement pasado. El algoritmo que sigue este método es el siguiente:

Y el código que hace todo esto es el siguiente:

private int GetOrderNumber(CodeStatement statement)
{
    object[] attributes=null;

    // Comentario: devolver -1

    if(statement is CodeCommentStatement)
        return -1;

    // Comando que no es una asignación de propiedad: devolver Int32.MaxValue.

    CodeAssignStatement assignStat=statement as CodeAssignStatement;
    if(!(assignStat!=null && assignStat.Left is CodePropertyReferenceExpression))
        return int.MaxValue;

    // Propiedad: obtiene su atributo DesignSerializationOrder en attributes.
        
    CodePropertyReferenceExpression propRef=(CodePropertyReferenceExpression)assignStat.Left;
    attributes=SerializedObject.GetType().GetProperty(propRef.PropertyName)
        .GetCustomAttributes(typeof(DesignSerializationOrderAttribute),true);

    // Devolvemos el valor del atributo, si existe, o Int32.MaxValue en caso contrario.

    if(attributes.Length==0)
        return int.MaxValue;
    else
        return ((DesignSerializationOrderAttribute)attributes[0]).Order;
}

Observa que usamos el objeto que está siendo serializado y que hemos almacenado previamente en SerializedObject. Su método GetType nos devuelve el tipo del objeto, y a partir de ahí sólo es cuestión de usar un par de funciones de reflexión (GetProperty y GetCustomAttributes, que son autoexplicativas) para obtener el atributo que buscamos.

Conclusión

CodeDom es un tema ciertamente complejo y aquí sólo hemos rascado la superficie, pero como puedes ver el potencial es enorme. Y más o menos lo mismo podríamos decir sobre la reflexión y sobre la creación de atributos personalizados. Mi consejo es que bucees un poco en la documentación de MSDN (y en la información presente en ésta y otras páginas web dedicadas a .NET) para aprender un poco más sobre estos temas porque ciertamente valen la pena.

En el fichero adjunto tienes el código completo de las clases DesignSerializationOrderAttribute y OrderedCodeDomSerializer, debidamente comentado y con las instrucciones using necesarias para poder compilarlo.


ir al índice

Fichero con el código de ejemplo (konamiman_ExtendiendoCodeDomSerializer.zip - 2,54 KB)