Página principal
Artículos y trucos
Catálogo de productos
Ejemplos y descargas
Mis libros
Cursos de formación
Investigación y desarrollo
Libros recomendados
Mis páginas favoritas
Acerca del autor
 
En colaboración con Amazon
 
Intuitive Sight

Reflexión

La reflexión es uno de los pilares de .NET. Esta característica permite almacenar y obtener información en tiempo de ejecución sobre casi cualquier objeto o tipo presente en un módulo. Es gracias a esto que es posible implementar técnicas fundamentales como la recolección de basura o la serialización en distintos formatos. Y aunque es cierto que la mayoría de los entornos de programación modernos proporcionan algún tipo de RTTI (runtime time information, el pariente pobre de la reflexión), nunca antes se había visto un uso tan extenso y generalizado de este recurso como en .NET.

INFORMACION SOBRE TIPOS EN TIEMPO DE EJECUCION

No es difícil aprender a usar la reflexión; se trata de una API basada en unas pocas clases. Lo complicado es adaptarse a la idea para comprender todo el partido que se le puede sacar. Por ejemplo, una de las cosas que más detesto es tener que retocar el menú y la barra de herramientas de la ventana principal cada vez que hay que añadir un nuevo formulario a una aplicación. Sí, podemos usar herencia visual y todos los trucos que sean para minimizar la cantidad de trabajo necesario para definir la ventana, pero al final tenemos que regresar a la ventana principal para poder utilizar el formulario. Este trabajo mecánico de "encolar piezas" es bastante molesto. Sobre todo cuando sabemos que existen alternativas.

¿Cómo podemos saltarnos el paso de modificar el menú? Está claro que necesitamos que ese menú se cree automáticamente, en tiempo de ejecución, y para ello necesitamos conocer qué clases de formularios existen dentro del proyecto. Algo aparentemente tan simple como esto, es imposible de averiguar en Delphi, a no ser que registremos cada clase mediante instrucciones explícitas. Es verdad que podemos realizar esto de forma "distribuida", es decir, que el registro puede realizarse mediante instrucciones ejecutadas en el código de inicialización de cada unidad, en vez de tener que reunir todas estas instrucciones de registro en un módulo central. De hecho, esta técnica se aplica ya en WebSnap, en los servicios Web, etc.

No obstante, en .NET existe una técnica más sencilla. Cree un proyecto para Windows Forms, configure una ventana principal, y añada un segundo formulario al proyecto. En el fichero de la ventana principal añadiremos la siguiente referencia al espacio de nombres que contiene las clases y tipos necesarios para la reflexión:

using System.Reflection;

Teclee entonces las siguientes instrucciones en el evento Load del formulario principal:

foreach (System.Type type in Assembly.GetExecutingAssembly().GetTypes())
   if (type.IsSubclassOf(typeof(Form)))
      MessageBox.Show(type.FullName);

GetExecutingAssembly es un método estático de la clase Assembly que nos devuelve un puntero a un objeto de esta misma clase que representa al ensamblado que está en ejecución. A este objeto le aplicamos el método GetTypes, que nos devolverá una colección con todas las clases declaradas dentro de este ensamblado. El código mostrado recorre los elementos de esta colección y se queda sólo con aquellos que son clases derivadas por herencia de la clase Form. Para cada uno de estos tipos, muestra entonces su nombre completo, incluyendo el del espacio de nombres en que ha sido declarado, en pantalla.

ATRIBUTOS

Así de fácil es obtener los nombres y tipos de todas las clases de formularios incluidas en un proyecto, siempre que estén declaradas dentro del mismo ensamblado. No obstante, es relativamente sencillo extender las técnicas que voy a mostrar para el caso en que necesitemos usar formularios definidos en varios ensamblados.

Ahora bien, ¿qué podemos hacer conociendo solamente un tipo de clase de formulario? Necesitamos crear un comando de menú para cada una de estas clases, pero para ello se requiere más información. Por ejemplo, ¿qué texto debe ir asociado al comando de menú? Podríamos exigir que fuese el mismo texto que el de la barra de título del correspondiente formulario... pero recuerde que tenemos en la mano solamente el tipo, no una instancia del tipo.

