Servicios polimórficos en WCF

El polimorfismo es una característica de la programación orientada a objetos que nos hace la vida más fácil. Se basa en el concepto de herencia, el agrupamiento inteligente para reducir el código y el mantenimiento del mismo, además de las implicaciones semánticas que puede acarrear el uso de la misma.

En Windows Communication Foundation podemos crear servicios polimórficos, es decir, podemos aprovechar esta característica de la POO. Sin embargo, debemos informar al servicio de que queremos hacerlo, y darle pistas para poder serializar y deserializar los objetos correctamente. Para ello existe el concepto de KnownTypes. Mediante los KnownTypes le informamos al generador de WSDL de WCF de los tipos que están relacionados con una clase base.

Veamos cómo se comportaría el servicio sin los KnownTypes:

Creamos un servicio de WCF en el que queremos poder manipular personas y empleador. Para ello necesitamos una dirección (Address), un binding (¿cómo se traduce esto?) y un contrato (Contract) (ABC). La dirección a utilizar será http://localhost:8080/PolyService

El contrato lo podemos ver a continuación:

   1:  namespace Geeks.PolyService
   2:  {
   3:      [ServiceContract()]
   4:      interface IPolyService
   5:      {
   6:          [OperationContract]
   7:          bool IsBirthDay(Person person);
   8:          [OperationContract]
   9:          int GetAge(Person person);
  10:      }
  11:  }

Como podemos ver, en el contrato utilizamos un objeto llamado Person. Vamos a ver el contenido de esta clase, y vamos a definir una clase que herede de Person, llamada Employee.

A continuación podemos ver ambas clases:

   1:  using System;
   2:  using System.Collections.Generic;
   3:  using System.Text;
   4:  using System.Runtime.Serialization;
   5:   
   6:  namespace Geeks.PolyService
   7:  {
   8:      [DataContract]
   9:      public class Person
  10:      {
  11:          private DateTime birthDate;
  12:   
  13:          [DataMember]
  14:          public DateTime BirthDate
  15:          {
  16:              get { return birthDate; }
  17:              set { birthDate = value; }
  18:          }
  19:      }
  20:   
  21:      [DataContract]
  22:      public class Employee : Person
  23:      {
  24:          private float income;
  25:   
  26:          [DataMember]
  27:          public float Income
  28:          {
  29:              get { return income; }
  30:              set { income = value; }
  31:          }
  32:      
  33:      }
  34:  }

Y por último la clase que implementa el servicio:

   1:  using System;
   2:  using System.Collections.Generic;
   3:  using System.Text;
   4:  using System.ServiceModel;
   5:  using System.Runtime.Serialization;
   6:   
   7:  namespace Geeks.PolyService
   8:  {
   9:   
  10:      public class PolyService : IPolyService
  11:      {
  12:   
  13:          #region IPolyService Members
  14:   
  15:          public bool IsBirthDay(Person person)
  16:          {
  17:              DateTime currentDate = DateTime.Now;
  18:   
  19:              return ((person.BirthDate.Month == currentDate.Month) && (person.BirthDate.Day == currentDate.Day));
  20:          }
  21:   
  22:          public int GetAge(Person person)
  23:          {
  24:              DateTime currentDate = DateTime.Now;
  25:              return currentDate.Year - person.BirthDate.Year - 
  26:                     (person.BirthDate.Month > currentDate.Month ? 1
  27:                     : (person.BirthDate.Month != currentDate.Month) ? 0
  28:                     : (person.BirthDate.Day > currentDate.Day) ? 1 : 0);
  29:   
  30:          }
  31:   
  32:          #endregion
  33:      }
  34:   
  35:  }

