Reflection


Donde nos ponemos a investigar un poco que tienen nuestros objetos.

Donde nos ponemos a investigar un poco que tienen nuestros objetos.

Motivación.

Para muchas de las cosas que venimos explicando, acerca de generalizaciones de código, métodos compartidos y demás, cuando programemos, seguramente querremos interactuar con dichas generalizaciones, pero de un modo más específico.

Por ejemplo, en la anterior publicación, hablábamos de Atributos.

¿Cómo hago para saber si una propiedad tiene determinado atributo?

¿Cómo obtengo los miembros que tienen cierto atributo?

El espacio de nombres System.Reflection.

El .Net Framework implementa este espacio de nombres específico, para poder investigar los objetos.

Con él, a partir del Type de un objeto, se puede obtener información de sus miembros, sus atributos, su jerarquía de herencia etc.

El espacio expone objetos que sirven como descriptores de las características de un tipo, como, por Ejemplo:

  • Assembly
  • MemberInfo
  • PropertyInfo

Dichos objetos se obtienen, como fue dicho, del Type de un objeto o instancia utilizando el método GetType()

Obtener el tipo

Existen distintos métodos para obtener el tipo, dependiendo si se tiene o no una instancia del mismos.

Los métodos, todos retornando un objeto Type, se describen en el siguiente cuadro

    

C#

VB

Con Instancia

variable.GetType()

variable.GetType

Sin Instancia

typeof(Nombre_De_Clase)

GetType(Nombre_de_Clase)

Funciones.

En muchos casos, utilizaremos LinQ sobre objetos para obtener información (lo cual facilita la codificación y acelera el proceso).

Comprobar Propiedades.

Comprobar si tiene un atributo


private static bool HasAttribute(PropertyInfo p, string attributeName)
{
   var attrs = p.GetCustomAttributesData();
    if (attrs.Count > 0)
    {
        var attrs2 = (from aa in attrs where aa.AttributeType.Name == attributeName select aa).ToList();
        return attrs2.Count > 0;
    }
    return false;
}


