jueves, 9 de julio de 2009

Introducción a System.Reflection (Parte 1)

Reflection es la habilidad que tiene un código para revisar su propia estructura, es decir, podemos revisar la metadata que está en el assembly (en el caso de .net) y manipularla a nuestro antojo. 

Con Reflection podemos encontrar cuarquier detalle sobre un objecto, assembly, propiedad, además de poder invocar métodos y obtener o establecer valores de una propiedad, todo ésto en tiempo de ejecución!

Esta habilidad nos da poder en nuestro código pero como todo,  hay que saber utilizarlo en el momento preciso y no abusar sobre el uso de Reflection ya que nuestro código podría verse afectado por pérdidas de performance.

Se preguntarán dónde y cuándo hay que utilizar Reflection, pues es algo que tienen que saber utilizar cuando lo necesiten, hay situaciones como formularios que requieren un diseño dinámico según el tipo que se pase en X parámetro, y en ese momento se preguntarán: "¿Escribo el formulario a hardcode, o utilizo reflection?", éste es uno de los casos más utilizados, también pódemos usarlo para serializar objectos de forma personalizada, etc.

¿Cómo comienzo a utilizar reflection?

Hay dos formas de comenzar: La primera es usando typeof() y la segunda es utilizando el método GetType() que todos los objectos tienen. Cualquiera de las dos formas que se usen retornan un objeto tipo Type, ese type es el que nos otorga la información de ese tipo.

Entre los principales métodos de Type tenemos:

GetField() Obtiene el campo según el nombre.

GetMethod() Obtiene un método, y si éste tiene sobre cargas, al igual que el constructor, hay que pasarle el tipo correspondiente de la sobre carga.

GetProperty() Obtiene la propiedad según el nombre.

Nota: aparte de los métodos anteriores existen los mismos en plural, es decir que en vez de obtener un miembro, obtienes un arreglo con los resultados según los parámetros que le indiques. Ejm: GetProperty() tienes:  GetProperties() el cual  puedes llamar sin parámetros para obtener un PropertyInfo[] con todas las propiedades.}

Un punto muy importante es que en todo ésto, aunque hayamos sacado toda ésta información a partir de una instancia, Reflection se basa en metadata, es decir todo lo que exploramos y manipulamos, lo hacemos sin la instancia inicial, es por eso que cuando se vaya a invocar un método, a establecer valores en campos o propiedades, etc, nos pidan la instancia a la cual queremos realizar ese trabajo.

Ahora que ya sabemos cómo explorar la clase, vamos a ver cómo se usa cada método, Para todos los ejemplos usaremos la siguiente clase:

public class Person

{

     private string m_firstName;

     private string m_lastName;

     public string FirstName

     {

          get { return m_firstName; } 

          set { m_firstName = value; }

      }

     public string LastName

     {

          get { return m_lastName; } 

          set { m_lastName = value; }

      }

      public void Run()

      {   }

}

GetField() y GetFields()

GetField nos regresa un FieldInfo y GetFields nos retorna un arreglo de FieldInfo, el cual podemos iterar.

FieldInfo tiene dos métodos muy importantes que son: GetValue y SetValue. Con GetValue obtenemos el valor que está almacenado en la instancia que pasemos como parámetro. Y SetValue establecemos a la instancia que pasamos como primer parámetro el valor que pongamos como segundo parámetro.

Ejemplo:

Person p = new Person();

Type t = p.GetType();

FieldInfo fInfo = t.GetField("m_firstName", BindingFlags.NonPublic | BindingFlags.Instance);

fInfo.SetValue(p,"Juan");

Bueno en primer lugar creamos una instancia de Person, luego obtenemos el tipo. De éste utilizamos GetField y el pasamos como parámetro el nombre del campo que queremos obtener, y también pasamos (BindingFlags.NonPublic | BindingFlags.Instance) como segundo parámetro, noten que es de tipo Flag, por lo que podemos pasar más de uno separado por el símbolo de pipe o tubería |. El BindingFlags.NonPublic es para decirle al método que el campo no es publico (obviamente). Y BindingFlags.Instance hace que el resultado sea de campos que se instancien,y no de campos estáticos. Luego de obtenerlo usamos el método SetValue al cual le pasa la instancia y el valor que se le quiere asignar al campo.


GetMethod() y GetMethods()

Al igual que GetFields, GetMethods nos retorna un arreglo pero de MethodInfo.

MethodInfo entre sus métodos podemos encontrar a GetParámeters y al Invoke, GetParámeters obtiene un arreglo con los parámetros, e Invoke que es el que va a hacer la llamada al método.

Para utilizarlo se hace de la siguiente manera:

Person p = new Person();

Type t = p.GetType();

MethodInfo mInfo = t.GetMethod("Run");

mInfo.Invoke(p, null);

