Reutilizar un convertidor de tipo en varias columnas del control DataGrid de Silverlight

Cuando mostramos ciertos tipos de datos, tales como fechas y números, mediante el control DataGrid de Silverlight, habitualmente necesitamos aplicarles un formato previo a la presentación, para que su visualización resulte más adecuada al usuario.


Como ya vimos en un artículo anterior, el mecanismo que nos ofrece Silverlight para aplicar formato a los datos antes de presentarlos pasa por el uso de un convertidor de tipo, que consiste en una clase que implementa la interfaz IValueConverter, cuyo método Convert recibe entre sus parámetros un tipo object llamado value, que contiene el valor a visualizar. Dentro del código de este método aplicamos el formato necesario al parámetro value, devolviéndolo como resultado.


 


Preparando los datos a visualizar


Supongamos que estamos desarrollando una página Web con contenido Silverlight, en donde planeamos situar un control DataGrid para visualizar el resultado de la siguiente consulta contra la base de datos Northwind de SQL Server.


SELECT OrderID, CustomerID, EmployeeID, OrderDate, ShippedDate, Freight FROM Orders

 


El servicio WCF. Comunicando con la fuente de datos


Para obtener el conjunto de datos en la parte Silverlight de nuestra aplicación utilizamos un servicio WCF creado dentro del proyecto Web de la solución, a partir de la nueva plantilla específica para servicios WCF de Silverlight que vemos en la siguiente imagen.



 


Este servicio contendrá un único método, encargado de realizar la consulta hacia la base de datos.


[ServiceContract(Namespace = «»)]
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
public class WSDatos
{
[OperationContract]
public List<Order> ObtenerOrders()
{
string sSQL = «SELECT OrderID, CustomerID, EmployeeID, OrderDate, ShippedDate, Freight FROM Orders»;

SqlConnection cnConexion = new SqlConnection(ConfigurationManager.ConnectionStrings[«CadConexNorthwind»].ConnectionString);
SqlCommand cmdComando = new SqlCommand(sSQL, cnConexion);
List<Order> lstOrders = new List<Order>();

cnConexion.Open();

SqlDataReader drLectorDatos = cmdComando.ExecuteReader();

while (drLectorDatos.Read())
{
lstOrders.Add(new Order((int)drLectorDatos.GetValue(drLectorDatos.GetOrdinal(«OrderID»)),
(string)drLectorDatos.GetValue(drLectorDatos.GetOrdinal(«CustomerID»)),
(int)drLectorDatos.GetValue(drLectorDatos.GetOrdinal(«EmployeeID»)),
(DateTime)drLectorDatos.GetValue(drLectorDatos.GetOrdinal(«OrderDate»)),
Convert.IsDBNull(drLectorDatos.GetValue(drLectorDatos.GetOrdinal(«ShippedDate»))) ? new DateTime() : (DateTime)drLectorDatos.GetValue(drLectorDatos.GetOrdinal(«ShippedDate»)),
(decimal)drLectorDatos.GetValue(drLectorDatos.GetOrdinal(«Freight»))));
}

cnConexion.Close();

return lstOrders;
}
}


 


La clase para el mapeo de los datos


Podemos apreciar en el código anterior que el conjunto de resultados obtenido de la base de datos es volcado a una colección de tipo Order, clase que habremos creado previamente para mapear los registros de la base de datos.


[DataContract]
public class Order
{
private int mnOrderID;
private string msCustomerID;
private int mnEmployeeID;
private DateTime mdtOrderDate;
private DateTime mdtShippedDate;
private decimal mnFreight;

public Order(int nOrderID, string sCustomerID, int nEmployeeID,
DateTime dtOrderDate, DateTime dtShippedDate, decimal nFreight)
{
mnOrderID = nOrderID;
msCustomerID = sCustomerID;
mnEmployeeID = nEmployeeID;
mdtOrderDate = dtOrderDate;
mdtShippedDate = dtShippedDate;
mnFreight = nFreight;
}

[DataMember]
public int OrderID
{
get { return mnOrderID; }
set { mnOrderID = value; }
}

[DataMember]
public string CustomerID
{
get { return msCustomerID; }
set { msCustomerID = value; }
}

[DataMember]
public int EmployeeID
{
get { return mnEmployeeID; }
set { mnEmployeeID = value; }
}

[DataMember]
public DateTime OrderDate
{
get { return mdtOrderDate; }
set { mdtOrderDate = value; }
}

[DataMember]
public DateTime ShippedDate
{
get { return mdtShippedDate; }
set { mdtShippedDate = value; }
}

[DataMember]
public decimal Freight
{
get { return mnFreight; }
set { mnFreight = value; }
}
}


 


