Silverlight DataGrid. Crear una columna a partir de varios valores

Cuando trabajamos con controles de tipo cuadrícula de datos, podemos encontrarnos ante la necesidad de incluir en una misma columna, varios de los campos obtenidos a partir de la fuente de datos con la que alimentamos el control, de forma que el usuario tenga la sensación de que se encuentra ante un único valor.


Para abordar el desarrollo de un requerimiento de estas características, el control DataGrid de Silverlight cuenta con un poderoso sistema de plantillas enlazadas a datos, que nos van a proporcionar una inusitada flexibilidad a la hora de definir las columnas de nuestro grid.


 


El sistema tradicional


Para conseguir un efecto como el que acabamos de mencionar, el truco al que habitualmente recurre el programador parte de la sentencia SQL utilizada para obtener los datos, y consiste en escribir dicha sentencia incluyendo un campo calculado, que engloba al conjunto de campos que se quieren mostrar en una sola columna.


Supongamos que la tabla con la que vamos a trabajar es Employees, de la base de datos Northwind. Aplicando el mencionado truco, la siguiente consulta nos devolvería los campos TitleOfCourtesy, FirstName y LastName como un único campo llamado NombreCompleto.


SELECT EmployeeID, Title,
(TitleOfCourtesy + ‘ ‘ + FirstName + ‘ ‘ + LastName) AS NombreCompleto,
City, Country
FROM Employees

Sin embargo, imaginemos que solamente necesitamos unir dichos campos en determinadas ocasiones, pero utilizando el mismo origen de datos. Para tales circunstancias podemos emplear la técnica que describiremos a lo largo del presente artículo.


 


Acceso directo a los datos desde ADO.NET


En un artículo publicado con anterioridad en este mismo blog, explicábamos los pasos a seguir para rellenar un control DataGrid de Silverlight utilizando LINQ to SQL, a través de un contexto de datos. Sin embargo, no es este el único medio de acceder al contenido de una fuente de datos, dado que también es posible utilizar para este propósito las clases tradicionales de ADO.NET (SqlConnection, SqlCommand, SqlDataAdapter, SqlDataReader, etc.), con un poco de trabajo adicional por parte del programador.


 


La clase para el mapeo de los registros de la base de datos


Tras crear un nuevo proyecto de tipo Silverlight, el primer paso que daremos será la creación de una clase con el nombre Employee, encargada de representar a los registros de la tabla Employees que posteriormente visualizaremos en el DataGrid. Debido a que la información de estos registros será volcada a una colección genérica de tipo List<Employee>, y enviada al control de cuadrícula a través de un servicio Web, es preciso que el tipo Employee pueda ser seriado, por lo que será necesario calificar la clase con el atributo DataContract, y cada propiedad con el atributo DataMember.


using System;
using System.Runtime.Serialization;
//….
[DataContract]
public class Employee
{
private int mnEmployeeID;
private string msTitle;
private string msTitleOfCourtesy;
private string msFirstName;
private string msLastName;
private string msCity;
private string msCountry;

public Employee(int nEmployeeID, string sTitle, string sTitleOfCourtesy,
string sFirstName, string sLastName, string sCity, string sCountry)
{
mnEmployeeID = nEmployeeID;
msTitle = sTitle;
msTitleOfCourtesy = sTitleOfCourtesy;
msFirstName = sFirstName;
msLastName = sLastName;
msCity = sCity;
msCountry = sCountry;
}

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

[DataMember]
public string Title
{
get { return msTitle; }
set { msTitle = value; }
}

[DataMember]
public string TitleOfCourtesy
{
get { return msTitleOfCourtesy; }
set { msTitleOfCourtesy = value; }
}

[DataMember]
public string FirstName
{
get { return msFirstName; }
set { msFirstName = value; }
}

[DataMember]
public string LastName
{
get { return msLastName; }
set { msLastName = value; }
}

[DataMember]
public string City
{
get { return msCity; }
set { msCity = value; }
}

[DataMember]
public string Country
{
get { return msCountry; }
set { msCountry = value; }
}
}


 


El servicio WCF. Canal de transporte para los datos


El siguiente paso consistirá en la creación del servicio WCF, al que llamaremos WSDatos, donde escribiremos un método encargado de conectar con la base de datos para obtener los registros de la tabla, los cuales pasaremos a una colección genérica de tipo List<Employee>, que devolveremos como resultado del método.


Recordemos que para que la comunicación entre el servicio y Silverlight funcione adecuadamente, en el archivo Web.config del proyecto Web tenemos que establecer el valor basicHttpBinding para el atributo binding de la etiqueta endpoint, dentro de la sección de configuración del servicio WCF.