Una vez tenemos la clase que implementa el servicio, el contrato, etc. debemos implementar un host que arranque el servicio, o bien usando IIS, generar un servicio IIS que haga referencia a nuestro componente. En el código de ejemplo está implementado un host. Tomemos la vía que tomemos para alojar nuestro servicio, hemos de configurarlo apropiadamente. Veamos qué pasaría si lo configuramos sin informarle de los KnownTypes:

   1:  <configuration>
   2:      <system.serviceModel>
   3:          <diagnostics>
   4:              <messageLogging logEntireMessage="true" logMalformedMessages="true"
   5:               logMessagesAtServiceLevel="true" logMessagesAtTransportLevel="false" />
   6:          </diagnostics>
   7:          <services>
   8:              <service name="Geeks.PolyService.PolyService" behaviorConfiguration="metadataBehavior">
   9:                  <endpoint address="mex" binding="mexHttpBinding" name="metadataEndpoint"
  10:                       contract="Geeks.PolyService.IPolyService" />
  11:                  <endpoint contract="Geeks.PolyService.IPolyService" binding="wsHttpBinding"/>
  12:              </service>
  13:          </services>
  14:          <!-- Enable service metadata to be populated. -->
  15:          <behaviors>
  16:              <serviceBehaviors>
  17:                  <behavior name="metadataBehavior">
  18:                      <serviceMetadata httpGetEnabled="true" />
  19:                      <serviceDebug includeExceptionDetailInFaults="true"/>
  20:                  </behavior>
  21:              </serviceBehaviors>
  22:          </behaviors>
  23:      </system.serviceModel>
  24:  </configuration>

Arrancamos nuestro servicio, y desde Visual Studio 2005 añadimos una referencia a servicio…

Y ahora supongamos que queremos usar nuestra clase Employee. Como la clase Employee hereda de Person, en principio deberíamos poder utilizarla. Sin embargo, utilizando el intellisense, vemos que el proxy que nos ha generado es el siguiente:

Hmmm… La clase Employee no aparece por ninguna parte…

Si lo pensamos con más detenimiento, podemos intentar averiguar cómo se genera el WSDL. Sería lógico que WCF se recorriese los contratos que se han definido, y que WCF examine los parámetros de las operaciones que contiene dicho contrato. Como todas las operaciones utilizan la clase Person, genera el WSDL necesario para dicha clase. Tiene sentido, verdad? Sin embargo, Employee hereda de Person, y no genera el proxy para esa clase. Hemos de indicarle a WCF que en cliente también vamos a utilizar dicha clase, y para ello se utilizan los KnownTypes. Tenemos dos opciones para ello,

  • Etiquetar la clase base con un atributo KnownType, indicándole qué clases heredan de ella. Esta solución, a parte de no ser muy flexible, para mí no tiene demasiado sentido, ya que la relación entre las clases ya la estoy indicando mediante la herencia…
  • Informar de los tipos mediante ficheros de configuración. Esta opción es más flexible, ya que simplemente cambiando el fichero de configuración podemos controlar qué es visible y qué no desde el cliente.

El fichero de configuración resultante sería el siguiente:

   1:  <configuration>
   2:      <system.serviceModel>
   3:          <diagnostics>
   4:              <messageLogging logEntireMessage="true" logMalformedMessages="true"
   5:               logMessagesAtServiceLevel="true" logMessagesAtTransportLevel="false" />
   6:          </diagnostics>
   7:          <services>
   8:              <service name="Geeks.PolyService.PolyService" behaviorConfiguration="metadataBehavior">
   9:                  <endpoint address="mex" binding="mexHttpBinding" name="metadataEndpoint"
  10:                       contract="Geeks.PolyService.IPolyService" />
  11:                  <endpoint contract="Geeks.PolyService.IPolyService" binding="wsHttpBinding"/>
  12:              </service>
  13:          </services>
  14:          <!-- Enable service metadata to be populated. -->
  15:          <behaviors>
  16:              <serviceBehaviors>
  17:                  <behavior name="metadataBehavior">
  18:                      <serviceMetadata httpGetEnabled="true" />
  19:                      <serviceDebug includeExceptionDetailInFaults="true"/>
  20:                  </behavior>
  21:              </serviceBehaviors>
  22:          </behaviors>
  23:      </system.serviceModel>
  24:   
  25:      <!-- Add KnownTypes here -->
  26:      <system.runtime.serialization>
  27:          <dataContractSerializer>
  28:              <declaredTypes>
  29:                  <add type="Geeks.PolyService.Person, Geeks.PolyService">
  30:                      <knownType type="Geeks.PolyService.Employee,Geeks.PolyService" />
  31:                  </add>
  32:              </declaredTypes>
  33:          </dataContractSerializer>
  34:      </system.runtime.serialization>
  35:      
  36:  </configuration>