Un convertidor de tipo para fechas


Pasando ahora al desarrollo de la interfaz de usuario, si en el DataGrid que vamos a utilizar para presentar los datos obtenidos del servicio WCF, queremos formatear de igual manera los campos de tipo fecha, aplicaremos el mismo convertidor de tipo a todas las columnas del control que vayan a mostrar dicho tipo de dato. A continuación tenemos el código del convertidor de tipo para fechas.


public class FechaConvertidor : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return ((DateTime)value).ToShortDateString();
}

public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return null;
}
}


 


Y seguidamente el XAML del contenido Silverlight perteneciente a la página Web. Obsérvese el código de marcado para la definición de las columnas que muestran las fechas, así como el hecho de que en la Release Candidate de Silverlight, la propiedad DisplayMemberBinding utilizada en los objetos columna para establecer el enlace de datos, ha sido sustituida por Binding.


<UserControl
xmlns=»http://schemas.microsoft.com/winfx/2006/xaml/presentation»
xmlns:x=»http://schemas.microsoft.com/winfx/2006/xaml»
xmlns:data=»clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data»
x:Class=»ReutilizarConvertidorTipo.Page»
xmlns:propio=»clr-namespace:ReutilizarConvertidorTipo»
Width=»700″ Height=»400″>

<UserControl.Resources>
<propio:FechaConvertidor x:Key=»cnvFechaConvertidor» />
</UserControl.Resources>

<Grid x:Name=»LayoutRoot» Background=»Turquoise»>
<data:DataGrid x:Name=»grdDatos»
Width=»650″ Height=»350″
Background=»LightGoldenrodYellow»
AutoGenerateColumns=»False»
ColumnHeaderHeight=»40″
HeadersVisibility=»All»>
<data:DataGrid.Columns>
<data:DataGridTextColumn Binding=»{Binding OrderID}» Header=»Factura» />
<data:DataGridTextColumn Binding=»{Binding CustomerID}» Header=»Cliente» />
<data:DataGridTextColumn Binding=»{Binding EmployeeID}» Header=»Empleado» />

<data:DataGridTextColumn
Binding=»{Binding OrderDate, Converter={StaticResource cnvFechaConvertidor}}»
Header=»Fecha factura» />

<data:DataGridTextColumn
Binding=»{Binding ShippedDate, Converter={StaticResource cnvFechaConvertidor}}»
Header=»Fecha envío» />

</data:DataGrid.Columns>
</data:DataGrid>
</Grid>
</UserControl>


 

En la siguiente imagen podemos apreciar el resultado en tiempo de ejecución.



 


ConverterParameter. La propiedad clave para la reutilización


¿Pero qué ocurre si nos piden que cada columna de fecha tenga un formato distinto?. El modo más inmediato de solucionar el problema pasaría por crear distintos convertidores de tipo, cada uno con un formato diferente para cada columna. Sin embargo esta solución no tiene aspecto de parecer muy óptima.


El truco para resolver este inconveniente reside en hacer uso de la capacidad que el convertidor tiene de recibir un parámetro, cuyo valor podemos manipular en el método Convert, para efectuar el formato.


En primer lugar nos situaremos en el código XAML, y dentro de aquellas columnas que necesitemos formatear, modificaremos la expresión de marcado asignada a la propiedad Binding, añadiendo la propiedad ConverterParameter junto con la cadena de formato a pasar como parámetro.


<!–….–>
<data:DataGridTextColumn
Binding=»{Binding OrderDate,
Converter={StaticResource cnvFechaConvertidor}, ConverterParameter=dd-MMM-yyyy}»

Header=»Fecha factura» />

<data:DataGridTextColumn
Binding=»{Binding ShippedDate,
Converter={StaticResource cnvFechaConvertidor}, ConverterParameter=dddd, dd \de MMMM \de yyyy}»

Header=»Fecha envío» />
<!–….–>


 

En el caso de cadenas de formato complejas, como ocurre en la definición de la segunda columna del anterior código fuente, tendremos que hacer uso del carácter de escape (backslash) para evitar que ciertos caracteres provoquen un error por parte del parser de XAML, o que sean interpretados incorrectamente.


A continuación volveremos al método Convert de la clase del convertidor, y sobre el valor a formatear -que convertimos a DateTime- aplicaremos el método ToString, el cual dispone de una sobrecarga que admite una cadena de formato, a la que pasaremos el parámetro parameter del método Convert, que contiene la cadena pasada desde XAML a la propiedad ConverterParameter.


public class FechaConvertidor : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return ((DateTime)value).ToString(parameter.ToString(), Thread.CurrentThread.CurrentCulture);
}
//….
}

 