Ejecutamos el GetMethod y le pasamos el nombre del método, nos retorna MethodInfo. Del MethodInfo ejecutamos el Invoke y le pasamos la instancia y como el método Run() no tiene parámetros le pasamos null, en caso contrario sería simplemente pasar un arreglo de object con los parámetros en el orden que aparecen en el método.

Nota: Invoke retorna object, así que si el método que están llamando tiene algún valor de retorno, solo sería cuestion de hacerle un cast: ejm:  

string nombre = (string) mInfo.Invoke(p, null);

GetProperty() y GetProperties()

Por último y no menos importante (de hecho es el que más he usado), están los GetProperty() que retorna un PropertyInfo y GetProperties() que retorna PropertyInfo[].

Los PropertyInfo funcionan parecido a los FieldInfo, tenemos GetValue y SetValue. Ahora un ejemplo:

Person p = new Person();

Type t = p.GetType();

PropertyInfo pInfo = t.GetProperty("FirstName");

string name = (string) pInfo.GetValue(p, new object[] {} );


Como siempre gracias por leer! y esperen la segunda parte!

16 comentarios:

  1. De donde sale fInfo y el metodo GetValue tiene 2 sobrecargas como minimo

    Person p = new Person();

    Type t = p.GetType();

    PropertyInfo pInfo = t.GetProperty("FirstName");

    string name = (string) fInfo.GetValue(p);

    ResponderEliminar
  2. Tienes Razón, ahora mismo lo corrijo, gracias por la observación.

    fInfo es pInfo, es un error de tipeo.
    Y GetValue si necesita dos parametros, el primero es el objeto que se vas a utilizar la propiedad y el segundo serian los parametros en caso de que los necesite, puedes pasar un array vacio de objects.

    Ahora mismo arreglo eso!

    Gracias!

    ResponderEliminar
  3. hola, muy bueno el post, me gustaria saber como recorrer hacer esto cuando se tiene propiedades de tipo list de otra clase.

    para luego crear un datatables a partir de esta propiedades

    ResponderEliminar
  4. Hola Veronica,

    Pues tendrías que hacer lo mismo que si quisieras acceder una propiedad. Imagina que reflection es como una masa y tu la mueves con las manos hasta que tenga la forma que quieres.

    Hay dos formas de hacer esto, te muestro la más sencilla:

    Veamos un ejemplo sencillo (sin VS abierto que tengo sueño) :

    // casa tiene un List
    var c = new Casa();

    Type ctype = c.GetType();

    var listProp = ctype.GetProperty("List", BindingFlags.Public);
    IEnumerable listObj= (IEnumerable) listProperty.GetValue(c, null);

    //con listObj como una instacia de un IEnumerable la iteramos
    foreach (var item in listObj)



    La segunda forma es emulando el foreach llamando a los métodos del Enumerator y IEnumerable pero igual tendrías que hacer un while o algo, asi que no vale la pena x)

    Un saludo. Buena pregunta la que has hecho!

    ResponderEliminar
  5. Hola, gracias por la ayuda ya probe y funciona bien, de la forma en la que lo explicas.

    De nuevo gracias, y buenisimo tu post.

    ResponderEliminar
  6. mi pregunta es como puedo obtener el nombre de una variable contenida en una clase, lo que necesito obtener el nombre de la varia y el contenido de ella.

    gracias !!!

    ResponderEliminar
  7. Milton, depende del scope/contexto en donde se encuentre esa variable.
    Si está dentro de una clase pues simplemente es un campo/field.

    Si está dentro de un método pues no se puede, tendrás que exponer esa variable como propiedad o aumentar su scope a field..

    Un saludo.

    ResponderEliminar
  8. Hola Juan como estas ? Mira tengo una problematica, entiendo lo que publicas pero quiero ir un paso mas atras, a ver si me podes ayudar.

    Estoy trabajando en C#

    Tengo un UserControl llamado Id_1001, Id_1002, Id_1003

    Segun la opcion de menu que el usuario selecciona, yo debo hacer un.

    y necesito instanciar cada User Control, pero yo tengo el nombre en una variable string, x que lo levando de un XML el menu.

    Si el usuario, hace clic en la opcion 1001, deberia instanciar.

    Id_1001 uc = new Id_1001();

    Si el usuario, hace clic en la opcion 1002, deberia instanciar.

    Id_1002 uc = new Id_1002();

    Pero Id_1001 y Id_1002 esta en una variable de texto.

    O sea que tengo un problema un poco mas arriba, como instanciar una User Control en forma dinamica.

    Tenes una idea como podria ??

    Esta muy bueno tu Blog, es muy explicativo.

    Saludos
    Eduardo




    ResponderEliminar
  9. Hola Eduardo,

    Si quieres hacerlo 100% bien haz lo siguiente:

    1) Implementa una interfaz común entre todos tus controles de usuario, id_1001, id_1002.. todos deben implementar una interfaz que te permita acceder a miembros comunes.

    2) Luego puedes usar:
    var controlType = Type.GetType(NombreTipo);
    var ControlInstance = Activator.CreateInstance(controlType) as IStyleSheet ;
    if (ControlInstance != null)
    {
    // continua codigo
    }

    Toma en cuenta que la variable NombreTipo es un string que debe tener el nombre del tipo (id_1001, id_1002...) y además debe tener el namespace correspondiente.

    Si el id_1001 está dentro un archivo id_1001.ascx.cs mira dentro donde pone
    namespace X.X.X.X
    {
    public class id_1001 : UserControl
    {

    }
    }

    El string debe ser "X.X.X.X.id_1001" , no solo el "id_1001".

    Si no estás seguro de como es esta parte. Puedes instancear el control en cualquier parte del codigo, solo por probar.

    var ctrl = new id_1001();

    Y cuando pongas el cursor del mouse sobre id_1001 sale un tooltip que debe decir algo asi: class Tu.Name.Space.id_1001

    Recuerda lo de la interface, es importante para que puedas acceder a miembros de varias clases sin saber de que clase es.


    Saludos y dime si te funciona.

    ResponderEliminar
  10. Hola me gustaria si es posible manejar la asosiacion y composicion de objetos con reflection en C#, te agradezco mucho tu respuesta ya que llevo bastante tiempo intentando hacer eso pero hasta ahora nada...

    ResponderEliminar
    Respuestas
    1. Si claro. Creando la instancia y luego llamando Invoke al método o a la propiedad correspondiente a la asociación/composición.
      Es exactamente el ejemplo que tengo arriba, pero en vez de usar un string, puedes utilizar cualquier objeto.

      Saludos

      Eliminar
  11. Este comentario ha sido eliminado por el autor.

    ResponderEliminar
  12. hola, antes que nada te agradezco por la rapidez de tu respuesta, pero te paso el metodo que utilizo para obtener las propiedades de un objeto, en el cual tiene como atributo otro objeto y quiero generar una query para utilizarla con sql. tanto para insertar como para obtener datos del motor.
    public override int Insertar(Object o)
    { string sql = "";
    string spa = "";
    string p = "(";
    string v = "(";
    int resp = 0;
    try
    {
    foreach (PropertyInfo propiedad in o.GetType().GetProperties()) {

    if (propiedad.Name != "ID")
    {
    spa = VerificaTipoDato(propiedad.PropertyType.ToString());
    p = p + propiedad.Name + ",";
    v = v + spa + propiedad.GetValue(o, null) + spa + ",";
    }
    }
    p = RightQUERY(p, p.Length);
    v = RightQUERY(v,v.Length);
    sql = "insert into " + o.GetType().Name + p + " Values " + v;
    /*---------Ejecucion de otro metodo-------*/
    resp = this.EjecutarQuery(sql);
    }
    catch (SqlException ex)
    {
    throw new Exception("Error al intentar insertar..." + ex.Errors.ToString());
    //throw (ex);
    }
    return resp;
    }

    ResponderEliminar
  13. Espero se entienda el problema, porque en el for each obtengo las propiedades, pero cuando una de ellas es otra clase que tiene como atributo ejemplo (clase persona que tiene otra clase domicilio), entonces no puedo obtener los atributos de domicilio. Ojalá se pueda hacer

    ResponderEliminar
  14. hola tengo un problema con reflexion y c#
    espero que me puedan dar la mano.
    he creado este metodo el cual lee el objeto(form) y a algunos form les he agregado la propiedad oUsuario.
    y lo que hace el metodo es si tiene la propiedad oUsuario entonces se le asigna el valor.
    el detalle es que me arroja error en la linea formulario.oUsuario = MiObjetoUsuario;
    formulario.oUsuario <== donde no reconoce la propiedad oUsuario.
    el mensaje de error que muestra es
    Error 3 'object' no contiene una definición de 'oUsuario' ni se encontró ningún método de extensión 'oUsuario' que acepte un primer argumento de tipo 'object' (¿falta una directiva de uso o una referencia de ensamblado?) D:\Desarrollo\SysAtlas451\SysAtlasConsorcio\mdlProcedimientos.cs 170 44 SysAtlasConsorcio

    espero que me puedan dar una mano.

    public void llamarform(object formulario )
    {
    PropertyInfo[] propiedades = formulario.GetType().GetProperties();
    foreach (PropertyInfo propiedad in propiedades)
    {
    if (propiedad.Name == "oUsuario" )
    {
    formulario.oUsuario = MiObjetoUsuario;
    }
    }

    }

    ResponderEliminar
    Respuestas
    1. Hola,
      Claro, porque no puedes asignarlo así. Tienes que hacer unboxing o usar reflection.
      Con reflection:
      propiedad.SetValue( formulario , MiObjetoUsuario);

      Saludos

      Eliminar