Si ahora eliminamos la referencia al servicio, y la volvemos a añadir (no sé exactamente por qué, pero refrescar en este caso no es suficiente…), veremos que ya podemos utilizar la clase Employee desde cliente, y además funciona correctamente.

Bueno, pues nada más por hoy… Un saludo!

El código de este artículo ha sido formateado con c# code format, y está disponible como adjunto.

DataBinding: Accediendo a propiedades de un objeto anidado desde una columna de DataGridView

Hola a todos,

En este primer artículo vamos a ver una posible solución a una situación que se puede presentar cuando utilizamos databinding a objetos, introducido en .NET 2.0.

Una de las capacidades que más me atrajo de .NET 2.0 es la posibilidad de crear fuentes de datos a partir de Web Services y de objetos. Las oportunidades que nos ofrece esta característica son más que interesantes, ya que nos evita (entre otras cosas) tener que crear código que se encargue de rellenar controles a partir de un objeto y obtener dichos valores para rellenar la instancia del objeto que queremos manipular. Este código, además de ser bastante tedioso, suele ser una fuente de errores más que considerable.

Una de las primeras pruebas que realicé fue crear un objeto, una lista de objetos, y crear una fuente de datos a partir de esa lista, para luego mostrar la lista de objetos en un DataGridView. Funcionó a la primera (con la beta 2 de Visual Studio 2005), y los ojos empezaron a hacerme chiribitas 😉 El siguiente paso, lógicamente, fue crear una vista Maestro-Detalle. Sin problemas, metemos una lista de objetos anidada en el objeto base, y creamos dos fuentes de datos. El grid de detalle se actualizaba el sólo al cambiar el registro seleccionado en la vista maestro. Muy buena pinta, pensé.

Sin embargo, a la hora de diseñar clases, existen situaciones en las que tenemos clases agregadas dentro de la clase actual, que no necesariamente tienen que representar una relación maestro-detalle. Por ejemplo, podemos definir una clase Persona, y una clase Direccion. Podríamos pensar que es lógico que la clase Persona contenga una dirección (lo lógico realmente es que pueda tener varias direcciones, pero para el ejemplo que trato de ilustrar nos vale).

Tomemos como ejemplo este código:

Por un lado tenemos la clase Address, que almacenará la dirección:

   1:  using System;
   2:  using System.Collections.Generic;
   3:  using System.Text;
   4:  using System.ComponentModel;
   5:   
   6:  namespace Geeks.DataObjects
   7:  {
   8:      public class Address : INotifyPropertyChanged
   9:      {
  10:          private string street;
  11:   
  12:          public string Street
  13:          {
  14:              get { return street; }
  15:              set
  16:              {
  17:                  if (!String.Equals(street, value))
  18:                  {
  19:                      street = value;
  20:                      OnPropertyChanged(new PropertyChangedEventArgs("Street"));
  21:                  }
  22:              }
  23:          }
  24:   
  25:          private string floor;
  26:   
  27:          public string Floor
  28:          {
  29:              get { return floor; }
  30:              set
  31:              {
  32:                  if (!String.Equals(floor, value))
  33:                  {
  34:                      floor = value;
  35:                      OnPropertyChanged(new PropertyChangedEventArgs("Floor"));
  36:                  }
  37:              }
  38:          }
  39:   
  40:          private string city;
  41:   
  42:          public string City
  43:          {
  44:              get { return city; }
  45:              set
  46:              {
  47:                  if (!String.Equals(city, value))
  48:                  {
  49:                      city = value;
  50:                      OnPropertyChanged(new PropertyChangedEventArgs("City"));
  51:                  }
  52:              }
  53:          }
  54:   
  55:          private string state;
  56:   
  57:          public string State
  58:          {
  59:              get { return state; }
  60:              set
  61:              {
  62:                  if (!String.Equals(state, value))
  63:                  {
  64:                      state = value;
  65:                      OnPropertyChanged(new PropertyChangedEventArgs("State"));
  66:                  }
  67:              }
  68:          }
  69:   
  70:          #region INotifyPropertyChanged Members
  71:   
  72:          protected virtual void OnPropertyChanged(PropertyChangedEventArgs args)
  73:          {
  74:              if (PropertyChanged != null)
  75:              {
  76:                  PropertyChanged(this, args);
  77:              }
  78:          }
  79:   
  80:          public event PropertyChangedEventHandler PropertyChanged;
  81:   
  82:          #endregion
  83:      }
  84:  }

