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.