Private Shared Function HasAttribute(p As PropertyInfo, attributeFullName As StringAs Boolean
    Dim attrs = p.GetCustomAttributesData
    If attrs.Count > 0 Then
        Dim attrs2 = (From aa In attrs Where aa.AttributeType.Name = attributeFullName).ToList
        Return attrs2.Count > 0
    End If
    Return False
End Function

Como se ve, se aplica sobre un objeto PropertyInfo y filtra directamente por nombre.

Obtener los atributos de una propiedad


public static NameValueList GetAttributes(PropertyInfo property)
{
    NameValueList result = new NameValueList();
    var v = property.GetCustomAttributesData();
    foreach (var a_loopVariable in v)
    {
        var a = a_loopVariable;
        foreach (var named_loopVariable in a.NamedArguments)
        {
            var named = named_loopVariable;
            NameValue<object> nv = new NameValue<object>(named.MemberName) { RawValue = named.TypedValue.Value };
            result.Add(nv);
        }
    }
    return result;
}


Public Shared Function GetAttributes(ByVal [property] As PropertyInfoAs DSCommon.NameValueList
    Dim result As New DSCommon.NameValueList
    Dim v = [property].GetCustomAttributesData()
    For Each a In v
        For Each named In a.NamedArguments
            Dim nv As New DSCommon.NameValue(Of Object)(named.MemberName) With {.RawValue = named.TypedValue.Value}
            result.Add(nv)
        Next
    Next
    Return result
End Function

En este caso, la función retorna todos los atributos asignados a un PropertyInfo (o sea, a una propiedad), y retorna nuestro NameValueList con los valores obtenidos.

Obtener miembros específicos por atributo de un tipo

Obtener las propiedades que poseen un determinado atributo.


public static List<PropertyInfo> GetPropertiesByAttribute(Type type, Attribute attribute)
{
    string s = attribute.GetType().ToString();
    var v = (from p in type.GetProperties() where (from aa in p.GetCustomAttributes(truewhere aa.ToString() == s select aa).Count() > 0 select p);
    List<PropertyInfo> l = new List<PropertyInfo>();
    l.AddRange(v);
    return l;
}


Public Shared Function GetPropertiesByAttribute(ByVal type As TypeByVal attribute As AttributeAs List(Of PropertyInfo)    Dim s As String = attribute.GetType.ToString
    Dim v = (From p As PropertyInfo In type.GetProperties
             Where (From aa In p.GetCustomAttributes(TrueWhere aa.ToString = s).Count > 0)
    Dim l As New List(Of PropertyInfo)
    l.AddRange(v)
    Return l
End Function

En este caso, la función se aplica sobre un tipo, del cual se obtienen las propiedades. De ellas, aquellas que tengan asignado un determinado atributo.

Obtener miembros específicos por nombre del atributo de un tipo


public static List<PropertyInfo> GetPropertiesByAttribute(
Type type, string attributeFullName)
    var v = (from p in type.GetProperties() where (HasAttribute(p, attributeFullName)) select p);
 
    List<PropertyInfo> l = new List<PropertyInfo>();    l.AddRange(v);
    return l;
}


Public Shared Function GetPropertiesByAttribute(ByVal type As TypeByVal attributeFullName As StringAs List(Of PropertyInfo)
    Dim v = (From p As PropertyInfo In type.GetProperties
            Where (HasAttribute(p, attributeFullName)))
    Dim l As New List(Of PropertyInfo)
    l.AddRange(v)
    Return l
End Function

En este caso, en lugar de utilizar el atributo propiamente dicho, se utiliza su nombre.

Comprobar clases.

Obtener los nombres de las clases que heredan de otra (o implementan una interfaz)


public static string[] GetClasesOfType<T>(System.Reflection.Assembly assembly)
{
    Type theI = typeof(T);
    var elems = (from el in assembly.GetTypes() where theI.IsAssignableFrom(el) select el.Name).ToArray();
    return elems;
}


Public Shared Function GetClasesOfType(Of T)(assembly As System.Reflection.AssemblyAs String()
    Dim theI As Type = GetType(T)
    Dim elems = (From el In assembly.GetTypes() Where theI.IsAssignableFrom(el) Select el.Name).ToArray()
    Return elems
End Function

Terminamos teniendo una nueva herramienta

Agregaremos entonces una clase a nuestra biblioteca de herramientas (Tools), que podríamos llamar, precisamente, Reflection, para exponer estos métodos.

Nótese que todos están declarados como static (Shared en VB), para poder utilizarlas sin necesitar crear una instancia de la clase.

En la próxima entrega, repasaremos el espacio de nombre Tools y lo complementaremos con más cositas

Atributos


Existen en .Net clases especiales que agregan indicadores funcionales a distintas partes del código, como Clases, métodos, propiedades, etc.

La intención de los mismos es permitir indicar características especiales a dichos elementos, como por ejemplo, en el caso de propiedades de controles, si son editables en tiempo de diseño, si se permite al usuario modificarlas, entre otras.

Otro ejemplo es la indicación de funcionalidades específicas, como por ejemplo, en un enumerador, cuando éste debe comportarse como “Mapa de bits” (atributo Flags).

Una característica interesante del propio .Net Framework es que es posible definir Atributos personalizados.

Se trata sencillamente de crear una clase que herede de System.Attribute

Los estándares de nomenclatura de .net Framework indican que el nombre de la clase debe terminar con Attribute. (Llamativamente, durante su utilización, esa parte del nombre no es necesaria). Por Ejemplo:

DataFieldAttribute

Por lo demás,al igual que cualquier otra clase, pueden tener propiedades, métodos, constructores, etc. aunque normalmente, no se implementa código ejecutable, si es posible hacer uso de los mismos con propiedades que definan características específicas de aquello a lo cual queremos aplicarle el atributo.

En el siguiente ejemplo, se define un atributo específico para propiedades, que indica que la misma se corresponde con un campo específico de una tabla de base de datos.Inclusive, en forma opcional, se podrá indicar que valor predeterminado utiliza (usando un enumerador).

Veamos el código,y luego analizaremos algunos detalles.

VB

Imports System.Runtime.CompilerServices
 
 
<AttributeUsage(AttributeTargets.[Property] Or AttributeTargets.Field, Inherited:=False, AllowMultiple:=False)> _
Public Class DataFieldAttribute
    Inherits Attribute
    Public Property ColumnName() As String
        Get
            Return m_ColumnName
        
End Get         Set(value As String)
             m_ColumnName
= value
        
End Set     End Property     Private m_ColumnName As String     Public Property DefaultValue() As DefaultValueEnum         Get             Return m_DefaultValue
        
End Get         Set(value As DefaultValueEnum)
             m_DefaultValue
= value
        
End Set     End Property     Private m_DefaultValue As DefaultValueEnum     Public Sub New(<CallerMemberName> Optional columnName As [String] = "", Optional defaultValue As DefaultValueEnum = DefaultValueEnum.None)
        
Me.ColumnName = columnName
        
Me.DefaultValue = defaultValue
    
End Sub End Class Public Enum DefaultValueEnum     Now
    AutoIncrement
    None
End Enum

C#

using System;
using System.Runtime.CompilerServices;
 
namespace DS.Common
{
     [
AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, Inherited = false, AllowMultiple = false)]     public class DataFieldAttribute : Attribute     {
        
public string ColumnName
         {
            
get { return m_ColumnName; }
            
set { m_ColumnName = value; }
         }
        
private string m_ColumnName;
        
public DefaultValueEnum DefaultValue
         {
            
get { return m_DefaultValue; }
            
set { m_DefaultValue = value; }
         }
        
private DefaultValueEnum m_DefaultValue;
        
public DataFieldAttribute([CallerMemberName()] String columnName = "", DefaultValueEnum defaultValue = DefaultValueEnum.None)
         {
            
this.ColumnName = columnName;
            
this.DefaultValue = defaultValue;
         }
     }
    
public enum DefaultValueEnum     {
         Now,
         AutoIncrement,
         None
     } }

 

Puntos importantes.

  • La clase hereda efectivamente de Attribute. A su vez, tiene asignado un atributo indicando que puede aplicarse a Propiedades y/o Campos.  Además, que sólo puede aplicarse una vez en cada propiedad o campo.
  • Posee propiedades ColumnName y DefaultValue
  • DefaultValue es de un tipo también definido en el código, el enumerador DefaultValueEnum
  • Finalmente,el constructor admite dos argumentos, columnName y defaultValue, respectivamente, para asignar a las propiedades correspondientes.
  • Ambos parámetros son opcionales.  Para el valor predeterminado se asigna None, y para el nombre de la columna, una cadena vacía.
  • Sin embargo, en caso deno indicarse un valor cuando se instancia el atributo, el mismo usa una funcionalidad del compilador, el atributo CallerMemberName, que permite asignar el valor del elemento que lo llama (en este caso, el nombre de la propiedad o el campo), en lugar de la cadena vacía, con lo cual, en ningún caso tendría un valor vacío.

Espero pronto poder, finalmente, combinar distintos elementos que vimos, para crear algo más funcional.

Nos vemos pronto.