domingo, 23 de noviembre de 2014


Personalizar el DataGridView


Introducción


Algunas veces, en un datagrid, queremos visualizar diferentes informaciones en una celda y no simplemente un valor por celda.

Por ejemplo, un ejemplo, sería un grid donde con una cierta frecuencia, el contenido numérico va a variar, pero, ¿qué sucede si para el usuario es importante no solamente la nueva cifra sino el incremento?




Suponiendo que tenemos una aplicación donde lleva el saldo de los Pitufos. Para el usuario de la aplicación, lo mismo sería interesante, no solamente ver el nuevo saldo, sino el tanto por ciento que ha variado.

Con los controles que nos ofrece .Net, una de las formas, sería añadir una nueva columna "% Balance", donde refleje la variación. Pero otra forma, sería, que en la misma celda, en letra más pequeña y con una flecha (hacía arriba para incrementos positivos y hacía abajo para incrementos negativos) para mostrar esta información.

Quedaría como:






Personalizar la celda del data Grid


Paso 1:

Crear una clase que derive de alguna de las siguientes clases, 

      System.Windows.Forms.DataGridViewButtonCell
      System.Windows.Forms.DataGridViewCheckBoxCell
      System.Windows.Forms.DataGridViewComboBoxCell
      System.Windows.Forms.DataGridViewHeaderCell
      System.Windows.Forms.DataGridViewImageCell
      System.Windows.Forms.DataGridViewLinkCell
      System.Windows.Forms.DataGridViewTextBoxCell

En este ejemplo, se eligio derivar de DataGridViewTextBoxCell, ya que era la clase que más acercaba a lo que queríamos representar. Dependiendo de lo que contendrá la celda, asi será la clase elegida para derivar.

class DataGridViewCellDoubleIncrement : System.Windows.Forms.DataGridViewTextBoxCell


Paso 2:

Sobreescribimos el método Paint. Este método será llamado siempre que la celda vaya a ser pintada. Aquí estará el código donde dibujaremos la información del incremento del %.

protected override void Paint(System.Drawing.Graphics graphics, System.Drawing.Rectangle clipBounds, System.Drawing.Rectangle cellBounds, int rowIndex, System.Windows.Forms.DataGridViewElementStates cellState, object value, object formattedValue, string errorText, 
            System.Windows.Forms.DataGridViewCellStyle cellStyle, System.Windows.Forms.DataGridViewAdvancedBorderStyle advancedBorderStyle, System.Windows.Forms.DataGridViewPaintParts paintParts)
        

Paso 3:

Sobre escribimos el método:

protected override System.Drawing.Size GetPreferredSize(System.Drawing.Graphics graphics, System.Windows.Forms.DataGridViewCellStyle cellStyle, int rowIndex, System.Drawing.Size constraintSize)


Este método será llamada para cuando se necesite saber el tamaño que tendrá la celda. Como vamos a añadir más información a la celda, necesitará más espacio. Entonces, aquí es donde se añada ese espacio extra para mostrar la información del %.

Paso 4:

Crear una clase que derive de .DataGridViewColumn, y está clase representará la columna que contendrá nuestro nuevo tipo de celda para visualizar el incremento del %.

 public class DataGridViewColumnDoubleIncrement : System.Windows.Forms.DataGridViewColumn


Paso 5:
Para que la columna sepa DataGridViewColumnDoubleIncrement  que tipo de celdas debe de crear, en el constructor se añade la línea:

this.CellTemplate = new DataGridViewCellDoubleIncrement();

Con lo anterior, ya tendríamos lo básico para personalizar la forma en que queremos visualizar los datos en cada celda.

Como ver el código, es más explicativo que párrafos y párrafos, en este link está el código fuente.

Conclusión

Como se ha visto, con un poco de trabajo, puede resultar de gran efecto visual en el interface de usuario y además, que el usuario, vea la información más cercana a su modelo mental, creando una mejor experiencia de usuario.

Félix Romo
felix.romo.sanchezseco@gmail.com



miércoles, 5 de noviembre de 2014


La API de tu interfaz (UserControls)


Introducción