Por otro lado, tenemos la clase Person, que almacenará datos de la persona. En este caso, sencillo, sólo almacena el nombre, el primer apellido y una dirección.

   1:  using System;
   2:  using System.Collections.Generic;
   3:  using System.Text;
   4:  using System.ComponentModel;
   5:   
   6:  namespace Geeks.DataObjects
   7:  {
   8:      public class Person : INotifyPropertyChanged
   9:      {
  10:          public Person()
  11:          {
  12:              address = new Address();
  13:              address.PropertyChanged += new PropertyChangedEventHandler(address_PropertyChanged);
  14:          }
  15:   
  16:          Address address;
  17:   
  18:          public Address Address
  19:          {
  20:              get { return address; }
  21:              set
  22:              {
  23:                  if (!Address.Equals(value, address))
  24:                  {
  25:                      address = value;
  26:                      address.PropertyChanged += new PropertyChangedEventHandler(address_PropertyChanged);
  27:                      OnPropertyChanged(new PropertyChangedEventArgs("Address"));
  28:                  }
  29:              }
  30:          }
  31:   
  32:          private string name;
  33:   
  34:          public string Name
  35:          {
  36:              get { return name; }
  37:              set
  38:              {
  39:                  if (!String.Equals(name, value))
  40:                  {
  41:                      name = value;
  42:                      OnPropertyChanged(new PropertyChangedEventArgs("Name"));
  43:                  }
  44:              }
  45:          }
  46:   
  47:          private string surname;
  48:   
  49:          public string Surname
  50:          {
  51:              get { return surname; }
  52:              set
  53:              {
  54:                  if (!String.Equals(surname, value))
  55:                  {
  56:                      surname = value;
  57:                      OnPropertyChanged(new PropertyChangedEventArgs("Surname"));
  58:                  }
  59:              }
  60:          }
  61:   
  62:          public override string ToString()
  63:          {
  64:              return string.Format("{0} {1}: {2}", name, surname, (address == null)? "<null>" : address.ToString());
  65:          }
  66:   
  67:          void address_PropertyChanged(object sender, PropertyChangedEventArgs e)
  68:          {
  69:              OnPropertyChanged(new PropertyChangedEventArgs("Address"));
  70:          }
  71:   
  72:          #region INotifyPropertyChanged Members
  73:   
  74:          public event PropertyChangedEventHandler PropertyChanged;
  75:   
  76:          protected virtual void OnPropertyChanged(PropertyChangedEventArgs args)
  77:          {
  78:              if(PropertyChanged!=null)
  79:              {
  80:                  PropertyChanged(this, args);
  81:              }
  82:          }
  83:   
  84:          #endregion
  85:      }
  86:  }

Por último, la clase PersonList, que no es más que una clase que hereda de BindingList<Person>, para tener ya toda la funcionalidad de notificación de listas. Así conseguiremos que las modificaciones realizadas a través del DataGridView, o a través de código, se lleven a cabo de manera inmediata.

   1:  using System;
   2:  using System.Collections.Generic;
   3:  using System.Text;
   4:  using System.ComponentModel;
   5:   
   6:  namespace Geeks.DataObjects
   7:  {
   8:      public class PersonList : BindingList<Person>
   9:      {
  10:      }
  11:  }

Si creamos una BindingSource en Visual Studio .NET 2005 a partir de la clase PersonList, veremos en la pestaña DataSources algo similar a lo siguiente:

Como podemos ver en la imagen, la propiedad Address de la clase Person es un objeto agregado (de ahí que nos muestre un símbolo + para poder expandir ese nodo en el árbol). Si creásemos un DataGridView a partir de esa fuente de datos (creamos un DataGridView y le asignamos como fuente de datos un objeto BindingSource creado a partir de PersonList – arrastrando PersonList en la pestaña Data Sources a la zona de componentes del formulario), veríamos tres columnas en el DataGridView:

Obviamente esto no es lo que deseamos, ya que el objeto Address no es editable, y además no podemos ver los contenidos de sus campos. En WinForms a día de hoy no existe ningún tipo de columna que nos permita navegar por el objeto y obtener una propiedad del objeto anidado, para posteriormente poder editarlo. Para ello, nos crearemos un tipo de columna de DataGridView propio. Para evitarnos el tener que implementar bastante funcionalidad, vamos a crear una columna que herede de DataGridViewTextBoxColumn.

Según MSDN, para crear una columna propia a partir de un tipo de columna existente, debemos realizar los siguientes pasos:

  • Crear una clase columna que herede de la columna que queremos personalizar.
  • Crear una clase celda que herede de la celda que queremos usar como base para nuestra columna.
  • Introducir si es necesario nuevas propiedades tanto para la columna como para la celda. Si queremos propiedades nuevas para la celda, hemos de añadirlas también en la columna, ya que las celdas las genera el DataGridView automáticamente, pidiéndole a la columna una celda que tomará como modelo. (CellTemplate)
  • Tanto en la celda como en la columna, si hemos añadido propiedades nuevas, hemos de sobreescribir el método Clone(), para poder persistir los cambios a dichas propiedades desde el diseñador.
  • En la clase de la columna, debemos proporcionar dicha celda modelo a través de la propiedad CellTemplate.

Una vez llegados a este punto, vamos a comentar brevemente lo que haremos para poder navegar por el objeto. Como a priori no conocemos el tipo del objeto que se va a enlazar con nuestra columna, hemos de utilizar reflexión (System.Reflection). Así, nuestra columna y celda podrá navegar por el objeto sin problemas, y servirá para cualquier objeto que le queramos pasar.

Vamos a empezar por pensar qué propiedades extra nos harán falta para poder navegar por el objeto:

  • El nombre de la propiedad a la que queremos acceder dentro del objeto hijo. (en nuestro ejemplo podría ser Street, o cualquier otra propiedad de la clase Address)
  • El nombre de la propiedad que contiene al objeto hijo dentro del objeto padre. (en nuestro ejemplo, Address, en la clase Person)

Desde el punto de vista de la columna, la segunda propiedad extra no es necesaria, ya que tenemos la propiedad DataPropertyName. Sin embargo la celda necesitará esta información para poder acceder al objeto hijo utilizando reflexión.