Como resultado obtendremos dos columnas formateadas de modo distinto, pero utilizando el mismo convertidor de tipo, tal y como apreciamos en la siguiente imagen.



 


Asignando el parámetro del convertidor desde codebehind


Si por algún motivo no fuera posible asignar el valor a la propiedad ConverterParameter desde XAML podemos recurrir al codebehind de la página Silverlight para tal cometido, aunque la técnica a utilizar posiblemente no sea la que tengamos pensada en primera instancia; expliquemos esto con más detalle.


Ya que tenemos que acceder a un objeto columna desde codebehind (por ejemplo, el que muestra el campo OrderDate), en lo primero que pensaremos será en asignar un nombre a dicha columna desde su definición en XAML, empleando el atributo x:Name.


<data:DataGridTextColumn
x:Name=»colOrderDate»
Binding=»{Binding OrderDate, Converter={StaticResource cnvFechaConvertidor}}»
Header=»Fecha factura» />

 

Pero si a continuación intentamos, desde el evento de carga del DataGrid, asignar la cadena de formato a esta columna, nos encontraremos con una excepción que nos indica que colOrderDate no contiene ningún objeto.



 


Dado que no podemos hacer referencia a la columna a través de su nombre, un modo de conseguir nuestro propósito consistiría en manipular la colección Columns del control DataGrid, obteniendo la columna a formatear a través del número de índice o posición que ocupa dentro de la colección.


public partial class Page : UserControl
{
public Page()
{
InitializeComponent();

this.Loaded += new RoutedEventHandler(Page_Loaded);
this.grdDatos.Loaded += new RoutedEventHandler(grdDatos_Loaded);
}

void grdDatos_Loaded(object sender, RoutedEventArgs e)
{
((DataGridTextColumn)this.grdDatos.Columns[3]).Binding.ConverterParameter = «dddd / MMMM-yyyy»;
}
//….
}


 


Pero el acceso a la columna del control utilizando esta técnica puede ser una fuente de problemas y suponer un trabajo extra en el mantenimiento del código de la aplicación, porque si por algún motivo nos vemos obligados a cambiar la posición de la columna, tendremos que ajustar todos aquellos lugares del código en los que hagamos referencia a la misma por la nueva posición que ocupe en la colección de columnas del DataGrid.


 


LINQ al rescate


Sin embargo no debemos desesperar, ya que aunque no podamos acceder a la columna a través de su nombre, la manipulación por índice de la colección de columnas del control no es la única alternativa que nos queda. Como vamos a ver a continuación, existe una tercera vía, en la que empleando un poco de la magia de LINQ, lograremos nuestro propósito; al fin y al cabo estamos en un blog de hechicería y conjuros con .NET, así que de vez en cuando podemos utilizarlos ¿no? ;-).


Bromas aparte, si observamos en la documentación las características de la propiedad Columns del control DataGrid, veremos que su árbol de herencia presenta la siguiente estructura.


 


IEnumerable<T>


    Collection<T>


        ObservableCollection<DataGridColumn>


            Columns


 


Dado que como podemos apreciar, se implementa IEnumerable<T>, es posible aplicar sobre esta colección expresiones LINQ para efectuar búsquedas entre sus elementos, y puesto que en las cabeceras -propiedad Header-de cada una de las columnas hemos asignado un título distinto, en el código que mostramos a continuación vemos cómo construir una expresión de este tipo, basada en una búsqueda por el texto de la cabecera. Una vez obtenido el iterador resultante de la expresión, ejecutaremos sobre el mismo su método Single, ya que solamente contendrá un único elemento -objeto DataGridTextColumn- al que asignaremos la cadena de formato.


void grdDatos_Loaded(object sender, RoutedEventArgs e)
{
var iterColumnas = from oColumna in this.grdDatos.Columns
where (string)oColumna.Header == «Fecha factura»
select oColumna;

((DataGridTextColumn)iterColumnas.Single()).Binding.ConverterParameter = «dddd / MMMM-yyyy»;
}


 


En la siguiente imagen se muestra el resultado de la ejecución de este código.



 


Como ventaja adicional del uso de LINQ en este escenario, podemos ahorrar gran parte del código que hemos escrito para construir la expresión de búsqueda, si al método Single de la colección DataGrid.Columns le pasamos una expresión lambda, que incorpore la condición a cumplir para asignar la cadena de formato al convertidor de la columna, como vemos seguidamente.


void grdDatos_Loaded(object sender, RoutedEventArgs e)
{
((DataGridTextColumn)this.grdDatos.Columns.Single(
col => (string)col.Header == «Fecha factura»)).Binding.ConverterParameter = «dddd / MMMM-yyyy»;
}

 