<endpoint address=»» binding=»basicHttpBinding» contract=»DataGridColumnaValores_CSWeb.IWSDatos»>

A continuación se muestra el código fuente del servicio junto a su correspondiente interfaz.


using System.ServiceModel;
//….
[ServiceContract]
public interface IWSDatos
{
[OperationContract]
List<Employee> ObtenerEmployees();
}

using System.Collections.Generic;
using System.Data.SqlClient;
//….
public class WSDatos : IWSDatos
{
public List<Employee> ObtenerEmployees()
{
SqlConnection cnConexion = new SqlConnection(ConfigurationManager.ConnectionStrings[«CadConexion»].ConnectionString);

string sSQL = «SELECT EmployeeID, Title, TitleOfCourtesy, « +
«FirstName, LastName, City, Country « +
«FROM Employees»;

SqlCommand cmdComando = new SqlCommand(sSQL, cnConexion);

cnConexion.Open();

SqlDataReader drReader = cmdComando.ExecuteReader();
List<Employee> lstEmployees = new List<Employee>();

while (drReader.Read())
{
lstEmployees.Add(new Employee((int)drReader.GetValue(drReader.GetOrdinal(«EmployeeID»)),
(string)drReader.GetValue(drReader.GetOrdinal(«Title»)),
(string)drReader.GetValue(drReader.GetOrdinal(«TitleOfCourtesy»)),
(string)drReader.GetValue(drReader.GetOrdinal(«FirstName»)),
(string)drReader.GetValue(drReader.GetOrdinal(«LastName»)),
(string)drReader.GetValue(drReader.GetOrdinal(«City»)),
(string)drReader.GetValue(drReader.GetOrdinal(«Country»))));
}

cnConexion.Close();

return lstEmployees;
}
}


Esta colección es obtenida desde el evento de carga de la página Silverlight, para lo que previamente deberemos haber establecido en el proyecto Silverlight de la solución, una referencia hacia el servicio WCF.



 