Analicemos el código de la celda:

   1:  using System;
   2:  using System.Collections.Generic;
   3:  using System.Text;
   4:  using System.Windows.Forms;
   5:  using System.Reflection;
   6:   
   7:  namespace Geeks.Controls
   8:  {
   9:      /// <summary>
  10:      /// Celda por defecto de CustomTextColumn, navega por el objeto enlazado 
  11:      /// para establecer y mostrar el valor de la propiedad elegida.
  12:      /// </summary>
  13:      public class CustomTextCell : DataGridViewTextBoxCell
  14:      {
  15:          /// <summary>
  16:          /// La propiedad del objeto padre que contiene al objeto hijo.
  17:          /// </summary>
  18:          private string parentPropertyName;
  19:          /// <summary>
  20:          /// La propiedad del objeto hijo que queremos manipular.
  21:          /// </summary>
  22:          private string dataMember;
  23:          /// <summary>
  24:          /// Información de la propiedad de objeto padre obtenida por Reflexión.
  25:          /// </summary>
  26:          private PropertyInfo property;
  27:          /// <summary>
  28:          /// Información de la propiedad del objeto hijo obtenida por Reflexión.
  29:          /// </summary>
  30:          private PropertyInfo childProperty;
  31:          /// <summary>
  32:          /// DataGridView al que pertenece la celda.
  33:          /// </summary>
  34:          private DataGridView ownerGrid;
  35:   
  36:          /// <summary>
  37:          /// La propiedad del objeto hijo que queremos manipular.
  38:          /// </summary>
  39:          public string DataMember
  40:          {
  41:              get { return dataMember; }
  42:              set
  43:              {
  44:                  dataMember = value;
  45:              }
  46:          }
  47:   
  48:          /// <summary>
  49:          /// La propiedad del objeto padre que contiene al objeto hijo.
  50:          /// </summary>
  51:          public string ParentPropertyName
  52:          {
  53:              get { return parentPropertyName; }
  54:              set
  55:              {
  56:                  parentPropertyName = value;
  57:              }
  58:          }
  59:   
  60:          /// <summary>
  61:          /// Crea una copia del objeto actual
  62:          /// </summary>
  63:          /// <returns>Una copia del objeto actual.</returns>
  64:          public override object Clone()
  65:          {
  66:              CustomTextCell cell = base.Clone() as CustomTextCell;
  67:   
  68:              cell.dataMember = this.dataMember;
  69:              cell.parentPropertyName = this.parentPropertyName;
  70:              cell.property = this.property;
  71:              cell.childProperty = this.childProperty;
  72:              cell.ownerGrid = ownerGrid;
  73:   
  74:              return cell;
  75:          }
  76:   
  77:          /// <summary>
  78:          /// Almacena el grid al que pertenece la celda actual.
  79:          /// </summary>
  80:          protected override void OnDataGridViewChanged()
  81:          {
  82:              base.OnDataGridViewChanged();
  83:              ownerGrid = this.DataGridView;
  84:          }
  85:   
  86:          /// <summary>
  87:          /// Devuelve el tipo del valor almacenado en la celda. Como el 
  88:          /// DataGridView chequea los tipos automáticamente, hemos de 
  89:          /// devolver el tipo de la propiedad hija para que el control 
  90:          /// funcione correctamente.
  91:          /// </summary>
  92:          public override Type ValueType
  93:          {
  94:              get
  95:              {
  96:                  Type result;
  97:   
  98:                  if (childProperty != null)
  99:                  {
 100:                      result = childProperty.DeclaringType;
 101:                  }
 102:                  else
 103:                  {
 104:                      result = typeof(object);
 105:                  }
 106:   
 107:                  return result;
 108:              }
 109:          }
 110:   
 111:          /// <summary>
 112:          /// Devuelve el objeto enlazado a esta columna. Dado que la columna
 113:          /// está enlazada al objeto hijo, hemos de devolver el tipo de 
 114:          /// dicho objeto, no el de la propiedad que mostramos.
 115:          /// </summary>
 116:          /// <param name="formattedValue">El valor de la propiedad hija.</param>
 117:          /// <param name="cellStyle">El estilo de la celda.</param>
 118:          /// <param name="formattedValueTypeConverter">Conversor de tipos para el valor formateado.</param>
 119:          /// <param name="valueTypeConverter">Conversor de tipos para el valor real.</param>
 120:          /// <returns>El objeto enlazado a esta columna en la fila actual (el objeto padre).</returns>
 121:          public override object ParseFormattedValue(object formattedValue,
 122:              DataGridViewCellStyle cellStyle, 
 123:              System.ComponentModel.TypeConverter formattedValueTypeConverter,
 124:              System.ComponentModel.TypeConverter valueTypeConverter)
 125:          {
 126:              object currentObject = null;
 127:   
 128:              if (this.SetValue(this.RowIndex, formattedValue))
 129:              {
 130:                  currentObject = this.UpdateCurrentObject(this.RowIndex);
 131:              }
 132:              return currentObject;
 133:          }
 134:   
 135:          /// <summary>
 136:          /// Obtiene el valor de la propiedad hija.
 137:          /// </summary>
 138:          /// <param name="rowIndex">Indice de la fila de la celda actual.</param>
 139:          /// <returns>El valor de la propiedad hija. Esto es lo que el DataGridView va a mostrar en la celda actual.</returns>
 140:          protected override object GetValue(int rowIndex)
 141:          {
 142:              object value = null;
 143:   
 144:              object currentObject = UpdateCurrentObject(rowIndex);
 145:   
 146:              if (currentObject != null)
 147:              {
 148:                  value = childProperty.GetValue(currentObject, new object[] { });
 149:              }
 150:   
 151:              return value;
 152:          }
 153:   
 154:          /// <summary>
 155:          /// Almacena el valor del elemento enlazado a la fila actual para la columna.
 156:          /// </summary>
 157:          /// <param name="rowIndex">Indice de la fila.</param>
 158:          /// <param name="value">Valor de la propiedad hija.</param>
 159:          /// <returns>Un booleano indicando si hemos podido almacenar el valor o no.</returns>
 160:          protected override bool SetValue(int rowIndex, object value)
 161:          {
 162:              bool setValueInBase = false;
 163:              bool result = false;
 164:   
 165:              try
 166:              {
 167:                  object currentObject = UpdateCurrentObject(rowIndex);
 168:   
 169:                  if (currentObject == null)
 170:                  {
 171:                      setValueInBase = true;
 172:                  }
 173:                  else
 174:                  {
 175:                      // SetValue puede ser invocado por el DataGridView despues de llamar a ParseFormattedValue.
 176:                      // Si es así, el valor que recibimos no es el valor formateado (un string en el ejemplo)
 177:                      // sino el objeto enlazado por la propiedad padre, y podemos considerar 
 178:                      // que se ha almacenado correctamente.
 179:                      if (currentObject.Equals(value))
 180:                      {
 181:                          result = true;
 182:                      }
 183:                      else
 184:                      {
 185:                          childProperty.SetValue(currentObject, value, new object[] { });
 186:                          result = true;
 187:                      }
 188:                  }
 189:              }
 190:              catch (Exception)
 191:              {
 192:                  setValueInBase = true;
 193:              }
 194:   
 195:              if (result)
 196:              {
 197:                  this.RaiseCellValueChanged(new DataGridViewCellEventArgs(this.ColumnIndex, rowIndex));
 198:              }
 199:   
 200:              if (setValueInBase)
 201:              {
 202:                  result = base.SetValue(rowIndex, value);
 203:              }
 204:   
 205:              return result;
 206:          }
 207:   
 208:          /// <summary>
 209:          /// Obtiene el objeto enlazado para la fila indicada. Esto devolverá 
 210:          /// el objeto completo, no el valor de la propiedad hija que mostramos.
 211:          /// </summary>
 212:          /// <param name="rowIndex">Fila de la cual obtenemos el valor enlazado</param>
 213:          /// <returns>El objeto enlazado completo, no el valor de la propiedad hija que mostramos.</returns>
 214:          private object UpdateCurrentObject(int rowIndex)
 215:          {
 216:              object currentObject = null;
 217:              object childObject = null;
 218:   
 219:              if (ownerGrid != null)
 220:              {
 221:                  currentObject = ownerGrid.Rows[rowIndex].DataBoundItem;
 222:   
 223:                  // Si tenemos un elemento enlazado, intentamos acceder a la propiedad hija  
 224:                  // para obtener o modificar su valor.
 225:                  if (currentObject != null)
 226:                  {
 227:                      if (property == null)
 228:                      {
 229:                          property = currentObject.GetType().GetProperty(parentPropertyName);
 230:                      }
 231:   
 232:                      childObject = property.GetValue(currentObject, new object[] { });
 233:   
 234:                      if (childObject != null && childProperty == null)
 235:                      {
 236:                          childProperty = childObject.GetType().GetProperty(dataMember);
 237:                      }
 238:                  }
 239:              }
 240:   
 241:              return childObject;
 242:          }
 243:      }
 244:  }