El resultado en ejecución será el mismo, pero el código a escribir habrá quedado mucho más compacto.


 


Creando un convertidor aplicable a varios tipos de datos


Si las columnas que debemos formatear en nuestro DataGrid son de diferentes tipos de datos -fecha y número como es el caso de este ejemplo-, no sería necesaria la creación de un convertidor para cada tipo, sino que podemos unificar las operaciones de conversión en una única clase, comprobando previamente, en el método Convert, el tipo de dato recibido, de modo que podamos formatearlo adecuadamente.


Trasladando estas especificaciones a una clase que llamaremos MultipleConvertidor, su código será el mostrado a continuación.


using System;
using System.Windows.Data;
using System.Threading;

namespace ReutilizarConvertidorTipo
{
public class MultipleConvertidor : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
string sResultadoFormateado = string.Empty;

if (value.GetType() == typeof(DateTime))
{
sResultadoFormateado = ((DateTime)value).ToString(parameter.ToString(), Thread.CurrentThread.CurrentCulture);
}
else if (value.GetType() == typeof(decimal))
{
sResultadoFormateado = ((decimal)value).ToString(parameter.ToString(), Thread.CurrentThread.CurrentCulture);
}

return sResultadoFormateado;
}

public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return null;
}
}
}


 


Retornando otra vez a XAML, vamos a configurar las columnas de los campos OrderDate y ShippedDate para que hagan uso de este nuevo convertidor -que añadiremos como recurso a la página Silverlight-, pero necesitamos otra columna adicional de un tipo de dato distinto.


El campo Freight, al mostrar un valor numérico monetario, se presenta como un estupendo candidato, por lo que también adaptaremos la definición de esta columna para que utilice el nuevo convertidor de tipo.


<UserControl.Resources>
<propio:FechaConvertidor x:Key=»cnvFechaConvertidor» />
<propio:MultipleConvertidor x:Key=»cnvMultipleConvertidor» />
</UserControl.Resources>
<!–….–>
<data:DataGridTextColumn
Binding=»{Binding OrderDate,
Converter={StaticResource cnvMultipleConvertidor}, ConverterParameter=dd-MMM-yyyy}»

Header=»Fecha factura» />

<data:DataGridTextColumn
Binding=»{Binding ShippedDate,
Converter={StaticResource cnvMultipleConvertidor}, ConverterParameter=dddd, dd \de MMMM \de yyyy}»

Header=»Fecha envío» />

<data:DataGridTextColumn
Binding=»{Binding Freight,
Converter={StaticResource cnvMultipleConvertidor}, ConverterParameter=#,#.## €}»

Header=»Gastos envío» />


 

A partir de este momento, estas tres columnas emplearán el nuevo convertidor de tipo, que formateará los valores mediante el parámetro, con el resultado que vemos en la siguiente imagen.



 


Finalizando las conversiones


Y llegados a este punto, damos por concluido este artículo en el que hemos tratado la capacidad de enviar un parámetro a un convertidor de tipo, de modo que mediante un único convertidor podamos efectuar operaciones de formato para varias columnas de un control DataGrid. Los ejemplos están disponibles en los siguientes enlaces: C# y VB.


Espero que os resulte de utilidad.


Un saludo.

9 Comentarios

  1. anonymous

    Excelente artículo!

  2. lmblanco

    Hola pacoweb

    Gracias por leerlo, celebro que te haya gustado.

    Un saludo.
    Luismi

  3. anonymous

    Creo que sería útil mencionarle al lector que para darle formato al texto de un Binding se podría simplemente usar algo como:

    Saludos!

  4. lmblanco

    Hola murki

    Gracias por el apunte, no obstante he intentado probar el código que has enviado y se produce un error de compilación.

    Quizá se trata de una característica específica de WPF que todavía no ha sido implementada en Silverlight. He utilizado para hacer esta prueba Visual Studio 2008 y Silverlight 2; puede que sea algún problema de versiones.

    Un saludo,
    Luismi

  5. anonymous

    Luismi, excelente artículo: claro y preciso.
    Gracias

  6. lmblanco

    Hola pontnou

    Muchas gracias por tu interés en el artículo, celebro que te haya gustado 😎

    Un saludo,
    Luismi

  7. anonymous

    Después de la introducción inicial a la plantilla ReadOnlyTemplate del control DataForm

  8. anonymous

    En el artículo dedicado a la edición de datos con plantillas en el DataForm, apuntábamos

  9. anonymous

    En el artículo dedicado a la edición de datos con plantillas en el DataForm, apuntábamos

Responder a Cancelar respuesta

Tema creado por Anders Norén