public partial class Page : UserControl
{
private SolidColorBrush oOriginalBrush;
private double nTamFuente;
private StackPanel oStackPanel;
private TextBlock oTextBlock;

public Page()
{
InitializeComponent();

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

void Page_Loaded(object sender, RoutedEventArgs e)
{
RefWSDatos.WSDatosClient wsDatos = new DataGridColumnaValores_CS.RefWSDatos.WSDatosClient();
wsDatos.ObtenerEmployeesCompleted += new EventHandler<DataGridColumnaValores_CS.RefWSDatos.ObtenerEmployeesCompletedEventArgs>(wsDatos_ObtenerEmployeesCompleted);
wsDatos.ObtenerEmployeesAsync();
}
//….


Completada la llamada al método del servicio, asignaremos el resultado de la misma a la propiedad ItemsSource de un control DataGrid que hemos incluido en el código XAML.


void wsDatos_ObtenerEmployeesCompleted(object sender, DataGridColumnaValores_CS.RefWSDatos.ObtenerEmployeesCompletedEventArgs e)
{
this.grdDatos.ItemsSource = e.Result;
}

 


Construyendo la interfaz de usuario


La definición inicial de la interfaz de usuario en XAML podemos verla a continuación.


<UserControl
x:Class=»DataGridColumnaValores_CS.Page»
xmlns:my=»clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data»
xmlns=»http://schemas.microsoft.com/winfx/2006/xaml/presentation»
xmlns:x=»http://schemas.microsoft.com/winfx/2006/xaml»
Width=»550″ Height=»350″>

<Grid x:Name=»LayoutRoot» Background=»LightGreen»>
<Grid.RowDefinitions>
<RowDefinition Height=»Auto»></RowDefinition>
<RowDefinition Height=»Auto»></RowDefinition>
</Grid.RowDefinitions>

<TextBlock Grid.Row=»0″ HorizontalAlignment=»Center»
Margin=»10″ TextDecorations=»Underline»>
Tabla Employees
</TextBlock>

<my:DataGrid x:Name=»grdDatos»
Grid.Row=»1″
Width=»500″ Height=»250″
Margin=»10″
AutoGenerateColumns=»False»>
</my:DataGrid>
</Grid>
</UserControl>


En el caso de que fuéramos a mostrar cada campo de la fuente de datos en columnas independientes, podríamos utilizar la característica de creación automática de columnas, mediante la propiedad AutoGenerateColumns a True, o bien crear nosotros manualmente las columnas mediante etiquetas DataGridTextColumn, como vemos en este fuente.


<my:DataGrid.Columns>
<my:DataGridTextColumn Header=»Código» DisplayMemberBinding=»{Binding EmployeeID}» />
<my:DataGridTextColumn Header=»Cargo» DisplayMemberBinding=»{Binding Title}» />
<my:DataGridTextColumn Header=»Trat.» DisplayMemberBinding=»{Binding TitleOfCourtesy}» />
<my:DataGridTextColumn Header=»Nombre» DisplayMemberBinding=»{Binding FirstName}» />
<my:DataGridTextColumn Header=»Apellido» DisplayMemberBinding=»{Binding LastName}» />
<my:DataGridTextColumn Header=»Ciudad» DisplayMemberBinding=»{Binding City}» />
<my:DataGridTextColumn Header=»País» DisplayMemberBinding=»{Binding Country}» />
</my:DataGrid.Columns>

Con el resultado mostrado a continuación.



Pero como ya hemos comentado anteriormente, queremos agrupar los campos TitleOfCourtesy, FirstName y LastName en uno, por lo que al tratarse de una visualización personalizada de columna debemos recurrir a la etiqueta DataGridTemplateColumn, que nos permite crear la columna con una mayor libertad de acción.


 


DataGridTemplateColumn. El poder de la personalización en nuestras manos


Mediante DataGridTemplateColumn podemos establecer el modo de visualización y de edición personalizada para los datos de una columna; en esta ocasión nos limitaremos al primero de estos dos modos, para el que debemos utilizar su propiedad CellTemplate; dentro de este último es necesario situar una etiqueta DataTemplate, en cuyo interior agregaremos aquellos elementos que necesitemos para visualizar el valor. En nuestro caso, ya que se trata de mostrar tres valores de texto que no vamos a editar, una buena opción consistiría en emplear un TextBlock para cada uno de ellos. No obstante, si intentamos añadir directamente tres controles TextBlock dentro del DataTemplate de esta manera.


<my:DataGridTemplateColumn Header=»Nombre completo»>
<my:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text=»{Binding TitleOfCourtesy}» Margin=»10,0,5,0″ />
<TextBlock Text=»{Binding FirstName}» Margin=»0,0,5,0″ />
<TextBlock Text=»{Binding LastName}» Margin=»0,0,0,0″ />
</DataTemplate>
</my:DataGridTemplateColumn.CellTemplate>
</my:DataGridTemplateColumn>

Al compilar la aplicación obtendremos un error que nos informa de que el árbol visual está intentando ser asignado más de una vez.



Lo que quiere decir este error es que dentro de la etiqueta DataTemplate solamente puede añadirse un control. En ese caso, ¿cómo nos apañamos para añadir los tres que necesitamos?, pues de una manera muy sencilla: ya que no podemos poner más de un control, utilizaremos sólo uno, pero que permita actuar como contenedor de otros, es decir, emplearemos un panel, el cual nos permitirá hacer lo que pretendemos sin encontrarnos con errores.


<my:DataGridTemplateColumn Header=»Nombre completo»>
<my:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<StackPanel Orientation=»Horizontal»>
<TextBlock Text=»{Binding TitleOfCourtesy}» Margin=»10,0,5,0″ />
<TextBlock Text=»{Binding FirstName}» Margin=»0,0,5,0″ />
<TextBlock Text=»{Binding LastName}» Margin=»0,0,0,0″ />
</StackPanel>
</DataTemplate>
</my:DataGridTemplateColumn.CellTemplate>
</my:DataGridTemplateColumn>

Mediante este pequeño truco, conseguimos concatenar varios campos que provienen de la fuente de datos utilizada por el DataGrid en una simple columna. Observemos que para que los valores tengan un mínimo de separación unos de otros, dentro de la declaración de los controles TextBlock utilizamos su atributo Margin allá donde sea necesario.



 


Limando asperezas


Aunque ya hemos conseguido nuestro objetivo, aún podemos «pulir» un poco más el resultado, ya que si observamos detenidamente, el valor de nuestra columna personalizada no queda alineado verticalmente con el resto de columnas del control, sino que se encuentra alineado a la parte superior de la celda. La forma de solucionarlo consiste simplemente en recurrir al atributo VerticalAlignment del StackPanel, asignando el valor Center, para obtener un resultado mucho más adecuado.


<my:DataGridTemplateColumn Header=»Nombre completo»>
<my:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<StackPanel Orientation=»Horizontal» VerticalAlignment=»Center»>
<!–….–>


 


Aplicando un toque personal


Y ya que estamos metidos en faena, aprovechemos para dar una vuelta de tuerca más a este ejemplo, que consistiría en lo siguiente: según el usuario vaya desplazando el cursor por cada uno de los elementos de nuestra columna, debemos resaltar cada una de las partes que la componen de forma independiente, volviendo a su estado original cuando el cursor salga del área que ocupa.


Si utilizamos los eventos MouseEnter y MouseLeave del StackPanel, el efecto se aplicará al unísono sobre todo lo que dicho panel contenga, lo cual no es el comportamiento del que estamos hablando.


Puesto que estamos trabajando con el concepto de elementos contenidos, vamos a dar un paso más allá añadiendo al StackPanel de la plantilla de datos otro panel del mismo tipo para cada uno de los TextBlock; en estos tres nuevos StackPanel codificaremos los antes mencionados eventos del ratón, para conseguir el efecto deseado. El código de marcado es el que vemos en el siguiente listado.


<my:DataGridTemplateColumn Header=»Nombre completo»>
<my:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<StackPanel Orientation=»Horizontal» VerticalAlignment=»Center»>
<StackPanel VerticalAlignment=»Center»
MouseEnter=»StackPanel_MouseEnter» MouseLeave=»StackPanel_MouseLeave»>
<TextBlock Text=»{Binding TitleOfCourtesy}» Margin=»10,0,5,0″ />
</StackPanel>

<StackPanel VerticalAlignment=»Center»
MouseEnter=»StackPanel_MouseEnter» MouseLeave=»StackPanel_MouseLeave»>
<TextBlock Text=»{Binding FirstName}» Margin=»0,0,5,0″ />
</StackPanel>

<StackPanel VerticalAlignment=»Center»
MouseEnter=»StackPanel_MouseEnter» MouseLeave=»StackPanel_MouseLeave»>
<TextBlock Text=»{Binding LastName}» Margin=»0,0,0,0″ />
</StackPanel>
</StackPanel>
</DataTemplate>
</my:DataGridTemplateColumn.CellTemplate>
</my:DataGridTemplateColumn>


El code-behind que es necesario escribir para estos eventos es el siguiente.


private SolidColorBrush oOriginalBrush;
private double nTamFuente;
private StackPanel oStackPanel;
private TextBlock oTextBlock;

private void StackPanel_MouseEnter(object sender, MouseEventArgs e)
{
oStackPanel = sender as StackPanel;
oTextBlock = oStackPanel.Children[0] as TextBlock;

oOriginalBrush = oStackPanel.Background as SolidColorBrush;
nTamFuente = oTextBlock.FontSize;

oStackPanel.Background = new SolidColorBrush(Colors.Orange);
oTextBlock.FontSize = 20;
}

private void StackPanel_MouseLeave(object sender, MouseEventArgs e)
{
oStackPanel = sender as StackPanel;
oTextBlock = oStackPanel.Children[0] as TextBlock;

oStackPanel.Background = oOriginalBrush;
oTextBlock.FontSize = nTamFuente;
}


Para poder retornar al estado original del contenido visualizado, observemos que se declaran con ámbito a nivel de la clase un conjunto de variables que nos permitirán almacenar el objeto Brush, tamaño de fuente, etc. originales.


En la siguiente imagen podemos apreciar el resultado.



Llegados a este punto podemos dar por concluido el desarrollo de este ejemplo, en el que hemos abordado el modo en cómo Silverlight nos permite manipular la presentación de los datos en el control DataGrid a través de su mecanismo de plantillas. El código fuente del proyecto está disponible en los siguientes enlaces: C# y VB.


Esperando que os resulte de utilidad, un saludo para todos.

8 Comentarios

  1. anonymous

    Hermano… lo tendras en VB?

    esta excelente pero no comprendo el C# o C++

  2. lmblanco

    Hola Jesús

    Gracias por tu interés en el post, y sí, el ejemplo está disponible también en VB. Mira al final del texto del post y verás que hay un enlace con los fuentes para VB.

    Un saludo.
    Luismi

  3. anonymous

    Luis, muchas gracias por el post… me ha sevido de mucha ayuda.

    Soy nuevo en esta area, y tengo un proyecto casi por culminar, me gustaria mucho si pudieras asesormarme en algo.

    Desde mi WebServices llamo a una tabla y la convierto en una Lista (Collection Data)

    pero necesito separar ciertos datos de alli para mostrar cada uno de los campos en diferentes textbox, ya que la consulta siempre me retornara un solo registro.

    Como lo hago?

  4. lmblanco

    Hola Jesús

    Celebro que el artículo te haya resultado útil.

    Respecto a tu consulta, en los casos en los que vayas a obtener un único registro que posteriormente visualizaras en los controles de la interfaz de usuario en Silverlight, puedes crear un método específico en el servicio, que reciba un parámetro para devolver una instancia del objeto que represente al registro.

    Siguiendo con el ejemplo del artículo, podemos crear el siguiente método:

    //—————————————–
    public Employee ObtenerEmployee(int nEmployeeID)
    {
    SqlConnection cnConexion = new SqlConnection(ConfigurationManager.ConnectionStrings[«CadConexion»].ConnectionString);

    string sSQL = «SELECT EmployeeID, Title, TitleOfCourtesy, » +
    «FirstName, LastName, City, Country » +
    «FROM Employees » +
    «WHERE EmployeeID = @EmployeeID»;

    SqlCommand cmdComando = new SqlCommand(sSQL, cnConexion);
    cmdComando.Parameters.AddWithValue(«@EmployeeID», nEmployeeID);
    cnConexion.Open();

    SqlDataReader drReader = cmdComando.ExecuteReader();
    List lstEmployees = new List();

    drReader.Read();

    Employee oEmployee = new Employee(
    (int)drReader.GetValue(drReader.GetOrdinal(«EmployeeID»)),
    (string)drReader.GetValue(drReader.GetOrdinal(«Title»)),
    (string)drReader.GetValue(drReader.GetOrdinal(«TitleOfCourtesy»)),
    (string)drReader.GetValue(drReader.GetOrdinal(«FirstName»)),
    (string)drReader.GetValue(drReader.GetOrdinal(«LastName»)),
    (string)drReader.GetValue(drReader.GetOrdinal(«City»)),
    (string)drReader.GetValue(drReader.GetOrdinal(«Country»))
    );

    cnConexion.Close();

    return oEmployee;
    }
    //—————————————–

    A continuación, declaramos este nuevo método en la interfaz del servicio:

    //—————————————–
    [ServiceContract]
    public interface IWSDatos
    {
    [OperationContract]
    List ObtenerEmployees();

    // declaramos el nuevo método
    [OperationContract]
    Employee ObtenerEmployee(int nEmployeeID);
    }
    //—————————————–

    Seguidamente, en el proyecto Silverlight, actualizamos la referencia al servicio, para que se actualicen los cambios que hemos hecho en el proyecto Web. Pasamos al code-behind de la página Silverlight, y por ejemplo, en el método de carga, llamamos al nuevo método. Dentro del método de retorno, asignamos el resultado (objeto Employee) a los controles de la página.

    //—————————————–
    void Page_Loaded(object sender, RoutedEventArgs e)
    {
    RefWSDatos.WSDatosClient wsDatos = new DataGridColumnaValores_CS.RefWSDatos.WSDatosClient();

    wsDatos.ObtenerEmployeeCompleted += new EventHandler(wsDatos_ObtenerEmployeeCompleted);
    wsDatos.ObtenerEmployeeAsync(7);
    }

    void wsDatos_ObtenerEmployeeCompleted(object sender, DataGridColumnaValores_CS.RefWSDatos.ObtenerEmployeeCompletedEventArgs e)
    {
    RefWSDatos.Employee oEmployee = e.Result as RefWSDatos.Employee;
    this.txtFirstName.Text = oEmployee.FirstName;
    //…. asignar propiedades del objeto al resto de controles
    }
    //—————————————–

    Con algunos ajustes, supongo que este ejemplo se adaptará a lo que me comentabas.

    Un saludo.
    Luismi

  5. ifernandez

    Luismy CRACK 🙂

  6. lmblanco

    Hola Isaac, qué tal compi? 😉

    Gracias por tu opinión 8-D

    Saludotes.
    Luismi

  7. anonymous

    Me da este error los archivos
    The property ‘DisplayMemberBinding’ was not found in type ‘DataGridTextColumn’.

  8. lmblanco

    Hola César

    Desde que este post fue publicado, han aparecido nuevas versiones de Silverlight, en las que sus componentes han sufrido cambios. En el caso concreto del control DataGrid, para el tipo DataGridTextColumn que representa las columnas de la cuadrícula, la propiedad DisplayMemberBinding ha cambiado su nombre y ahora es Binding.

    Un saludo,
    Luismi

Responder a Cancelar respuesta

Tema creado por Anders Norén