Lo que hace esta clase es ir actualizando el objeto actual en el método UpdateCurrentObject. La idea es utilizar reflexión lo mínimo posible, ya que la información que nos devuelve la reflexión a la hora de obtener datos sobre una propiedad puede ser reutilizada en cualquier instancia del tipo para el que hemos obtenido la información. Por lo tanto, en ese método cacheamos la información obtenido utilizando reflexión, y la utilizamos para cualquier objeto enlazado en las filas del DataGridView.

Cuando el DataGridView tenga que obtener un valor para mostrar, llamará a nuestro método GetValue, y cuando quiera modificar el valor porque un usuario ha cambiado los datos, llamará a nuestro método SetValue. Estos dos métodos utilizarán la información de la reflexión para poder acceder o modificar el valor de la propiedad hija que queremos manupular.

Examinemos ahora el código de la columna:

   1:  using System;
   2:  using System.Collections.Generic;
   3:  using System.Text;
   4:  using System.Windows.Forms;
   5:   
   6:  namespace Geeks.Controls
   7:  {
   8:      /// <summary>
   9:      /// Columna de texto que permite manipular una propiedad de un objeto 
  10:      /// agregado.
  11:      /// </summary>
  12:      public class CustomTextColumn : DataGridViewTextBoxColumn
  13:      {
  14:          /// <summary>
  15:          /// La propiedad que queremos manipular.
  16:          /// </summary>
  17:          private string dataMember;
  18:          /// <summary>
  19:          /// La celda modelo de todas las celdas de esta columna.
  20:          /// </summary>
  21:          private CustomTextCell template;
  22:   
  23:          /// <summary>
  24:          /// Constructor. Inicializa la celda modelo.
  25:          /// </summary>
  26:          public CustomTextColumn()
  27:              : base()
  28:          {
  29:              template = new CustomTextCell();
  30:          }
  31:   
  32:          /// <summary>
  33:          /// La propiedad hija a manipular.
  34:          /// </summary>
  35:          public string DataMember
  36:          {
  37:              get { return dataMember; }
  38:              set
  39:              {
  40:                  dataMember = value;
  41:                  UpdateTemplate();
  42:              }
  43:          }
  44:   
  45:          /// <summary>
  46:          /// Actualiza la celda modelo con las propiedades de la columna.
  47:          /// </summary>
  48:          private void UpdateTemplate()
  49:          {
  50:              template.DataMember = dataMember;
  51:              template.ParentPropertyName = DataPropertyName;
  52:          }
  53:   
  54:          /// <summary>
  55:          /// Obtiene o establece la celda a utilizar como modelo de las celdas de la columna.
  56:          /// </summary>
  57:          public override DataGridViewCell CellTemplate
  58:          {
  59:              get
  60:              {
  61:                  UpdateTemplate();
  62:                  return template;
  63:              }
  64:              set
  65:              {
  66:                  CustomTextCell cell = value as CustomTextCell;
  67:   
  68:                  if (cell == null)
  69:                  {
  70:                      throw new ArgumentException("Tipo inválido de celda. " + 
  71:                      "CustomTextColumn sólo puede tener celdas de tipo " + 
  72:                      "CustomTextCell.");
  73:                  }
  74:   
  75:                  template = cell;
  76:              }
  77:          }
  78:   
  79:          /// <summary>
  80:          /// Devuelve una copia del objeto actual.
  81:          /// </summary>
  82:          /// <returns>La copia del objeto.</returns>
  83:          public override object Clone()
  84:          {
  85:              CustomTextColumn cloned = base.Clone() as CustomTextColumn;
  86:   
  87:              cloned.dataMember = dataMember;
  88:              cloned.template = template;
  89:   
  90:              return cloned;
  91:          }
  92:   
  93:      }
  94:  }