Cuando uno desarrolla una aplicación de escritorio, piensa que el interfaz será estable, que una vez que se implemente, no se modificará y si se modifica son algunos detalles. Si se sigue este principio durante el desarrollo del UI, tendremos un problema. Ya que al cabo de una año o dos, por razones comerciales, de un cliente especial, de un nuevo jefe, se modifique gran parte del UI.

Un ejemplo, supongamos que estamos desarrollando un software para la gestión del video club, y en una parte del interfaz tenemos un ListView con el titulo de la pelicula, director, etc. Esta ListView se rellena desde una base de datos. Creo que el típico error aquí, es hacer visible la ListView con el resto de la aplicación. Es decir, seguir estos pasos:

- Leer información de las peliculas de la base de datos
- Añadir información a la ListView.



Pero vamos a complicar un poco la cosa. Ahora, una vez que se la información principal de la película, queremos que en otra parte de la pantalla, se muestre el argumento de la película.
Con lo cual, lo más normal es capturar el evento de selección de la ListView para actualizar y visualizar el argumento de la película seleccionada.



private void ON_lvFilms_SelectedIndexChanged(object sender, EventArgs e)
        {
            showPlotFromFilm();
        }



Así, el método showPlotFromFilm accederá al control listview, para obtener el titulo de la película y asi buscar en la base de datos o en memoria y mostrar el argumento. Aquí, hemos introducido un acoplamiento de miedo.

El problema viene cuando al cabo de un mes o un año, por el motivo que sea, hay que cambiar la listview por un treeview, ya que se ha decidido ver las películas por géneros y el treeview es por el momento el que más se ajusta.

Entonces, si hay que cambiarlo, habría que mirar todo el programa, desde donde es visible el listview para ahora engancharlo a un treeview. Podría llevar mucho tiempo, más luego los posibles bugs por un cambio que afecta tanto.

¿Cómo disminuir este riesgo?

Crear la API del UI para tu Aplicación


Para disminuir este riesgo, una opción (la verdad que otra de momento no alcanzo a verla) es crear un control de interfaz de usuario que su responsabilidad sea la de visualizar películas.

Al igual que existen los controles para visualizar texto (TextBox) o un fichero .jpg (PictureBox), nos creamos un control para representar un objeto del dominio del problema, en este caso, una conjunto de película (no escribo 'lista' para no sugerir que en la implementación que sea una lista).

El control que visualizará la lista de películas será FilmControl.cs (deriva de UserControl)

Y tendrá una propiedad:

public List<Kernel.Film> Films

Que de hay el control rellenará la lista de las peliculas en la lista que tiene interna, pero si en el futuro se cambia a un arbol, no afectaría externamente, es decir, fuera del ámbito del control.

Y luego tendría el evento:


[Description("Se selecciono una pelicula en el control.")]
public event selectFilmHandle ONselectFilm;




Que se dispara cuando el usuario seleccione una película en la listview. Si en el futuro fuese una arbol, la implementación por dentro, detectaría la selección en el arbol y lanzará despues el evento ONselectFilm.



Esta ha sido una forma algo simple de intentar explicar la buena practica de crear partes del interfaz de usuario que manejen directamente conceptos a nivel del dominio del problema y no de implementación.

En este ejemplo se ve por ejemplo en ONselectFilm, donde es un evento que se lanza cuando se selecciona una pelicula, pero por el contrario si fuese ON__lvFilms_SelectedIndexChanged, es un evento que se lanza cuando se selecciona un elemento de la lista, y lista es ya un elemento de la implementación, y sin embargo en ONselectFilm, no se hace referencia a ningún elemento de implementación del interfaz, sino que solamente se ha selecionado, ya sea por un ListView o un TreeView o cualquier otro UI más extraño. Así, hemos reducido el acoplamiento entre partes del programa enormemente.

Para descargar el código fuente de videoClub aquí.

Conclusión


Como experiencia, esta forma de trabajar me ha ahorrado bastantes horas de trabajo, ya que se intento crear una api UI para los objetos del dominio del problema (en la carpeta Kernel del ejemplo), de esta forma si una parte del interfaz se quiere llevar a otro sitio del interfaz, o recolocar la interfaz, será más rápido y menos propenso a errores (que los habrá claro).

Félix Romo

felix.romo.sanchezseco@gmail.com