La solución consiste en asociar información adicional a las ventanas que queremos manejar automáticamente. Y en .NET es posible realizar esa asociación mediante atributos. Un atributo es simplemente un objeto que se asocia a una declaración en el propio código fuente. Esto es lo importante:

  • El atributo va asociado a los metadatos, no a instancias de la clase.

... que es precisamente lo que necesitamos. Vamos pues a definir un nuevo tipo de atributo, declarando una clase derivada de System.Attribute:

namespace Reflex
{
   /// <summary>
   /// Atributo que marca las clases manejables por el LayoutManager
   /// </summary>
   public class LayoutAttribute: System.Attribute
   {
      private string description;
      private bool uniqueInstance = false;

      public LayoutAttribute(string description)
      {
         this.description = description;
      }

      public string Description
      {
         get { return description; }
      }

      public bool UniqueInstance
      {
         get { return uniqueInstance; }
         set { uniqueInstance = value; }
      }
   }
}

Cada instancia de esta clase puede almacenar una descripción, que será utilizada para crear el comando de menú, e información adicional sobre si una clase dada debe permitir que se creen varias instancias suyas a la vez (aunque no aprovecharemos esta información en nuestro ejemplo). La nueva clase se utiliza para asociar atributos a las ventanas que queremos que participen en el sistema, como en el siguiente ejemplo:

[Layout("Clientes", UniqueInstance=true)]
public class Clientes: System.Windows.Forms.Form
{
   // ...
}

Observe, en primer lugar, que podemos abreviar LayoutAttribute a Layout cuando declaramos un atributo. Como el constructor del atributo exige un parámetro, la descripción, tenemos que pasar ésta en primer lugar, como parámetro sin nombre o parámetro posicional. A continuación podemos pasar, como si fuesen parámetros con nombres, valores para las propiedades públicas de la clase de atributo. En este caso, indicamos explícitamos que el contenido de UniqueInstance debe ser verdadero. Podemos omitir esta propiedad, en cuyo caso el valor por omisión de UniqueInstance sería false.

CREACION MEDIANTE REFLEXION

Ahora mejoraremos nuestra técnica para aprovechar la información que podemos asociar mediante atributos. Primero crearemos una colección, de tipo Hashtable, para asociar comandos de menú con los tipos de formularios que deberán crear:

private System.Collections.Hashtable menuTable = new Hashtable();

Algunos componentes en .NET tienen una propiedad Tag, de tipo object, para que asociemos información arbitraria en ella. No es el caso de MenuItem, la clase que representa a los comandos de menú, por lo que tenemos que recurrir a un diccionario externo a las instancias.

La inicialización del menú dinámico tiene lugar durante la construcción del formulario principal:

public MainForm()
{
   InitializeComponent();
   InitLayoutWindows();
}

El nuevo método InitLayoutWindows será el que añada instancias de MenuItem dentro del submenú Ver, que hemos creado en tiempo de diseño y al cual podemos referirnos mediante el campo miView:

private void InitLayoutWindows()
{
   System.EventHandler clickHandler = new EventHandler(MenuClicked);
   foreach (System.Type type in Assembly.GetExecutingAssembly().GetTypes())
      if (type.IsSubclassOf(typeof(Form)) &&
          type.IsDefined(typeof(LayoutAttribute), false))
      {
         object[] attrs = type.GetCustomAttributes(typeof(LayoutAttribute), false);
         if (attrs.Length > 0)
         {
            MenuItem item = new MenuItem(
               ((LayoutAttribute) attrs[0]).Description, clickHandler);
            miView.MenuItems.Add(item);
            menuTable.Add(item, type);
         }
      }
}

Como puede constatar, ya hemos visto el núcleo del método: la iteración sobre los tipos contenidos en el ensamblado activo. Una de las adiciones es la comprobación de que el tipo examinado, además de heredar de la clase Form, esté "adornado" con el atributo LayoutAttribute. El segundo parámetro de IsDefined exige que el atributo haya sido aplicado directamente a la clase, no a sus ancestros:

type.IsDefined(typeof(LayoutAttribute), false)

Si el tipo examinado cumple ambas condiciones, obtenemos todas las instancias del atributo en cuestión que han sido aplicadas a la clase:

object[] attrs = type.GetCustomAttributes(typeof(LayoutAttribute), false);

No se extrañe: un atributo puede aplicarse varias veces a una misma declaración, y por lo tanto necesitamos un vector para recibir todas las potenciales instancias del mismo asociadas a una misma declaración. No obstante, para que podamos usar instancias múltiples de un atributo, tendríamos que indicarlo explícitamente al definir la clase... usando otro atributo, llamado AttributeUsage. Podemos añadir este atributo a nuestro LayoutAttribute, para dejar bien claro que no permitiremos instancias múltiples, y que el atributo sólo puede asociarse a declaraciones de clases:

[AttributeUsage(AttributeTargets.Class, AllowMultiple=false)]
public class LayoutAttribute: System.Attribute
{
   // ...
}

Naturalmente, en nuestro ejemplo sólo utilizaremos el primer objeto del vector de atributos. Necesitamos este atributo para extraer el texto que utilizaremos en el comando de menú. Observe que creamos un objeto de la clase MenuItem, le asociamos un manejador para el evento Click... y añadimos el menú junto con el tipo de clase asociado a la variable menuTable, que actúa como un diccionario. Más adelante veremos la utilidad de esta precaución.

La verdadera novedad está en el manejador del evento Click compartido por todos los comandos dinámicos:

private void MenuClicked(object sender, System.EventArgs e)
{
   System.Type type = (System.Type) menuTable[sender];
   if (type != null)
   {
      Form f = type.InvokeMember(null,
         BindingFlags.DeclaredOnly |
         BindingFlags.Public | BindingFlags.NonPublic |
         BindingFlags.Instance | BindingFlags.CreateInstance,
         null, null, null) as System.Windows.Forms.Form;
      f.Show();
   }
}

Como era de esperar, el parámetro sender, a pesar de declararse como object, apunta al comando de menú que hemos pulsado. Cuando se ejecuta el manejador del evento, buscamos en la tabla el tipo de datos asociado al menú. A continuación, recurrimos al método InvokeMember para ejecutar indirectamente el constructor sin parámetros asociado a la clase de ventana. La declaración de la versión de InvokeMember que estamos usando es la siguiente:

public object InvokeMember(
   string name,
   BindingFlags invokeAttr,
   Binder binder,
   object target,
   object[] args
);

El parámetro name debe contener el nombre del método a ejecutar, pero al tratarse de un constructor, pasamos un puntero vacío; no olvide que los constructores en C# son anónimos. A continuación, pasamos una serie de indicadores en invokeAttr. En nuestro caso, DeclaredOnly indica que el constructor debe haberse declarado en la propia clase, Public y NonPublic habilitan la búsqueda de constructores públicos, privados o protegidos. A pesar de lo que podríamos pensar, debemos incluir Instance para buscar "miembros" de instancias, y CreateInstance es el indicador que nos permite ejecutar un constructor. El siguiente parámetro del método, binder, es el más sofisticado, porque se utiliza para afinar el traspaso de algunos parámetros, pero en este ejemplo no lo necesitamos y pasamos un null en su lugar. El penúltimo parámetro, target, es el puntero al objeto que actuará como this; evidentemente, al tratarse de un constructor debemos pasar null. Y en args debemos pasar un vector con los argumentos que necesita el constructor. Como queremos ejecutar el constructor sin parámetros, podemos también pasar null.

Como InvokeMember devuelve la instancia creada como si fuese de tipo object, necesitamos convertir el valor al tipo Form. Para terminar, ejecutamos el método Show para mostrar la nueva ventana en el escritorio.

¿Extensiones? Se me ocurren muchas. Las más sencillas y necesarias:

  1. ¡Recuerde que no hemos tenido en cuenta el valor de UniqueInstance!
  2. Puede incluir en el atributo Layout alguna sugerencia sobre la ubicación del comando de creación de la ventana dentro del menú o dentro de una barra de herramientas.
  3. Hemos asumido que todas las ventanas se deben ejecutar de forma no modal, y que estamos trabajando en un entorno SDI. Podríamos añadir propiedades al atributo Layout para indicar este tipo de opciones.