Como podéis ver, el código de la columna es más sencillo, ya que lo que hace es mantener actualizada la celda "modelo", y proporcionarsela al DataGridView cuando se la pide mediante la propiedad CellTemplate. Además, sobreescribe el método Clone, para poder copiar los miembros añadidos a la columna.

Para terminar, para utilizar esta columna, simplemente hemos de seleccionarla como tipo de columna (al compilar el proyecto nos debería aparecer como uno de los tipos de columna disponibles en el DataGridView) y rellenar las propiedades DataPropertyName, con el nombre de la propiedad que contiene el objeto hijo, y la propiedad DataMember con la propiedad hija a manipular.

Posibles mejoras que se pueden hacer:

  • Sólo navega un nivel de profundidad. Sería deseable que el nivel de profundidad fuese arbitrario, por ejemplo haciendo que la propiedad DataMember pudiese indicar un camino de la forma siguiente "Propiedad1.Propiedad2….PropiedadN"
  • Que acepte otros tipos de datos, a parte de Strings (usar los conversores de tipos apropiados una vez que sepamos el tipo de la propiedad hija)
  • Que haya más tipos de columnas: Checkboxes, imágenes… Lo que se os ocurra.

Bueno, menudo ladrillo que os he metido en este mi primer post. Espero que lo disfrutéis. Saludos!

El código de este artículo está formateado utilizando c# code format.