Si estamos interesados en ir probando el funcionamiento de Silverlight 4 en Visual Studio 2010 Beta 2 (en Visual Studio 2010 RC todavía no está disponible Silverlight 4, Tim Heuer lo explica aquí), puede que nos encontremos con un extraño comportamiento a la hora de intentar agregar una referencia a nuestro proyecto, consistente en que la pestaña .NET, perteneciente a la ventana encargada de mostrar las referencias, aparece completamente vacía. A continuación se explica esta situación con más detalle y una posible solución.
En mi caso, una vez descargados e instalados todos los elementos necesarios para preparar el entorno de trabajo con Visual Studio 2010-Silverlight 4 (Visual Studio 2010 Beta 2, Silverlight 4 Tools for Visual Studio 2010 Beta 2 y Silverlight 4 Toolkit), inicié Visual Studio 2010, creando un nuevo proyecto de tipo Silverlight, en el que quería hacer algunas pruebas con el control DataForm.
Dado que el control DataForm pertenece al Silverlight Toolkit y no viene incorporado en el Cuadro de herramientas del entorno de desarrollo, dentro del Explorador de soluciones hice clic derecho en el nodo References del proyecto Silverlight, seleccionando la opción Add Reference.

Como resultado se abrió la ventana Add Reference, pero curiosamente, la pestaña .NET estaba vacía, con lo cual no podía seleccionar el elemento que necesitaba.

Se trata de un problema registrado por Microsoft (explican que será subsanado en una próxima release), que podemos solucionar utilizando la pestaña Browse de la misma ventana Add Reference, para desplazarnos hasta la ruta en la que reside el ensamblado, y agregarlo al proyecto.
Puesto que lo que necesitaba era hacer uso de un control, en mi caso opté por una vía alternativa para solucionar el problema. Para ello me situé en la Caja de herramientas de Visual Studio 2010, y añadí un nuevo grupo con el nombre Toolkit, haciendo clic derecho y seleccionando la opción Add Tab.

A continuación hice nuevamente clic derecho sobre el grupo recién creado, seleccionando la opción Choose Items, lo que abrió la ventana Choose Toolbox Items. En la pestaña Silverlight Components de esta ventana ya sí pude seleccionar el control DataForm para añadirlo al Cuadro de herramientas.

Finalmente, desde la Caja de herramientas incorporé una instancia del control al código XAML de la página Silverlight.

Como acabamos de comprobar, solucionar este inconveniente no resulta complicado, mientras esperamos la salida de la versión definitiva de Visual Studio 2010. Espero que os resulte de utilidad.
Un saludo.
Después de explicar el modo de dibujo y estados de presentación visual de los botones en la segunda parte, en esta tercera entrega, que concluye el presente artículo, abordaremos la interacción con las celdas de nuestra columna, pero esta vez mediante el teclado.
Ganando y perdiendo el foco de la celda mediante teclado... y clic de ratón
El siguiente comportamiento a programar para la celda será aquel que se produzca al dar o quitar el foco de entrada a la misma, cuando naveguemos con el teclado por las celdas del DataGridView, o bien, al hacer clic directamente sobre una celda.
Los métodos que debemos reemplazar en este caso son OnEnter y OnLeave, y el estado visual que necesitamos aplicar al botón cuando se ejecute OnEnter, consiste en el borde que revela que el botón tiene el foco de entrada. La siguiente imagen muestra ambos estados en los botones.
Dicho estado se aplicará en realidad dentro del código del método Paint, cuando hagamos la llamada al método ButtonRenderer.DrawButton, más concretamente, en el parámetro focused de dicho método. Utilizaremos el valor true para establecer el foco y false para quitarlo. Sin embargo, el valor del parámetro focused lo estableceremos en los métodos OnEnter y OnLeave a través de la variable bEnfocado, con ámbito a nivel de clase.
Por otro lado, también podemos encontrarnos con un efecto indeseado por una situación similar a la siguiente: supongamos que tenemos el cursor situado en una celda de nuestra columna personalizada, y mediante el teclado navegamos por otras celdas distintas de la misma columna. Es posible que la celda sobre la que se encuentra el cursor pierda su estado visual, PushButtonState.Hot, y pase a estado PushButtonState.Normal, debido a que no estamos manteniendo el índice de fila sobre la que está el cursor.
Para remediar este problema, declararemos otra nueva variable con ámbito de clase llamada nFilaCeldaHot, que será la encargada de almacenar en todo momento la fila sobre la que se encuentra el cursor. La asignación a esta variable la realizaremos en el método OnMouseEnter, mientras que en los métodos OnEnter y OnLeave consultaremos su valor, para así determinar el estado visual con que deberá dibujarse el botón cuando sea llamado el método Paint.
class DGVCeldaBoton : DataGridViewCell
{
//....
private bool bEnfocado = false;
static int nFilaCeldaHot = -1;
//....
protected override void OnMouseEnter(int rowIndex)
{
base.OnMouseEnter(rowIndex);
lstOperacion.Add(new Operacion()
{
Metodo = "OnMouseEnter",
Fila = rowIndex,
EstadoBoton = PushButtonState.Hot,
AjustePosicionImagen = 16,
AjusteDimensionesImagen = 28
});
// establecer el índice de fila para el estado visual del botón
nFilaCeldaHot = rowIndex;
this.DataGridView.InvalidateCell(this.ColumnIndex, rowIndex);
}
protected override void OnEnter(int rowIndex, bool throughMouseClick)
{
base.OnEnter(rowIndex, throughMouseClick);
if (throughMouseClick)
{
xEstadoBoton = PushButtonState.Pressed;
}
else
{
if (rowIndex == nFilaCeldaHot)
{
xEstadoBoton = PushButtonState.Hot;
}
}
bEnfocado = true;
}
protected override void OnLeave(int rowIndex, bool throughMouseClick)
{
base.OnLeave(rowIndex, throughMouseClick);
if (rowIndex == nFilaCeldaHot)
{
xEstadoBoton = PushButtonState.Hot;
}
else
{
xEstadoBoton = PushButtonState.Normal;
}
bEnfocado = false;
}
//....
}
Para que el nuevo comportamiento que deseamos conseguir surta efecto, nuevamente deberemos retocar el código del método Paint, de forma que el dibujo del botón refleje adecuadamente el estado que necesitamos en cada momento. En el siguiente bloque de código se muestra parte de dicho método, indicando los puntos en los que se han realizado las modificaciones.
protected override void Paint(Graphics graphics, Rectangle clipBounds, Rectangle cellBounds,
int rowIndex, DataGridViewElementStates cellState, object value, object formattedValue,
string errorText, DataGridViewCellStyle cellStyle,
DataGridViewAdvancedBorderStyle advancedBorderStyle, DataGridViewPaintParts paintParts)
{
//....
// si se ha ejecutado el método OnMouseEnter u OnMouseLeave,
// la colección lstOperacion contendrá elementos
if (lstOperacion.Count > 0)
{
//....
//....
//....
}
else
{
// ¡¡¡modificado!!!
// calcular posición y tamaño de la imagen a dibujar dentro del botón para el resto de métodos
nAjustePosicionImagen = nFilaCeldaHot == rowIndex ? (cellBounds.Width * 16) / 100 : (cellBounds.Width * 12) / 100;
nAjusteDimensionesImagen = nFilaCeldaHot == rowIndex ? (cellBounds.Width * 28) / 100 : (cellBounds.Width * 22) / 100;
}
// crear el rectángulo para la imagen que se situará dentro del botón
Rectangle rctImagen = new Rectangle(rctBoton.X + nAjustePosicionImagen, rctBoton.Y + nAjustePosicionImagen,
rctBoton.Width - nAjusteDimensionesImagen, rctBoton.Height - nAjusteDimensionesImagen);
// ¡¡¡modificado!!!
// dibujar el botón
ButtonRenderer.DrawButton(graphics,
rctBoton,
((DGVColumnaBoton)this.OwningColumn).ImagenBitmap,
rctImagen,
bEnfocado,
xEstadoBoton);
}
En la siguiente imagen podemos apreciar los diferentes estados visuales del botón en función de que este tenga o no el foco, y el cursor del ratón esté o no situado encima de la celda.

OnKeyDown y OnKeyUp. Simulando clics mediante teclado
Además de realizar la pulsación de los botones de las celdas mediante el ratón, también puede resultar de utilidad el uso del teclado, empleando las teclas Enter y la barra espaciadora. Esta funcionalidad podemos conseguirla reemplazando los métodos OnKeyDown y OnKeyUp, donde mediante el parámetro KeyEventArgs que reciben, comprobaremos la tecla pulsada, y en caso de coincidir con alguna de las anteriormente mencionadas, estableceremos los valores correspondientes en las variables utilizadas para controlar el estado visual del botón que se dibujará en Paint. Entre las propiedades de KeyEventArgs que debemos manipular se encuentra Handled, a la que asignaremos true cuando el usuario haya pulsado alguna de las teclas mencionadas, para indicar que manejaremos de forma personalizada el comportamiento en estos casos.
Adicionalmente, para poder capturar el evento de pulsación del botón desde el código del DataGridView, llamaremos, desde el método OnKeyUp de nuestra clase DGVCeldaBoton, al método RaiseCellClick, que heredamos de DataGridViewCell.
protected override void OnKeyDown(KeyEventArgs e, int rowIndex)
{
base.OnKeyDown(e, rowIndex);
if (e.KeyCode == Keys.Space || e.KeyCode == Keys.Enter)
{
xEstadoBoton = PushButtonState.Pressed;
this.DataGridView.InvalidateCell(this.ColumnIndex, rowIndex);
e.Handled = true;
}
}
protected override void OnKeyUp(KeyEventArgs e, int rowIndex)
{
base.OnKeyUp(e, rowIndex);
if (e.KeyCode == Keys.Space || e.KeyCode == Keys.Enter)
{
if (rowIndex == nFilaCeldaHot)
{
xEstadoBoton = PushButtonState.Hot;
}
else
{
xEstadoBoton = PushButtonState.Normal;
}
this.RaiseCellClick(new DataGridViewCellEventArgs(this.OwningColumn.Index, rowIndex));
this.DataGridView.InvalidateCell(this.ColumnIndex, rowIndex);
e.Handled = true;
}
}
Escribiendo el evento CellClick para el DataGridView
Llegados a este punto hemos finalizado la escritura del código para la clase DGVCeldaBoton. Volviendo al código del formulario del proyecto, escribiremos el manipulador para el evento CellClick del control DataGridView, donde al pulsar uno de los botones de nuestra columna personalizada, realizaremos un cálculo basado en la celda de la columna ListPrice, perteneciente a la fila sobre la que hemos pulsado, y en el TextBox del formulario.
private void dataGridView1_CellClick(object sender, DataGridViewCellEventArgs e)
{
if (this.txtPorcentaje.Text == string.Empty)
{
MessageBox.Show("Introducir valor para el porcentaje");
this.txtPorcentaje.Focus();
}
else
{
if (this.dataGridView1.Columns[e.ColumnIndex].Name == "colBotones")
{
decimal nListPrice = (decimal)this.dataGridView1.Rows[e.RowIndex].Cells["ListPrice"].Value;
int nPorcentaje = int.Parse(this.txtPorcentaje.Text);
decimal nDescuento = (nListPrice * nPorcentaje) / 100;
MessageBox.Show(nDescuento.ToString("#,#.##"), "Descuento aplicable");
}
}
}
El resultado de la operación lo visualizaremos en una caja de mensaje.

Y llegados a este punto, damos por concluido esta tercera entrega y el presente artículo, en el que paso a paso, hemos expuesto una técnica que nos permite, partiendo de cero, crear una columna personalizada para el control DataGridView, adaptando su comportamiento a nuestros requerimientos. En los enlaces C# y VB encontraremos el proyecto con el código fuente de este ejemplo. Espero que os resulte de ayuda.
Un saludo.
En la primera entrega de este artículo, el desarrollo de nuestra columna quedó pendiente de mostrar su contenido, aspecto que abordaremos en esta segunda parte, donde además de realizar el dibujo de las celdas y los botones que estas alojan, trataremos sobre la interacción que el usuario realiza sobre las mismas mediante el cursor del ratón.
Paint y ButtonRenderer.DrawButton. Dibujando la celda y su contenido
El problema radica en que no hemos escrito el código necesario para que se realice el dibujo de la celda y su contenido, lo cual haremos a continuación, reemplazando el método Paint de esta clase.
class DGVCeldaBoton : DataGridViewCell
{
//....
protected override void Paint(Graphics graphics, Rectangle clipBounds, Rectangle cellBounds,
int rowIndex, DataGridViewElementStates cellState, object value, object formattedValue,
string errorText, DataGridViewCellStyle cellStyle,
DataGridViewAdvancedBorderStyle advancedBorderStyle, DataGridViewPaintParts paintParts)
{
// llamar al método base
base.Paint(graphics, clipBounds, cellBounds, rowIndex, cellState, value, formattedValue,
errorText, cellStyle, advancedBorderStyle, paintParts);
// pintar borde de la celda
base.PaintBorder(graphics, clipBounds, cellBounds, cellStyle, advancedBorderStyle);
// crear el rectángulo que contendrá el botón,
// calcular su posición y dimensiones
int nAjustePosicionBoton = (cellBounds.Width * 10) / 100;
int nAjusteDimensionesBoton = (cellBounds.Width * 20) / 100;
Rectangle rctBoton = new Rectangle(cellBounds.X + nAjustePosicionBoton, cellBounds.Y + nAjustePosicionBoton,
cellBounds.Width - nAjusteDimensionesBoton, cellBounds.Height - nAjusteDimensionesBoton);
// crear el rectángulo que contendrá la imagen,
// calcular su posición y dimensiones
int nAjustePosicionImagen = (cellBounds.Width * 12) / 100;
int nAjusteDimensionesImagen = (cellBounds.Width * 22) / 100;
Rectangle rctImagen = new Rectangle(rctBoton.X + nAjustePosicionImagen, rctBoton.Y + nAjustePosicionImagen,
rctBoton.Width - nAjusteDimensionesImagen, rctBoton.Height - nAjusteDimensionesImagen);
ButtonRenderer.DrawButton(graphics,
rctBoton,
((DGVColumnaBoton)this.OwningColumn).ImagenBitmap,
rctImagen,
false,
PushButtonState.Normal);
}
//....
Como podemos apreciar en el código anterior, el parámetro cellBounds, que recibe el método Paint, nos servirá como base para calcular las superficies rectangulares del botón y la imagen que se alojarán en cada celda de la columna. Una vez realizados todos estos cálculos, dibujaremos el botón mediante la clase ButtonRenderer y su método estático DrawButton. En la sobrecarga que actualmente utilizamos de este método, los parámetros que recibe pertenecen a los siguientes tipos:
--Graphics. Tipo que encapsula la superficie de dibujo GDI+ sobre la que se va a realizar la operación de dibujo.
--Rectangle. Superficie que contendrá el botón.
--Image. Imagen que se dibujará sobre el botón.
--Rectangle. Superficie para dibujar la imagen que se situará encima del botón.
--Boolean. Permite indicar si el botón mostrará el rectángulo indicativo de haber obtenido el foco de entrada del usuario.
--PushButtonState. Tipo enumerado que permite establecer el estado visual del botón.
En la siguiente imagen apreciamos el resultado en tiempo de ejecución.

OnMouseEnter y OnMouseLeave. Respondiendo a las acciones del usuario con el ratón
Nuestro siguiente paso consistirá en reemplazar ciertos métodos de la clase base DataGridViewCell, para poder dotar al botón del comportamiento visual que necesitemos, como respuesta a las acciones del usuario.
Comenzaremos por OnMouseEnter y OnMouseLeave, los cuales son llamados al entrar y salir, respectivamente, el cursor del ratón en la celda.
La mecánica básica que debemos implementar en estos métodos es simple: llamar al método base e invalidar la celda para provocar una llamada al método Paint, que obligue a pintarla de nuevo.
protected override void OnMouseXXXr(int rowIndex)
{
base.OnMouseXXX(rowIndex);
this.DataGridView.InvalidateCell(this.ColumnIndex, rowIndex);
}
Sin embargo nos encontramos ante un problema: si el cursor está situado sobre una celda de nuestra columna personalizada y nos movemos a una celda adyacente de la misma columna, los métodos OnMouseEnter y OnMouseLeave se ejecutarán seguidos en primer lugar, y a continuación se ejecutarán los dos métodos Paint, como respuesta a haber invalidado visualmente las celdas en OnMouseEnter y OnMouseLeave. En el siguiente esquema podemos apreciar el flujo de ejecución que acabamos de describir.

Si el desplazamiento del cursor se realiza desde abajo hacia arriba, el orden de ejecución del método Paint sigue siendo el mismo, es decir, si movemos el cursor desde la fila 3 a la 2, Paint se ejecutará en primer lugar para la celda de la fila 2 y después para la fila 3.
Puesto que en este escenario se producen dos ejecuciones seguidas de Paint, necesitamos saber cuál ha sido debida a OnMouseEnter y cuál a OnMouseLeave. La técnica que vamos a emplear para solucionar el problema consistirá en pasar a Paint, desde cada uno de los métodos OnMouseXXX, información suficiente para saber cómo dibujar el botón.
El medio de transporte consistirá en una nueva clase que llamaremos Operación, cuyo código, que vemos a continuación, añadiremos a nuestro proyecto.
class Operacion
{
public string Metodo;
public int Fila;
public PushButtonState EstadoBoton;
public int AjustePosicionImagen;
public int AjusteDimensionesImagen;
}
En cada uno de los métodos OnMouseEnter y OnMouseLeave, crearemos una instancia de esta clase, añadiéndola a una colección genérica basada en dicho tipo, que habremos declarado con ámbito a nivel de la clase DGVCeldaBoton. Por otra parte, para poder manipular con más comodidad el estado visual del botón a dibujar, también declararemos, con el mismo ámbito, una variable de tipo PushButtonState, que inicializaremos en el constructor de la clase DGVCeldaBoton.
class DGVCeldaBoton : DataGridViewCell
{
private List<Operacion> lstOperacion = new List<Operacion>();
private PushButtonState xEstadoBoton;
public DGVCeldaBoton()
: base()
{
// establecer el estado visual inicial para el botón en el constructor
xEstadoBoton = PushButtonState.Normal;
}
//....
protected override void OnMouseEnter(int rowIndex)
{
base.OnMouseEnter(rowIndex);
lstOperacion.Add(new Operacion()
{
Metodo = "OnMouseEnter",
Fila = rowIndex,
EstadoBoton = PushButtonState.Hot,
AjustePosicionImagen = 16,
AjusteDimensionesImagen = 28
});
this.DataGridView.InvalidateCell(this.ColumnIndex, rowIndex);
}
protected override void OnMouseLeave(int rowIndex)
{
base.OnMouseLeave(rowIndex);
lstOperacion.Add(new Operacion()
{
Metodo = "OnMouseLeave",
Fila = rowIndex,
EstadoBoton = PushButtonState.Normal,
AjustePosicionImagen = 12,
AjusteDimensionesImagen = 22
});
this.DataGridView.InvalidateCell(this.ColumnIndex, rowIndex);
}
//....
}
Pasando al código del método Paint, ahora comprobaremos si la colección lstOperacion contiene elementos (indicativo de que se ha ejecutado un método OnMouseXXX), y en caso afirmativo, obtendremos de la colección (mediante una expresión LINQ) el objeto Operacion correspondiente a la fila sobre la que se está ejecutando el método Paint, para realizar los cálculos de posición y dimensiones de la imagen a dibujar, en función de las propiedades de dicho objeto.
protected override void Paint(Graphics graphics, Rectangle clipBounds, Rectangle cellBounds,
int rowIndex, DataGridViewElementStates cellState, object value, object formattedValue,
string errorText, DataGridViewCellStyle cellStyle,
DataGridViewAdvancedBorderStyle advancedBorderStyle, DataGridViewPaintParts paintParts)
{
// llamar al método base
base.Paint(graphics, clipBounds, cellBounds, rowIndex, cellState, value, formattedValue,
errorText, cellStyle, advancedBorderStyle, paintParts);
// pintar borde de la celda
base.PaintBorder(graphics, clipBounds, cellBounds, cellStyle, advancedBorderStyle);
// crear el rectángulo que contendrá el botón,
// calcular su posición y dimensiones
int nAjustePosicionBoton = (cellBounds.Width * 10) / 100;
int nAjusteDimensionesBoton = (cellBounds.Width * 20) / 100;
Rectangle rctBoton = new Rectangle(cellBounds.X + nAjustePosicionBoton, cellBounds.Y + nAjustePosicionBoton,
cellBounds.Width - nAjusteDimensionesBoton, cellBounds.Height - nAjusteDimensionesBoton);
// variables para calcular posición y tamaño de la imagen
// a dibujar dentro del botón
int nAjustePosicionImagen = 0;
int nAjusteDimensionesImagen = 0;
// si se ha ejecutado el método OnMouseEnter u OnMouseLeave,
// la colección lstOperacion contendrá elementos
if (lstOperacion.Count > 0)
{
// obtener de la colección lstOperacion
// la información relacionada con el método OnMouseXXX
// para dibujar el botón
Operacion oOperacion = lstOperacion.Find(oOp => oOp.Fila == rowIndex);
if (oOperacion != null)
{
nAjustePosicionImagen = (cellBounds.Width * oOperacion.AjustePosicionImagen) / 100;
nAjusteDimensionesImagen = (cellBounds.Width * oOperacion.AjusteDimensionesImagen) / 100;
xEstadoBoton = oOperacion.EstadoBoton;
}
// eliminar el objeto Operacion obtenido de la colección
lstOperacion.Remove(oOperacion);
}
else
{
// calcular posición y tamaño de la imagen a dibujar dentro del botón para el resto de métodos
nAjustePosicionImagen = (cellBounds.Width * 12) / 100;
nAjusteDimensionesImagen = (cellBounds.Width * 22) / 100;
}
// crear el rectángulo para la imagen que se situará dentro del botón
Rectangle rctImagen = new Rectangle(rctBoton.X + nAjustePosicionImagen, rctBoton.Y + nAjustePosicionImagen,
rctBoton.Width - nAjusteDimensionesImagen, rctBoton.Height - nAjusteDimensionesImagen);
// dibujar el botón
ButtonRenderer.DrawButton(graphics,
rctBoton,
((DGVColumnaBoton)this.OwningColumn).ImagenBitmap,
rctImagen,
false,
xEstadoBoton);
}
La siguiente imagen muestra un momento de la ejecución, donde podemos apreciar que una de las celdas se ha dibujado de modo diferente, debido a que hemos situado el cursor sobre la misma.

Y terminamos aquí la segunda entrega de este artículo, en la tercera parte, que concluye esta serie, abordaremos la navegación y pulsación mediante el teclado de las celdas de nuestra columna. En los enlaces C# y VB el lector tiene disponible el proyecto con el código fuente completo del ejemplo.
Un saludo.
La creación para el control DataGridView, de una columna cuyas celdas contengan botones, los cuales realicen una determinada acción al ser pulsados, es una tarea muy sencilla, tal y como ya explicamos en un artículo anterior de este blog, donde la clase DataGridViewButtonColumn nos proveía de los botones con toda la funcionalidad típica ya implementada.
Pero supongamos que nuestro DataGridView necesita una columna de botones con un mayor grado de personalización, donde por ejemplo, dibujemos una imagen dentro del botón, la cual cambie su tamaño cuando el cursor del ratón entre en su superficie.
Este comportamiento personalizado y otros muchos pueden ser logrados partiendo de cero si heredamos de las clases DataGridViewColumn y DataGridViewCell, como veremos a lo largo de las diferentes entregas que componen este artículo.
El proyecto base del ejemplo
En esta primera parte comenzaremos creando, desde Visual Studio 2008, un nuevo proyecto de tipo Windows Forms Application con el nombre DGVColumnaCeldasBotonPersonalizado, agregando al diseñador del formulario un DataGridView y varios controles adicionales, hasta conseguir el aspecto de la siguiente imagen.
En el manipulador del evento Load de este formulario añadiremos el siguiente bloque de código, que rellenará el DataGridView con un conjunto de registros de la tabla DimProduct, perteneciente a la base de datos AdventureWorksDW.
//....
using System.Data.SqlClient;
//....
private void Form1_Load(object sender, EventArgs e)
{
SqlConnection cnConexion = new SqlConnection();
cnConexion.ConnectionString = "Data Source=localhost;" +
"Initial Catalog=AdventureWorksDW;" +
"Integrated Security=True";
string sSQL = "SELECT ProductKey, SpanishProductName, ListPrice " +
"FROM DimProduct WHERE ListPrice IS NOT NULL";
SqlCommand cmdComando = new SqlCommand(sSQL, cnConexion);
SqlDataAdapter daAdaptador = new SqlDataAdapter(cmdComando);
DataSet dsDatos = new DataSet();
daAdaptador.Fill(dsDatos, "DimProduct");
this.dataGridView1.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.AllCells;
this.dataGridView1.DataSource = dsDatos;
this.dataGridView1.DataMember = "DimProduct";
}
De esta forma, el control presentará el siguiente aspecto en tiempo de ejecución.

Comenzando la creación de la columna personalizada
A continuación añadiremos al proyecto dos nuevas clases: la primera, DGVColumnaBoton, heredará de DataGridViewColumn, y constituirá la base para las celdas que conformarán su contenido; la segunda, DGVCeldaBoton, heredará de DataGridViewCell, y en ella crearemos los botones que constituirán el contenido de la columna, así como la implementación de la lógica para su comportamiento visual.
using System.Windows.Forms;
using System.Drawing;
using System.Windows.Forms.VisualStyles;
//....
//----------------------------------------
class DGVColumnaBoton : DataGridViewColumn
{
// inicializar una nueva instancia de esta clase
// pasando como parámetro una plantilla de celda, es decir,
// una instancia de la clase DataGridViewCell o derivada
public DGVColumnaBoton()
: base(new DGVCeldaBoton())
{
}
//....
}
//------------------------------------
class DGVCeldaBoton : DataGridViewCell
{
//....
}
DGVColumnaBoton. La clase para la construcción de columnas
Comenzando por la clase DGVColumnaBoton, el código que tenemos que escribir para la misma es muy sencillo, ya que consistirá en dos propiedades, que contendrán la información de la ruta en la que está el archivo de imagen para el botón, y una instancia de la propia imagen.
class DGVColumnaBoton : DataGridViewColumn
{
private string sImagen;
private Bitmap bmpImagen;
// inicializar una nueva instancia de esta clase
// pasando como parámetro una plantilla de celda, es decir,
// una instancia de la clase DataGridViewCell o derivada
public DGVColumnaBoton()
: base(new DGVCeldaBoton())
{
}
public string Imagen
{
get
{
return sImagen;
}
set
{
sImagen = value;
bmpImagen = new Bitmap(value);
this.Width = bmpImagen.Size.Width;
}
}
internal Bitmap ImagenBitmap
{
get
{
return bmpImagen;
}
}
}
DGVCeldaBoton. La clase para la construcción de celdas
La clase DGVCeldaBoton sin embargo va a darnos más trabajo, lo cual resulta totalmente comprensible, puesto que, como ya hemos apuntado anteriormente, será donde tendremos que codificar todo el comportamiento visual que queramos proporcionar a los botones alojados en las celdas. Empezaremos reemplazando la propiedad FormattedValueType, que se emplea para establecer el tipo de los datos formateados que se visualizan en la celda. En nuestro caso devolveremos un tipo object.
class DGVCeldaBoton : DataGridViewCell
{
public override Type FormattedValueType
{
get
{
return typeof(object);
}
}
//....
}
En este momento ya podríamos crear una instancia de nuestra nueva clase personalizada de columna, y añadirla al DataGridView del formulario de ejemplo. Observe el lector, cómo establecemos el tamaño de la columna personalizada mediante la asignación de las propiedades DataGridView.RowTemplate.Height y DGVColumnaBoton.Width.
private void Form1_Load(object sender, EventArgs e)
{
//....
this.dataGridView1.RowTemplate.Height = 70;
this.dataGridView1.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.AllCells;
this.dataGridView1.DataSource = dsDatos;
this.dataGridView1.DataMember = "DimProduct";
DGVColumnaBoton colBotones = new DGVColumnaBoton();
colBotones.Name = "colBotones";
colBotones.HeaderText = "Descuento";
colBotones.Imagen = System.Environment.CurrentDirectory + @"\BotonIr.jpg";
colBotones.Width = 70;
this.dataGridView1.Columns.Add(colBotones);
}
No obstante, el resultado obtenido no será el esperado, ya que como vemos en la siguiente imagen, el área del DataGridView donde debería aparecer nuestra nueva columna se encuentra vacía.

Concluye aquí la primera parte de este artículo, en donde hemos sentado las bases para la construcción de una columna personalizada para el control DataGrid. En la siguiente entrega explicaremos los pasos relacionados con el dibujo de las celdas contenidas en la columna, así como los diferentes estados visuales que pueden tomar los botones contenidos en las celdas al interaccionar el usuario. En los enlaces C# y VB el lector tiene disponible el proyecto con el código fuente completo del ejemplo.
Un saludo.
Cuando presentamos un conjunto de datos en forma tabular mediante el control DataGridView, hay ocasiones en las que se necesita proporcionar al usuario la posibilidad de realizar ciertas operaciones, en base a uno o varios valores situados en las celdas de una determinada fila del control.
Por ejemplo, en el formulario que vemos en la siguiente imagen se muestra un DataGridView con algunos campos de la tabla DimProduct, perteneciente a la base de datos AdventureWorksDW. El objetivo consiste en que el usuario pueda introducir en el TextBox un valor, que representa un porcentaje de descuento a aplicar sobre el campo ListPrice, de la fila del DataGridView que seleccione.

Entre las diversas técnicas disponibles para realizar la operación que acabamos de mencionar, en este artículo vamos a hacer uso de las clases DataGridViewButtonColumn y DataGridViewButtonCell, que combinaremos para crear, dentro del control DataGridView, una nueva columna calculada, cuyas celdas contengan botones, que al ser pulsados, nos permitan realizar una operación con los valores de otras celdas de la fila seleccionada. Como particularidad añadida, los botones de nuestra nueva columna contendrán una imagen.
En primer lugar crearemos, en Visual Studio 2008, un nuevo proyecto de tipo Windows Forms Application, al que daremos el nombre DGVColumnaBotonesImagen, añadiendo al diseñador del formulario los controles mostrados en la anterior imagen.
Seguidamente pasaremos al editor de código del formulario, y en su evento Load escribiremos el siguiente bloque de código, que usaremos para conectarnos a una base de datos y cargar el DataGridView con un subconjunto de los registros de la tabla.
using System.Data.SqlClient;
//....
namespace DGVColumnaBotonesImagen
{
public partial class Form1 : Form
{
//....
private void Form1_Load(object sender, EventArgs e)
{
SqlConnection cnConexion = new SqlConnection();
cnConexion.ConnectionString = "Data Source=localhost;" +
"Initial Catalog=AdventureWorksDW;" +
"Integrated Security=True";
string sSQL = "SELECT ProductKey, SpanishProductName, ListPrice " +
"FROM DimProduct WHERE ListPrice IS NOT NULL";
SqlCommand cmdComando = new SqlCommand(sSQL, cnConexion);
SqlDataAdapter daAdaptador = new SqlDataAdapter(cmdComando);
DataTable tblDimProduct = new DataTable();
daAdaptador.Fill(tblDimProduct);
this.dataGridView1.DataSource = tblDimProduct;
//....
}
//....
A continuación definiremos nuestra columna personalizada creando una instancia de la clase DataGridViewButtonColumn, que añadiremos a la colección de columnas del DataGridView.
private void Form1_Load(object sender, EventArgs e)
{
//....
DataGridViewButtonColumn colBotones = new DataGridViewButtonColumn();
colBotones.Name = "colBotones";
colBotones.HeaderText = "Valor Stock";
this.dataGridView1.Columns.Add(colBotones);
}
La siguiente imagen muestra el resultado.

El anterior código crea una nueva columna basada en botones, pero estos carecen de contenido. Para incluir una imagen en su interior crearemos un manipulador para el evento CellPainting del control, en el que realizaremos las siguientes operaciones:
En primer lugar comprobaremos si la celda a pintar corresponde a nuestra columna de botones y se encuentra en una fila válida; en caso afirmativo, pintamos la celda ejecutando el método DataGridViewCellPaintingEventArgs.Paint. Seguidamente obtenemos una instancia del botón situado en la celda y otra de la imagen (un icono en este ejemplo) a situar en su interior, dibujando esta mediante el método Graphics.DrawIcon, con un tamaño ligeramente inferior al del botón.
Finalmente, para poder visualizar de forma más adecuada el botón, ajustamos las dimensiones de la fila y columna que lo alojan en función de las dimensiones de la imagen. También debemos asignar el valor true a la propiedad DataGridViewCellPaintingEventArgs.Handled, que nos servirá para que el control tenga en cuenta la implementación que hemos realizado sobre este evento.
private void dataGridView1_CellPainting(object sender, DataGridViewCellPaintingEventArgs e)
{
if (e.ColumnIndex >= 0 && this.dataGridView1.Columns[e.ColumnIndex].Name == "colBotones" && e.RowIndex >= 0)
{
e.Paint(e.CellBounds, DataGridViewPaintParts.All);
DataGridViewButtonCell celBoton = this.dataGridView1.Rows[e.RowIndex].Cells["colBotones"] as DataGridViewButtonCell;
Icon icoAtomico = new Icon(Environment.CurrentDirectory + @"\Atomico.ico");
e.Graphics.DrawIcon(icoAtomico, e.CellBounds.Left + 3, e.CellBounds.Top + 3);
this.dataGridView1.Rows[e.RowIndex].Height = icoAtomico.Height + 10;
this.dataGridView1.Columns[e.ColumnIndex].Width = icoAtomico.Width + 10;
e.Handled = true;
}
}
En la siguiente imagen podemos ver el control con la columna resultante, incluyendo los botones con el icono.

Tan sólo resta añadir al botón la lógica que efectúe el cálculo del descuento comentado al comienzo del artículo, aspecto este que resolveremos codificando el evento CellClick del DataGridView. Al producirse este evento comprobaremos si existe valor en el TextBox del formulario, y en caso afirmativo, nos cercioraremos de que la celda sobre la que se ha realizado la pulsación corresponde a nuestra columna calculada, si esto se cumple, procederemos con el cálculo, mostrándolo en una caja de mensaje.
private void dataGridView1_CellClick(object sender, DataGridViewCellEventArgs e)
{
if (this.txtPorcentaje.Text == string.Empty)
{
MessageBox.Show("Introducir valor para el porcentaje");
this.txtPorcentaje.Focus();
}
else
{
if (this.dataGridView1.Columns[e.ColumnIndex].Name == "colBotones")
{
decimal nListPrice = (decimal)this.dataGridView1.Rows[e.RowIndex].Cells["ListPrice"].Value;
int nPorcentaje = int.Parse(this.txtPorcentaje.Text);
decimal nDescuento = (nListPrice * nPorcentaje) / 100;
MessageBox.Show(nDescuento.ToString("#,#.##"), "Descuento aplicable");
}
}
}
La siguiente imagen muestra el resultado de una operación.

Utilizando el control DataGridView, en este artículo hemos explicado una técnica, que de manera sencilla nos permite crear una columna conteniendo botones que muestran un icono en su interior, lo cual nos puede servir para dar una mejor apariencia a nuestros controles de cuadrícula. En los siguientes enlaces: C# y VB, disponemos del proyecto de ejemplo. Espero que os resulte de utilidad.
Un saludo.
En esta tercera entrega, que concluye el presente artículo sobre .NET RIA Services, veremos cómo dotar de mayor funcionalidad al servicio de dominio, añadiéndole nuevos métodos con los que enriquecer la interacción entre las capas cliente y servidora de nuestra aplicación. También explicaremos el modo de rellenar con valores el control DataGrid, objetivo principal del ejemplo.
Ordenando la lista de valores mediante LINQ
Posiblemente nos hayamos percatado de que al desplegar la lista de valores del ComboBox, estos se muestran sin un orden preestablecido, cuando lo más adecuado sería que estuvieran ordenados alfabéticamente.
Aunque la solución más sencilla sería modificar la vista de la base de datos para que devolviera el conjunto de resultados ya ordenado, en esta ocasión vamos a optar por la vía difícil, es decir, realizar la ordenación desde el código de la aplicación. Disponemos de dos técnicas para lograrlo.
La primera de estas técnicas pasa por manipular el objeto EntityQuery que devuelve el método GetPaisesPedidosQuery del contexto de dominio, aplicándole la ordenación mediante la cláusula orderby de LINQ, obteniendo de esta manera un nuevo objeto EntityQuery, que pasaremos como parámetro al método NorthwindContext.Load.
public partial class MainPage : UserControl
{
NorthwindContext oNorthwindContext = new NorthwindContext();
public MainPage()
{
InitializeComponent();
EntityQuery<PaisesPedido> qryConsulta = from oPaisPedido
in oNorthwindContext.GetPaisesPedidosQuery()
orderby oPaisPedido.ShipCountry
select oPaisPedido;
LoadOperation<PaisesPedido> oLoadOperation = oNorthwindContext.Load(qryConsulta);
this.cboPaises.ItemsSource = oLoadOperation.Entities;
}
//....
}
La siguiente imagen muestra la lista de elementos del ComboBox ordenados gracias a esta técnica.

La segunda técnica consiste en crear un nuevo método en el servicio de dominio, que devuelva la lista de entidades, ordenada mediante el método OrderBy de LINQ.
[EnableClientAccess()]
public class NorthwindService : LinqToSqlDomainService<NorthwindDataContext>
{
//....
public IQueryable<PaisesPedido> GetPaisesPedidosOrdenados()
{
return this.Context.PaisesPedidos.OrderBy(oPaisPedido => oPaisPedido.ShipCountry);
}
}
Volveremos a compilar la solución, de forma que .NET RIA Services genere el código correspondiente para la capa cliente de la aplicación, y desde el constructor de la página Silverlight invocaremos a este nuevo método.
//....
public MainPage()
{
InitializeComponent();
oNorthwindContext.Load(oNorthwindContext.GetPaisesPedidosOrdenadosQuery());
this.cboPaises.ItemsSource = oNorthwindContext.PaisesPedidos;
}
Carga de registros en el DataGrid
El siguiente paso consistirá en cargar en el DataGrid aquellos registros de la tabla Orders, cuyo campo ShipCountry sea igual al valor seleccionado en el ComboBox. Actualmente, el servicio de dominio sólo dispone del método GetOrders para obtener los valores de esta tabla, por lo que tendremos que añadir un nuevo método, al que daremos el nombre GetOrdersCiudadEntrega, que recibirá como parámetro una cadena con el nombre de la ciudad, que actuará como filtro en la devolución de los registros de la tabla. Al igual que en las pasadas ocasiones, emplearemos LINQ para aplicar la condición de filtro.
[EnableClientAccess()]
public class NorthwindService : LinqToSqlDomainService<NorthwindDataContext>
{
//....
public IQueryable<Order> GetOrdersCiudadEntrega(string sCiudadEntrega)
{
return this.Context.Orders.Where(oOrder => oOrder.ShipCountry == sCiudadEntrega);
}
//....
Después de generar nuevamente la solución, en el code-behind de la página Silverlight crearemos un manipulador para el evento SelectionChanged del ComboBox. Dentro del código de este manipulador, la primera acción será limpiar la colección de entidades Orders, ya que cada vez que hagamos una nueva carga de datos, la colección de valores que hubiera previamente no será eliminada de forma automática.
A continuación invocaremos al nuevo método que hemos creado para cargar los registros de Orders filtrados por ciudad. Debido a que la lista de elementos del ComboBox es una colección de objetos de tipo PaisesPedido, tendremos que hacer una operación de type-casting sobre el elemento seleccionado (ComboBox.SelectedItem), para obtener el valor de su propiedad ShipCountry.
Finalmente, asignaremos al DataGrid la colección de entidades de tipo Order, que se habrán cargado en la colección Orders del contexto de dominio.
//....
public MainPage()
{
InitializeComponent();
oNorthwindContext.Load(oNorthwindContext.GetPaisesPedidosOrdenadosQuery());
this.cboPaises.ItemsSource = oNorthwindContext.PaisesPedidos;
this.cboPaises.SelectionChanged += new SelectionChangedEventHandler(cboPaises_SelectionChanged);
}
void cboPaises_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
oNorthwindContext.Orders.Clear();
oNorthwindContext.Load(oNorthwindContext.GetOrdersCiudadEntregaQuery(((PaisesPedido)this.cboPaises.SelectedItem).ShipCountry));
this.grdDatos.ItemsSource = oNorthwindContext.Orders;
}
//....
La siguiente imagen muestra el resultado obtenido.

Visualizando la tabla Orders al completo en el DataGrid
Llegados al punto actual podríamos dar por concluido el ejemplo, pero existe un pequeño aspecto de la aplicación que podemos retocar para hacer que mejore su funcionalidad.
En su estado actual, nuestra aplicación permite seleccionar y presentar los registros de la tabla Orders, filtrados por el campo ShipCountry, pero es posible que en alguna ocasión también nos interese visualizar todos los registros de la tabla, lo que actualmente no es posible.
Para solucionar este pequeño inconveniente, vamos a eliminar de la base de datos la vista PaisesPedidos, creándola nuevamente con la siguiente sentencia.
CREATE VIEW PaisesPedidos
AS
SELECT DISTINCT ShipCountry FROM Orders
UNION
SELECT '----' AS ShipCountry
De esta forma, cuando el usuario seleccione el elemento del ComboBox que contiene los guiones, cargaremos en el DataGrid todos los registros de la tabla Orders usando el método GetOrders del servicio de dominio. El código que realiza la carga de los datos quedará a partir de ahora como vemos a continuación.
void cboPaises_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
oNorthwindContext.Orders.Clear();
string sShipCountry = ((PaisesPedido)this.cboPaises.SelectedItem).ShipCountry;
if (sShipCountry == "----")
{
oNorthwindContext.Load(oNorthwindContext.GetOrdersQuery());
}
else
{
oNorthwindContext.Load(oNorthwindContext.GetOrdersCiudadEntregaQuery(sShipCountry));
}
this.grdDatos.ItemsSource = oNorthwindContext.Orders;
}
A continuación podemos observar el DataGrid visualizando todos los registros de la tabla.

Y tras este retoque damos por finalizado este artículo donde nos hemos introducido en el acceso a datos desde Silverlight, utilizando .NET RIA Services. Espero que os resulte de interés y utilidad. Al igual que en las anteriores entregas, el proyecto de ejemplo está disponible en los enlaces C# y VB.
Un saludo.
En la primera entrega de este artículo acabábamos de crear el servicio de dominio de la aplicación, con los métodos para el lado servidor de la misma, que se ocupaban de interactuar con la base de datos. Si en este momento volvemos a compilar la solución, Visual Studio arrojará un error relacionado con el servicio de dominio que acabamos de crear.

El motivo de dicho error radica en que la entidad PaisesPedido, perteneciente al contexto de datos, carece de una clave; sin embargo, el servicio de dominio requiere que todas las entidades que expone tengan al menos una clave definida. Este problema lo solucionaremos abriendo el diseñador del modelo de datos, y asignando a la propiedad Primary Key, de la entidad PaisesPedido, el valor True.

De esta forma, al volver a compilar la aplicación ya no se producirá este error.
DomainContext. El código generado por .NET RIA Services para la capa cliente
La siguiente fase en el desarrollo de nuestro ejemplo corresponde a la interfaz de usuario. Situándonos en el proyecto Silverlight de la solución, al hacer clic en el icono Show All Files de la ventana Solution Explorer veremos que la carpeta Generated_Code contiene un archivo llamado SeleccionComboBox.Web.g.cs, en cuyo interior se encuentra el código correspondiente a las clases generadas por el servicio de dominio, pero en esta ocasión para el lado cliente de la aplicación.

Este código recibe también el nombre de contexto de dominio (DomainContext) y en él podemos encontrar la clase que lo representa: NorthwindContext, que hereda de la clase base DomainContext.
Dentro de NorthwindContext hallaremos las versiones cliente de los métodos de consulta del servicio de dominio. El nombre de estos métodos será igual que el existente en el lado servidor de la aplicación, pero incluyendo el sufijo Query.
public sealed partial class NorthwindContext : DomainContext
{
//....
public EntityQuery<Order> GetOrdersQuery()
{
return base.CreateQuery<Order>("GetOrders", null, false, true);
}
public EntityQuery<PaisesPedido> GetPaisesPedidosQuery()
{
return base.CreateQuery<PaisesPedido>("GetPaisesPedidos", null, false, true);
}
//....
}
Definiendo la interfaz de usuario
La primera acción a desempeñar en la construcción de nuestra interfaz de usuario Silverlight, consistirá en añadir los controles de datos necesarios en la página XAML; en concreto, un ComboBox y un DataGrid con las columnas de la tabla Orders que queramos visualizar.
<UserControl
x:Class="SeleccionComboBox.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:data="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"
mc:Ignorable="d" d:DesignWidth="640" d:DesignHeight="480">
<Grid x:Name="LayoutRoot">
<StackPanel Background="LightGreen">
<ComboBox Name="cboPaises" Width="200" DisplayMemberPath="ShipCountry" Margin="5" />
<data:DataGrid Name="grdDatos"
Width="600" Height="370"
AutoGenerateColumns="False" IsReadOnly="True">
<data:DataGrid.Columns>
<data:DataGridTextColumn Header="Pedido" Binding="{Binding OrderID}" />
<data:DataGridTextColumn Header="Cliente" Binding="{Binding CustomerID}" />
<data:DataGridTextColumn Header="Destinatario" Binding="{Binding ShipName}" />
<data:DataGridTextColumn Header="País entrega" Binding="{Binding ShipCountry}" />
<data:DataGridTextColumn Header="Ciudad entrega" Binding="{Binding ShipCity}" />
</data:DataGrid.Columns>
</data:DataGrid>
</StackPanel>
</Grid>
</UserControl>
Pasando seguidamente al code-behind de esta página, la primera operación que debemos realizar será durante la carga de la misma, y se basará en rellenar el ComboBox con la lista de países obtenidos de la vista PaisesPedidos. Para ello, crearemos una instancia del contexto de dominio (clase NorthwindContext) con un ámbito que sea accesible a todo el código de la página XAML. A continuación, en el constructor de la página, llamaremos al método Load del contexto de dominio, pasándole como parámetro el método GetPaisesPedidosQuery, también de este mismo contexto, que devuelve las entidades resultantes de ejecutar la vista de la base de datos. Finalmente, asignamos como fuente de datos para el ComboBox, la propiedad PaisesPedidos del contexto de dominio, que contendrá la colección de entidades, una vez que el método NorthwindContext.Load haya finalizado.
//....
using SeleccionComboBox.Web;
namespace SeleccionComboBox
{
public partial class MainPage : UserControl
{
NorthwindContext oNorthwindContext = new NorthwindContext();
public MainPage()
{
InitializeComponent();
oNorthwindContext.Load(oNorthwindContext.GetPaisesPedidosQuery());
this.cboPaises.ItemsSource = oNorthwindContext.PaisesPedidos;
}
}
}
En la siguiente imagen podemos ver el ComboBox en tiempo de ejecución, mostrando la lista de valores obtenidos de la base de datos.

¿Cuándo se completa la carga de entidades en el lado cliente?
Debemos tener en cuenta que la ejecución de NorthwindContext.Load se produce de modo asíncrono (que es el modo de ejecución en el que Silverlight trabaja con servicios), por lo que la asignación a la propiedad cboPaises.ItemsSource, no significa que el control ComboBox pueda mostrar inmediatamente la lista de valores.
Si deseamos saber el momento exacto en el que la operación de carga se ha completado, para así poder empezar a utilizar el ComboBox, debemos utilizar una técnica consistente en obtener, a partir del método DomainContext.Load, el tipo genérico LoadOperation<T> que dicho método devuelve, y que representa a la operación de carga de datos; creando a continuación un manipulador para el evento Completed del objeto LoadOperation. Lo primero que haremos será añadir en el código XAML un TextBlock, que utilizaremos para informar al usuario del momento a partir del cual puede usar el ComboBox.
<!--....-->
<StackPanel Background="LightGreen">
<TextBlock Name="txtAviso" Text="" Margin="5" HorizontalAlignment="Center" />
<ComboBox Name="cboPaises" Width="200" DisplayMemberPath="ShipCountry" Margin="5" />
<!--....-->
A continuación, desde el code-behind de la página, recogeremos el valor devuelto por la llamada al método NorthwindContext.GetPaisesPedidosQuery: el objeto LoadOperation<PaisesPedido>. De este objeto crearemos un manipulador para su evento Completed, el cual, al ser ejecutado, asignará un mensaje de aviso al TextBlock de la interfaz de usuario.
//....
using SeleccionComboBox.Web;
using System.Windows.Ria.Data;
namespace SeleccionComboBox
{
public partial class MainPage : UserControl
{
NorthwindContext oNorthwindContext = new NorthwindContext();
public MainPage()
{
InitializeComponent();
LoadOperation<PaisesPedido> oLoadOperation = oNorthwindContext.Load(oNorthwindContext.GetPaisesPedidosQuery());
oLoadOperation.Completed += new EventHandler(oLoadOperation_Completed);
this.cboPaises.ItemsSource = oLoadOperation.Entities;
}
void oLoadOperation_Completed(object sender, EventArgs e)
{
this.txtAviso.Text = "ComboBox Cargado!!!!";
}
}
}
Observe el lector, que para poder utilizar la clase LoadOperation<T>, debemos declarar en el archivo de código el espacio de nombres System.Windows.Ria.Data. Por otro lado, para asignar al ComboBox los valores a mostrar emplearemos la propiedad LoadOperation<T>.Entities, que será la que contenga la colección entidades una vez completado el proceso de carga.

Finalizamos aquí la segunda parte de este artículo. En la tercera y última entrega explicaremos la forma de ordenar la lista de elementos del ComboBox, así como la carga de datos en el DataGrid con los registros de la tabla Orders. El ejemplo completo está disponible en los siguientes enlaces: C# y VB.
Un saludo.
El desarrollo de una aplicación Silverlight, además de las ventajas que ofrece en cuanto al tratamiento de video y gráficos se refiere, precisará, con toda probabilidad, de la implementación de una cierta cantidad de operaciones de manipulación contra un origen de datos, pongamos como ejemplo una base de datos SQL Server.
Los retos del desarrollo bajo el modelo RIA
Si aplicamos a nuestro desarrollo un enfoque basado en el modelo RIA, nos encontraremos con que tendremos que realizar un importante esfuerzo de codificación en la creación de un conjunto de servicios y clases, las cuales se ocupen del trasiego de datos entre la interfaz de usuario Silverlight y la fuente de datos situada en el lado servidor del diseño de nuestra aplicación.
Como factor adicional, debemos tener en cuenta que dadas las características de funcionamiento de Silverlight, todos los elementos que forman parte de esta maquinaria deberán de funcionar de modo asíncrono, lo cual introduce un nivel añadido de complejidad al desarrollo de la aplicación.
La llegada de .NET RIA Services a la arena del desarrollo Silverlight, sirve para mitigar la carga de trabajo que el desarrollador debe realizar, ya que se trata de un producto que se ocupa de generar automáticamente las clases y servicios necesarios para estas operaciones de mantenimiento de datos, a partir del modelo de datos que hayamos definido en nuestro proyecto.
Aplicando .NET RIA Services
El mejor modo de ilustrar el uso de .NET RIA Services pasa por realizar un proyecto de ejemplo, en el que paso a paso, podamos ver las fases principales de su creación. Antes de comenzar, resulta necesario resaltar que para trabajar con .NET RIA Services debemos tener instalado Silverlight 3.
Pongamos como caso que necesitamos desarrollar una página Silverlight, en la que tenemos que visualizar, dentro de un DataGrid, algunas de las columnas de la tabla Orders (perteneciente a la base de datos Northwind), con la particularidad de que en lugar de mostrar todos los registros de la tabla, filtraremos por el campo ShipCountry, visualizando solamente aquellos que tengan un mismo valor para este campo, que previamente el usuario habrá seleccionado mediante un ComboBox, que contendrá la lista de posibles valores.
A la mencionada lista de valores del campo ShipCountry accederemos mediante una vista, que crearemos empleando la siguiente sentencia SQL.
CREATE VIEW PaisesPedidos
AS
SELECT DISTINCT ShipCountry FROM Orders
Una vez creada esta vista dentro de la base de datos Northwind, abriremos Visual Studio 2008 y crearemos un nuevo proyecto de tipo SilverlightApplication, al que daremos el nombre SeleccionComboBox, marcando además, la casilla Enable .NET RIA Services en el cuadro inicial de creación del proyecto.

Definición del modelo de datos de la aplicación
El primer paso a dar consistirá en la creación del modelo de datos de la aplicación, para lo cual nos situaremos en el proyecto Web (SeleccionComboBox.Web) de la solución recién creada, seleccionaremos la opción de menú Project > Add New Item, y dentro de la categoría Data añadiremos un nuevo elemento de tipo LINQ to SQL Classes, al que daremos el nombre Northwind.dbml.

A continuación, desde la ventana Server Explorer, crearemos una nueva conexión contra la base de datos Northwind, completando el diálogo de creación de conexión que se muestra al hacer clic en el icono Connect to Database

Como resultado, la ventana Server Explorer mostrará la conexión recién creada a partir del nodo Data Connections. Desplegando los nodos correspondientes a las tablas y vistas de esta base de datos, arrastraremos hasta el área de diseño del modelo de datos la tabla Orders y la vista PaisesPedidos.

Compilando la solución se generará la clase NorthwindDataContext, que representa al contexto/modelo de datos actualmente reflejado en el diseñador; y las clases Order y PaisesPedido, que identifican respectivamente a la tabla y vista seleccionadas de la base de datos.
DomainService. Creando el servicio de dominio
El siguiente paso consiste en crear el servicio de dominio para la aplicación, por lo que, nuevamente, desde el proyecto Web de la solución, seleccionaremos la opción de menú Project > Add New Item, y esta vez, desde la categoría Web, elegiremos un elemento de tipo Domain Service Class, al que daremos el nombre NorthwindService.cs.
Inmediatamente a continuación se abrirá un cuadro de diálogo para configurar el servicio de dominio, dentro del cual podremos elegir el contexto de datos (modelo de datos) con el que deseamos trabajar. En este ejemplo solamente disponemos del contexto de datos que acabamos de crear: NorthwindDataContext, que se mostrará de manera predeterminada.
Justo debajo del contexto de datos se muestran las entidades que lo componen, junto a una casilla para marcar aquellas con las que vamos a trabajar desde el lado cliente de la aplicación. Para que el código necesario sea generado en la parte cliente (proyecto Silverlight de la solución) deberemos marcar la casilla Enable client access.
Si queremos realizar operaciones de edición sobre las entidades, también podemos marcar la casilla Enable editing, aunque para la aplicación que estamos desarrollando no será necesario, ya que se trata de crear un ejemplo sencillo, basado exclusivamente en operaciones de consulta.

Tras aceptar este cuadro de diálogo generaremos la solución, que para el lado servidor de la aplicación (proyecto Web) tendrá como resultado la generación, por parte de .NET RIA Services, de la clase NorthwindService, derivada de LinqToSqlDomainService, que representa al servicio de dominio. Esta última clase hereda a su vez de DomainService, clase base que contiene el funcionamiento esencial, común a todos los servicios de dominio.
La clase NorthwindService contendrá un método de consulta por cada una de las entidades existentes en el contexto de datos. La convención de notación seguida por .NET RIA Services para asignar los nombres a estos métodos se basa en utilizar el prefijo Get, seguido del nombre de la entidad. Cada uno de estos métodos devolverá una colección de entidades que representa a los registros de la tabla o vista de la base de datos, según el método que hayamos utilizado.
[EnableClientAccess()]
public class NorthwindService : LinqToSqlDomainService<NorthwindDataContext>
public IQueryable<Order> GetOrders()
{
return this.Context.Orders;
}
public IQueryable<PaisesPedido> GetPaisesPedidos()
{
return this.Context.PaisesPedidos;
}
Y llegados a este punto damos por concluida esta primera parte del artículo. En la próxima entrega abordaremos el código generado por .NET RIA Services a partir del servicio de dominio para la capa cliente de la aplicación y la creación de la interfaz de usuario. El ejemplo completo está disponible en los siguientes enlaces: C# y VB.
Un saludo.
Si acabamos de instalar .NET RIA Services (CTP Mayo 2009) para disponernos a realizar nuestras primeras pruebas con esta tecnología de acceso a datos, pudiera ser que a la hora de intentar añadir al proyecto Web de la solución un servicio de dominio (DomainService), nos encontremos con que la plantilla correspondiente a este elemento, Domain Service Class, no está disponible en nuestro IDE de Visual Studio 2008.

En el caso de que esto nos ocurra, es posible que el origen del problema se encuentre motivado por el idioma configurado dentro del entorno de desarrollo. Si tenemos una versión de Windows en español, y dentro de la configuración internacional de Visual Studio 2008 (menú Herramientas > Opciones > Entorno > Configuración internacional), el apartado Idioma tiene el valor "Igual que en Microsoft Windows" o "español", es probable que este sea el motivo de tal comportamiento.

Para intentar solucionarlo, en la lista desplegable Idioma cambiaremos el valor a "English", aceptaremos el diálogo, y reiniciaremos Visual Studio 2008. Abriendo nuevamente nuestro proyecto, al intentar agregar un nuevo Domain Service Class, ya debería de aparecer el icono correspondiente en el diálogo Add New Item.

Pudiera ocurrir que el elemento "Domain Service Class" del diálogo Add New Item aparezca solamente con el texto descriptivo del elemento, sin el dibujo del icono, aunque esto no supondrá un problema, ya que podremos seleccionarlo igualmente para añadirlo al proyecto.
Espero que este truco resulte de ayuda a cualquiera que se pueda encontrar con este mismo problema.
Un saludo.
El planteamiento del problema
Durante el desarrollo de una aplicación ASP.NET, a la hora de diseñar la interfaz de usuario, podemos encontrarnos ante un escenario en el que se requiera que la navegación a ciertas páginas se centralice en un único punto de la aplicación.
En situaciones como esta, hacer uso de una página maestra resulta una excelente solución, ya que nos evita tener que duplicar el código en todas aquellas páginas que necesiten disponer de la mencionada funcionalidad de navegación. Adicionalmente, tendremos que incluir en dicha página maestra un control o controles que realicen la operación de navegación.
El control que en esta ocasión utilizaremos para el ejemplo a desarrollar será el TreeView de Silverlight 2, incluido en el Silverlight 2 Toolkit de marzo de 2009, cuyos pasos de descarga e instalación explicamos seguidamente.
Descarga e instalación del Silverlight 2 Toolkit
En caso de no disponer del Silverlight 2 Toolkit, podemos descargarlo en el siguiente enlace. Una vez completada la descarga, ejecutaremos su instalador, que a través de unos sencillos pasos, como vemos en la siguiente imagen, instalará el conjunto de controles que extienden las funcionalidades de Silverlight.

Finalizado el trabajo del instalador, en Visual Studio 2008 aparecerán, dentro de la caja de herramientas, los nuevos controles que forman parte del Toolkit (TreeView incluido, naturalmente).

Creando el proyecto Web y la página maestra
Como hemos indicado al comienzo del artículo, vamos a desarrollar una aplicación Web que estará compuesta por varios WebForm, los cuales harán uso de una página maestra, que albergará un control TreeView de Silverlight. El TreeView contendrá un conjunto de opciones (elementos TreeViewItem) en los que al hacer clic se producirá la redirección a la página correspondiente.
Comenzaremos pues, el desarrollo de nuestro ejemplo, iniciando Visual Studio 2008 y creando un nuevo proyecto Web con el nombre NavMasterPageTreeView, al que añadiremos una MasterPage con el nombre Maestra.Master. Posteriormente añadiremos el proyecto Silverlight a la solución, pero por ahora nos centraremos en los formularios Web.

Esta página maestra, como vemos en su código de presentación, estará compuesta por una tabla con dos celdas; una de ellas corresponderá a la zona que reservaremos para el contenido Silverlight, y la otra para los formularios Web a los que naveguemos dentro de la aplicación.
<%@ Master Language="C#" AutoEventWireup="true" CodeBehind="Maestra.master.cs" Inherits="NavMasterPageTreeView.Maestra" %>
<%@ Register Assembly="System.Web.Silverlight" Namespace="System.Web.UI.SilverlightControls"
TagPrefix="asp" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title></title>
<asp:ContentPlaceHolder ID="head" runat="server">
</asp:ContentPlaceHolder>
</head>
<body>
<form id="form1" runat="server">
<asp:ScriptManager ID="ScriptManager1" runat="server">
</asp:ScriptManager>
<asp:UpdatePanel ID="UpdatePanel1" runat="server">
<ContentTemplate>
<div>
<table border="7" width="100%">
<tr>
<td width="20%" align="center">
<%Espacio para contenido Silverlight%>
</td>
<td width="80%">
<asp:ContentPlaceHolder ID="ContentPlaceHolder1" runat="server">
<%Espacio que ocuparán los WebForms%>
</asp:ContentPlaceHolder>
</td>
</tr>
</table>
</div>
</ContentTemplate>
</asp:UpdatePanel>
</form>
</body>
</html>
Los formularios de contenido
A continuación agregaremos al proyecto los diferentes formularios Web que utilizaremos para efectuar las operaciones de navegación. Dado que su contenido no representa un aspecto relevante en el desarrollo del ejemplo, estarán compuestos simplemente por un control Label a modo de título de la página y un TextBox, al que en el evento Load, asignaremos como valor la hora en que se produjo la carga de la página; esto nos servirá para demostrar, más adelante, un curioso efecto que se produce al cargar el WebForm desde el TreeView.
En total serán cuatro los formularios que crearemos, los cuales vemos remarcados en la siguiente imagen del Explorador de soluciones de Visual Studio.

Seguidamente podemos ver tanto el código de presentación como el code behind de uno de estos WebForm, obviamos el resto ya que comparten las mismas características.
<%@ Page Title="" Language="C#" MasterPageFile="~/Maestra.Master" AutoEventWireup="true"
CodeBehind="Facturas.aspx.cs" Inherits="NavMasterPageTreeView.Facturas" %>
<asp:Content ID="Content1" ContentPlaceHolderID="head" runat="server">
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="ContentPlaceHolder1" runat="server">
<table border="2" style="margin: 10px">
<tr>
<td>
<asp:Label ID="Label1" runat="server" Text="FACTURAS"></asp:Label>
</td>
<td>
<asp:TextBox ID="txtHora" runat="server"></asp:TextBox>
</td>
</tr>
</table>
</asp:Content>
public partial class Facturas : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
this.txtHora.Text = DateTime.Now.ToString("HH:mm:ss");
}
}
Creación del contenido Silverlight
El siguiente punto a desarrollar consiste en añadir el control TreeView a la aplicación y dotarle de la funcionalidad necesaria. Para ello, agregaremos a la solución con la que estamos trabajando un nuevo proyecto de tipo Silverlight con el nombre Opciones.

Cuando el asistente muestre el cuadro de diálogo para configurar la creación de este proyecto, desmarcaremos la casilla para la creación automática de la página de pruebas para el contenido Silverlight.

El motivo de no emplear una página de prueba reside en que nuestra página de testeo para el contenido Silverlight será la propia master page, tal y como indicábamos en un apartado anterior.
Situándonos en el editor de código XAML, diseñaremos una página que contenga un control TreeView, con varios elementos TreeViewItem dispuestos en dos niveles, que nos permitirán organizar por unos hipotéticos tipos de funcionalidad, las páginas de nuestra aplicación. No obstante, nos interesa visualizar en todo momento el árbol completo de opciones, por lo que en los TreeViewItem de nivel superior, asignamos el valor True a su propiedad IsExpanded. Todo ello lo vemos en el siguiente bloque de código.
<UserControl xmlns:controls="clr-namespace:Microsoft.Windows.Controls;assembly=Microsoft.Windows.Controls" x:Class="Opciones.Page"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="140" Height="170">
<Grid x:Name="LayoutRoot" Background="White">
<StackPanel>
<controls:TreeView x:Name="tvOpciones" Width="130" Height="160" Margin="5">
<controls:TreeViewItem Header="Gestión" IsExpanded="True">
<controls:TreeViewItem x:Name="tviFacturas" Header="Facturas"></controls:TreeViewItem>
<controls:TreeViewItem x:Name="tviListados" Header="Listados"></controls:TreeViewItem>
</controls:TreeViewItem>
<controls:TreeViewItem Header="Contabilidad" IsExpanded="True">
<controls:TreeViewItem x:Name="tviApuntes" Header="Apuntes"></controls:TreeViewItem>
<controls:TreeViewItem x:Name="tviBalances" Header="Balances"></controls:TreeViewItem>
</controls:TreeViewItem>
</controls:TreeView>
</StackPanel>
</Grid>
</UserControl>
El resultado visual quedará de la forma mostrada en la siguiente imagen.
Al hacer clic en cada uno de los TreeViewItem de nivel inferior, el resultado que pretendemos conseguir es la carga en el navegador del WebForm correspondiente; por lo tanto, como siguiente paso, debemos dotar a esta interfaz que acabamos de diseñar de la funcionalidad necesaria.
Abriendo el editor de code behind de la página XAML, escribiremos un manipulador para el evento Selected de los objetos TreeViewItem. Puesto que la operación a realizar es la misma en todos los casos, y únicamente cambiará la página a la que debemos navegar, solamente será necesario un único manipulador de evento, ya que tomaremos el valor de la propiedad Header del TreeViewItem que recibe el evento en su parámetro sender; concatenaremos a dicho valor la cadena ".aspx", y realizaremos la navegación mediante el método Navigate del objeto HtmlPage.Window. Dentro del constructor de la página Silverlight, efectuaremos la asociación entre el evento de los objetos TreeViewItem y el manipulador. Todo ello lo vemos en el siguiente bloque de código.
using Microsoft.Windows.Controls;
using System.Windows.Browser;
//....
public partial class Page : UserControl
{
public Page()
{
//....
this.tviFacturas.Selected += new RoutedEventHandler(tvi_Selected);
this.tviListados.Selected += new RoutedEventHandler(tvi_Selected);
this.tviApuntes.Selected += new RoutedEventHandler(tvi_Selected);
this.tviBalances.Selected += new RoutedEventHandler(tvi_Selected);
}
//....
void tvi_Selected(object sender, RoutedEventArgs e)
{
TreeViewItem tviOpcion = sender as TreeViewItem;
string sNombreItem = tviOpcion.Header.ToString();
HtmlPage.Window.Navigate(new Uri(sNombreItem + ".aspx", UriKind.Relative));
}
}
Para completar la operación, en la master page del proyecto Web agregaremos un control Silverlight con el nombre slOpciones, cuya propiedad Source apunte al archivo xap del proyecto Silverlight que hemos desarrollado.
<form id="form1" runat="server">
<asp:ScriptManager ID="ScriptManager1" runat="server">
</asp:ScriptManager>
<asp:UpdatePanel ID="UpdatePanel1" runat="server">
<ContentTemplate>
<div>
<table border="7" width="100%">
<tr>
<td width="20%" align="center">
<asp:Silverlight ID="slOpciones" runat="server"
Width="140px" Height="170px"
Source="~/ClientBin/Opciones.xap" />
</td>
<td width="80%">
<asp:ContentPlaceHolder ID="ContentPlaceHolder1" runat="server">
</asp:ContentPlaceHolder>
</td>
</tr>
</table>
</div>
</ContentTemplate>
</asp:UpdatePanel>
</form>
A continuación observamos el resultado en tiempo de ejecución.

El problema con el método HtmlPage.Window.Navigate
Sin embargo, como no podía ser de otro modo, y para hacer más interesante el ejemplo, ahora nos encontramos con un "pequeño" inconveniente, motivado por la mecánica de navegación que hemos implementado mediante el control TreeView.
El problema consiste en que después de haber cargado por primera vez uno de los formularios de la aplicación, haciendo clic en el elemento TreeViewItem correspondiente; en las sucesivas ocasiones en que volvamos a hacer clic en ese mismo TreeViewItem, el WebForm seleccionado no se cargará nuevamente, sino que se utilizará la versión cacheada de dicha página. Este comportamiento podemos comprobarlo muy fácilmente poniendo un punto de ruptura en el evento Load del WebForm, aunque en nuestro ejemplo no sería necesario porque observando el TextBox que contiene cada página, donde se muestra la hora de carga de la misma, nos percataremos de que en las siguientes ejecuciones a partir de la primera, la hora no se actualiza.

Una forma de solucionar esta contrariedad, de modo que se fuerce la carga de la página, consiste en utilizar el método HtmlPage.Window.Eval, al que pasaremos como parámetro una cadena conteniendo una sentencia en JavaScript que efectúe este cometido, tal y como vemos en el siguiente bloque de código.
void tvi_Selected(object sender, RoutedEventArgs e)
{
TreeViewItem tviOpcion = sender as TreeViewItem;
string sNombreItem = tviOpcion.Header.ToString();
HtmlPage.Window.Eval("window.location = '" + sNombreItem + ".aspx ';");
}
En el siguiente post perteneciente al blog de Kevin Hazzard, se comenta también este comportamiento.
Resaltando visualmente el TreeViewItem seleccionado
Otro aspecto que también puede resultar problemático radica en el hecho de que al hacer clic en uno de los elementos del TreeView, no queda constancia visual del elemento seleccionado, ya que la navegación a una nueva página produce una recarga completa, contenido Silverlight incluido.
Lo deseable en este caso sería que el TreeViewItem pulsado fuera informado de la página que ha sido cargada en el navegador (página actual en ejecución), para que de alguna manera pudiera quedar remarcado visualmente
La técnica que vamos a seguir en nuestro ejemplo para conseguir esta funcionalidad pasa por hacer uso, desde el code behind de la master page, de la propiedad InitParameters perteneciente al control Silverlight alojado en esta página, lo que nos permitirá enviar, desde el lado servidor, información al contenido Silverlight.
Para asignar valores a InitParameters deberemos utilizar una cadena compuesta por pares de identificador=valor separados por coma, en un formato como el que vemos a continuación.
ControlSilverlight.InitParameters = "Identificador01=Valor01,Identificador02=Valor02...";
Esta operación la realizaremos en el evento Load de la página maestra, como vemos en el siguiente bloque de código. Para obtener la página que acaba de ser cargada recurriremos a la propiedad PhysicalPath del objeto Request.
public partial class Maestra : System.Web.UI.MasterPage
{
protected void Page_Load(object sender, EventArgs e)
{
string sPagina = Path.GetFileName(this.Request.PhysicalPath);
this.slOpciones.InitParameters = "PaginaActual=" + sPagina.Replace(".aspx", string.Empty);
}
}
La información asignada a InitParameters, será recogida en el proyecto Silverlight por el objeto Application que se halla en el código del archivo App.xaml.cs (en realidad se trata de una clase con el nombre App, que deriva de Application), más concretamente en el evento Startup de este objeto, que se desencadena al comienzo de la ejecución del contenido Silverlight.
El evento Startup recibe un parámetro de tipo StartupEventArgs, cuya propiedad InitParameters, de tipo IDictionary, utilizaremos para recuperar el valor de parámetro PaginaActual, que anteriormente establecimos en la página maestra. Dado que este valor necesitaremos utilizarlo desde el código de marcado de la página Silverlight (archivo Page.xaml), crearemos una propiedad en la clase App con el nombre PaginaActual, para poder manipularlo más fácilmente, como veremos brevemente.
public partial class App : Application
{
string msPaginaActual;
public string PaginaActual
{
get { return msPaginaActual; }
}
//....
private void Application_Startup(object sender, StartupEventArgs e)
{
this.RootVisual = new Page();
msPaginaActual = e.InitParams["PaginaActual"];
}
//....
}
Finalmente, en el code behind de la página Silverlight, escribiremos un manipulador para el evento Loaded, dentro de cuyo código, a partir de la propiedad estática App.Current, accederemos a la instancia del objeto App, y por ende a nuestra propiedad PaginaActual, que en función de su valor, nos servirá para resaltar el elemento TreeViewItem pertinente, utilizando su propiedad Background.
public partial class Page : UserControl
{
public Page()
{
InitializeComponent();
this.Loaded += new RoutedEventHandler(Page_Loaded);
//....
}
void Page_Loaded(object sender, RoutedEventArgs e)
{
SolidColorBrush scbItemSeleccionado = new SolidColorBrush(Colors.Cyan);
App oAplicacion = App.Current as App;
switch (oAplicacion.PaginaActual)
{
case "Facturas":
this.tviFacturas.Background = scbItemSeleccionado;
break;
case "Listados":
this.tviListados.Background = scbItemSeleccionado;
break;
case "Apuntes":
this.tviApuntes.Background = scbItemSeleccionado;
break;
case "Balances":
this.tviBalances.Background = scbItemSeleccionado;
break;
}
}
}
La siguiente imagen muestra el resultado de esta característica que acabamos de añadir.

Conclusiones
El empleo de una interfaz basada en una master page, que contenga elementos Silverlight tales como el control TreeView que hemos utilizado en el ejemplo, puede resultar de gran utilidad en el momento de desarrollar un mecanismo de navegación hacia las páginas Web que componen nuestra aplicación. Los ejemplos de código están disponibles en los siguientes enlaces: C# y VB. Espero que os pueda resultar de ayuda.
Un saludo.
Marino Posadas acaba de publicar "Programación en Silverlight 2.0", editado por Netalia, un libro que contiene todo lo necesario para introducirnos en el desarrollo de aplicaciones RIA, utilizando este entorno de trabajo desarrollado por Microsoft.
A lo largo de la obra se cubren todos aquellos aspectos que necesitaremos para abordar y llevar a buen puerto un proyecto basado en Silverlight 2.0.
Comenzando por la descripción de la arquitectura con la que se ha diseñado Silverlight, se tratan todos aquellos temas que conforman los pilares fundamentales que debemos tener en cuenta a la hora de afrontar nuestros desarrollos con esta tecnología. Desde el lenguaje XAML hasta la distribución de aplicaciones, pasando por los controles disponibles para construir nuestra interfaz de usuario, el sistema de animación, el Visual State Manager, acceso a datos, etc.; todo ello entroncado con la perspectiva de Visual Studio 2008 como herramienta principal para la construcción de aplicaciones, y Expression Blend para el diseño de la interfaz de usuario.
Cuenta además, como baza importante, el hecho de ser una obra escrita en castellano, por lo que resultará muy interesante para todos aquellos que no hagan buenas "migas" con el idioma del Bardo de Stratford-upon-Avon 8-).
En definitiva, un libro totalmente recomendable del que podréis obtener en breve más información en la web de dotNetManía.
Un saludo.
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.
MessageBox. Un nuevo modo de mostrar mensajes
Una de las novedades que incorpora la Release Candidate de Silverlight 2 consiste en la posibilidad de utilizar la nueva clase MessageBox para crear un sencillo cuadro de diálogo que nos permita mostrar un mensaje o aviso al usuario.
El método Show de esta clase es el encargado de la visualización de los mensajes, disponiendo de dos sobrecargas que nos permiten presentar el aviso al usuario con ligeras variaciones.
Un mensaje sencillo
Mediante la primera sobrecarga visualizaremos la caja de mensaje con un texto simple; para ello, vamos a crear un proyecto en Visual Studio con el siguiente código en su página XAML.
<UserControl x:Class="SL2RCMessageBox.Page"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="400" Height="300">
<Grid x:Name="LayoutRoot" Background="White">
<StackPanel x:Name="pnlPrincipal" Background="Beige">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="5">
<TextBlock Margin="5" TextDecorations="Underline" FontSize="16">
Clase MessageBox
</TextBlock>
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left" Margin="5">
<TextBlock Margin="5">Texto del mensaje</TextBlock>
<TextBox x:Name="txtMensaje" Width="200" Height="25" />
</StackPanel>
<StackPanel Margin="5" >
<Button x:Name="btnMensajeSimple"
Width="150" Height="25"
Content="Mensaje simple"
Click="btnMensajeSimple_Click" />
</StackPanel>
</StackPanel>
</Grid>
</UserControl>
Para el evento Click del botón btnMensajeSimple escribiremos el siguiente bloque de código, en el que utilizaremos la clase MessageBox para mostrar un mensaje conteniendo el texto que hayamos escrito en el TextBox txtMensaje.
private void btnMensajeSimple_Click(object sender, RoutedEventArgs e)
{
MessageBox.Show(this.txtMensaje.Text);
}
El resultado en tiempo de ejecución lo podemos ver en la siguiente imagen.

Si estamos acostumbrados al uso de los cuadros de mensaje desde JavaScript utilizando la función alert, como el de la siguiente imagen, nos habremos percatado inmediatamente de ciertas diferencias.

Utilizando Internet Explorer como navegador, la ventana obtenida mediante la función alert muestra un título, y al lado del texto se visualiza un icono, cosa que no ocurre con MessageBox.Show, lo cual en ciertos casos es de agradecer, ya que puede que no queramos o no nos guste que nuestros mensajes tengan dicho título e icono en la ventana.
Completando el mensaje con un título y selección de respuesta
No obstante, si el hecho de poder utilizar un título personalizado para la ventana de mensaje nos resulta útil, la otra sobrecarga de Show nos permite disponer de dicha característica, así como de la posibilidad de visualizar los botones Aceptar y Cancelar, de forma que el usuario pueda seleccionar una respuesta ante una pregunta que le formulemos mediante el mensaje.
A fin de ilustrar esta otra forma de mostrar el mensaje, añadiremos un nuevo TextBox a la página XAML para poder escribir el título, y otro botón para llamar a esta sobrecarga de Show.
<!--....-->
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left" Margin="5">
<TextBlock Margin="5">Título del mensaje</TextBlock>
<TextBox x:Name="txtTitulo" Width="200" Height="25" />
</StackPanel>
<StackPanel Margin="5" >
<Button x:Name="btnMensajeTitulo"
Width="150" Height="25"
Content="Mensaje con título"
Click="btnMensajeTitulo_Click" />
</StackPanel>
<!--....-->
La funcionalidad que vamos a implementar consiste en lo siguiente: cuando el usuario pulse uno de los botones del MessageBox, cambiaremos el color de fondo de las cajas de texto de la página. Mediante la enumeración MessageBoxButton especificamos los botones a mostrar en el cuadro de diálogo de mensaje; mientras que el método Show devuelve un tipo MessageBoxResult cuyo valor consultaremos para comprobar el botón pulsado y actuar en consecuencia.
private void btnMensajeTitulo_Click(object sender, RoutedEventArgs e)
{
MessageBoxResult oResultado = MessageBox.Show(this.txtMensaje.Text,
this.txtTitulo.Text, MessageBoxButton.OKCancel);
switch (oResultado)
{
case MessageBoxResult.OK:
this.txtMensaje.Background = new SolidColorBrush(Colors.Cyan);
this.txtTitulo.Background = new SolidColorBrush(Colors.Cyan);
break;
case MessageBoxResult.Cancel:
this.txtMensaje.Background = new SolidColorBrush(Colors.White);
this.txtTitulo.Background = new SolidColorBrush(Colors.White);
break;
}
}
La siguiente imagen muestra el efecto obtenido.

La clase HtmlWindow. Puerta de acceso a los mensajes en JavaScript
La clase HtmlPage, situada en el espacio de nombres System.Windows.Browser, dispone de una propiedad llamada Window, que contiene un objeto de tipo HtmlWindow, el cual nos proporciona acceso, mediante los métodos Alert y Confirm, a las funciones JavaScript del mismo nombre, con las cuales conseguiremos la misma funcionalidad que la ya explicada anteriormente con la clase MessageBox, si bien el uso de MessageBox proporciona un estilo de codificación más "a la C#", por lo que pensamos que el programador se encontrará más cómodo utilizando esta última clase.
No obstante, si por cualquier motivo no queremos emplear MessageBox, vamos a añadir a nuestro ejemplo un par de botones adicionales para demostrar el funcionamiento de estos métodos.
<!--....-->
<StackPanel x:Name="pnlHtmlPageWindow"
Background="Aquamarine"
Margin="5,20,5,5"
Width="380">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="5">
<TextBlock Margin="5" TextDecorations="Underline" FontSize="16">HtmlPage.Window</TextBlock>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="5" HorizontalAlignment="Center">
<Button x:Name="btnAlert"
Width="100" Height="25"
Margin="0,0,20,0"
Content="Alert"
Click="btnAlert_Click" />
<Button x:Name="btnConfirm"
Width="100" Height="25"
Margin="0,0,20,0"
Content="Confirm"
Click="btnConfirm_Click" />
</StackPanel>
</StackPanel>
<!--....-->
Tanto Alert como Confirm reciben un parámetro de tipo string que será el texto mostrado por la caja de mensaje, con la diferencia de que Confirm devuelve un tipo bool, mediante el que podremos averiguar el botón pulsado por el usuario, para realizar la acción correspondiente.
private void btnAlert_Click(object sender, RoutedEventArgs e)
{
HtmlPage.Window.Alert(this.txtMensaje.Text);
}
private void btnConfirm_Click(object sender, RoutedEventArgs e)
{
if (HtmlPage.Window.Confirm(this.txtMensaje.Text))
{
this.pnlHtmlPageWindow.Background = new SolidColorBrush(Colors.Magenta);
}
else
{
this.pnlHtmlPageWindow.Background = new SolidColorBrush(Colors.Gray);
}
}
El mensaje obtenido mediante HtmlPage.Window.Alert es el siguiente.

En cuanto a HtmlPage.Window.Confirm, la siguiente imagen muestra el resultado.

HtmlWindow.Invoke. Invocando a funciones JavaScript desde Silverlight
Y para finalizar este artículo vamos a presentar a Invoke, otro de los métodos de la clase HtmlWindow, al que pasaremos como primer parámetro una cadena con el nombre de la función JavaScript a ejecutar, mientras que como segundo parámetro podemos utilizar otra cadena o bien un array que será pasado como parámetro a la función. Este método devuelve un tipo object, que deberemos convertir a un tipo más adecuado, según el valor que devuelva la función JavaScript que hayamos llamado.
Para ilustrar el uso de este método, en primer lugar añadiremos una función JavaScript a la página aspx que aloja el contenido Silverlight de la aplicación.
<script type="text/javascript">
function MensajeJS(sTextoMensaje) {
return confirm(sTextoMensaje);
}
</script>
Seguidamente añadiremos otro control Button a la página XAML, escribiendo a continuación el código correspondiente a su evento Click, en el que mediante el método Invoke ejecutaremos la función MensajeJS(), que abrirá un cuadro de mensaje de confirmación. Al obtener la respuesta de la ejecución del código JavaScript, cambiaremos el color del panel que contiene estos botones según el botón pulsado.
<!--....-->
<Button x:Name="btnInvoke"
Width="100" Height="25"
Content="Invoke"
Click="btnInvoke_Click" />
<!--....-->
private void btnInvoke_Click(object sender, RoutedEventArgs e)
{
bool bResultado = (bool)HtmlPage.Window.Invoke("MensajeJS", this.txtMensaje.Text);
if (bResultado)
{
this.pnlPrincipal.Background = new SolidColorBrush(Colors.Orange);
}
else
{
this.pnlPrincipal.Background = new SolidColorBrush(Colors.Purple);
}
}
La siguiente imagen muestra esta parte del ejemplo en funcionamiento.

Y esto ha sido todo en este artículo, en el que hemos realizado un repaso de las distintas posibilidades que ofrece Silverlight en cuanto a la creación de cuadros de mensaje se refiere. El código de ejemplo puede descargarse en los siguientes enlaces: C# y VB.
Espero que os resulte de utilidad.
Un saludo.
Si trabajando con alguna de las betas de Silverlight hemos creado una página que contuviera un control TextBox, a buen seguro que nos hemos llevado un disgusto cuando al pulsar alguna de las teclas especiales (@ , | , # , \ , signos de acentuación, etc.), comprobábamos que este control hacía caso omiso de nuestras pulsaciones -por muy fuerte que aporreáramos el teclado- no mostrando el carácter correspondiente.
Este ha sido un tema ya comentado en los foros de Silverlight, donde se proponían algunas soluciones y trucos para salir del paso ante el problema.
Sin embargo, desde la reciente liberación de la Release Candidate de Silverlight 2, parece que este ha sido uno de los inconvenientes solucionados por el equipo de desarrollo del producto, ya que nada más hacerse pública la noticia, me descargué los bits de esta versión -próxima a la definitiva-, y en un pequeño proyecto de prueba observé que el TextBox ya posibilita teclear todos estos caracteres que anteriormente no permitía.
Lo cual quiere decir que ya no necesitaremos capturar los eventos de teclado de este control para añadir manualmente esta funcionalidad.
Un saludo.
Nota. A efectos de poder seguir adecuadamente el contenido de este artículo, los ejemplos de código fuente del libro que se menciona se encuentran en este enlace para C# y en este enlace para VB.
Un interesante control de selección de fecha
Hace unos cuantos meses leí el estupendo libro "Applications = Code + Markup" de Charles Petzold, una obra totalmente recomendable para todos aquellos que quieran iniciarse y profundizar en el nuevo paradigma de aplicaciones de escritorio que representa Windows Presentation Foundation (WPF).
Al llegar al capítulo 25 encontré un ejemplo muy interesante, en el cual se desarrollaba un control de selección de fecha denominado DatePicker, que al ser ejecutado presenta el siguiente aspecto.

Este control cumple su cometido a la perfección, aunque debemos destacar un inconveniente que el propio Petzold menciona en su obra, más concretamente en la página 696, y que consiste en que el primer día de la semana que se muestra es el domingo, lo cual representa un problema si lo utilizamos en países donde el primer día de la semana es lunes, tal y como ocurre en España.
Continúa Petzold indicando en su libro, que una posible solución a este problema pasaría por acceder, desde el codebehind de la página XAML, a la propiedad FirstDayOfWeek de la clase DateTimeFormatInfo, para asignar manualmente el valor correcto. Sin embargo, si accedemos a dicha propiedad, veremos que ya tiene establecido el lunes como primer día de la semana -aunque lo muestre en inglés-, por lo que parece ser que la raíz del problema no se encuentra en esta propiedad.

Observando el código XAML del control DatePicker, vemos que para mostrar las columnas que contienen como título los nombres abreviados de los días de la semana utiliza un control StatusBar, cuyo contenido rellena asignando a su propiedad ItemsSource un enlace a datos que apunta a la propiedad AbbreviatedDayNames del objeto DateTimeFormatInfo, situado en DateTimeFormatInfo.CurrentInfo, como podemos apreciar en el siguiente bloque de código.
<!-- =============================================
DatePicker.xaml (c) 2006 by Charles Petzold
============================================= -->
<UserControl xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:g="clr-namespace:System.Globalization;assembly=mscorlib"
x:Class="Petzold.CreateDatePicker.DatePicker">
<!--....-->
<!-- StatusBar with UniformGrid for days of the week. -->
<StatusBar Grid.Row="1"
ItemsSource="{Binding Source={x:Static
g:DateTimeFormatInfo.CurrentInfo},
Path=AbbreviatedDayNames}">
<!--....-->
Si en tiempo de ejecución observamos los elementos de la colección contenida en la propiedad AbbreviatedDayNames, comprobaremos que el primer elemento es "dom", siendo esta la causa de que el control visualice los nombres de los días en un orden que no se ajusta al utilizado en nuestro calendario.

Retocando la colección AbbreviatedDayNames
Para solventar este inconveniente podemos recurrir al empleo de un pequeño truco dentro del constructor del control DatePicker, consistente en obtener el objeto CultureInfo de la hebra de ejecución en curso de la aplicación, y pasar a una variable el contenido de la propiedad AbbreviatedDayNames. A continuación añadimos un nuevo elemento al final de esta colección, en el que depositamos el mismo valor existente dentro del primer elemento, eliminando por último el primer elemento, con lo que ya tenemos la colección que utilizará el StatusBar rellena de los valores en el orden que necesitamos.
//-------------------------------------------
// DatePicker.cs (c) 2006 by Charles Petzold
//-------------------------------------------
//....
// código añadido ---------------
using System.Threading;
using System.Collections.Generic;
// ----------------------------------
namespace Petzold.CreateDatePicker
{
public partial class DatePicker
{
//....
// Constructor.
public DatePicker()
{
//....
// código añadido -------------------------------------
CultureInfo oCultureInfoActual = (CultureInfo)Thread.CurrentThread.CurrentCulture;
List<string> lstDiasAbreviados = new List<string>(oCultureInfoActual.DateTimeFormat.AbbreviatedDayNames);
lstDiasAbreviados.Add(lstDiasAbreviados[0]);
lstDiasAbreviados.RemoveAt(0);
// ----------------------------------------------------
//....
Empleando el depurador, podemos apreciar el resultado de las mencionadas operaciones, como muestra la siguiente imagen.

Para que estos cambios sobre la colección de nombres de días de la semana que acabamos de realizar sean efectivos, tenemos que asignar la variable que contiene la colección de nuevo a la propiedad AbbreviatedDayNames del objeto CultureInfo.DateTimeFormat, pero en este punto vamos a encontrarnos con una desagradable sorpresa ya que al realizar dicha operación mediante la siguiente línea de código.
oCultureInfoActual.DateTimeFormat.AbbreviatedDayNames = lstDiasAbreviados.ToArray();
Se producirá una excepción que nos indica que el objeto al que intentamos asignar el valor es de sólo lectura.
La forma de solucionar este problema pasa por crear una copia del objeto CultureInfo obtenido de la hebra de ejecución -utilizando su método Clone-, lo que nos permitirá realizar la modificación que precisamos y devolverlo de nuevo a la hebra actual de ejecución, como vemos en el siguiente bloque de código. Nótese igualmente, que para conseguir que este código funcione eficazmente, debe estar situado antes de la llamada al método InitializeComponent; dicho método es el encargado de hacer que el compilador de código de marcado procese el código XAML de la página, por lo que si en el momento de invocar a InitializeComponent la propiedad AbbreviatedDayNames no tiene establecida nuestra modificación, la página no se percatará de la misma.
CultureInfo oCultureInfoActual = (CultureInfo)Thread.CurrentThread.CurrentCulture.Clone();
List<string> lstDiasAbreviados = new List<string>(oCultureInfoActual.DateTimeFormat.AbbreviatedDayNames);
lstDiasAbreviados.Add(lstDiasAbreviados[0]);
lstDiasAbreviados.RemoveAt(0);
oCultureInfoActual.DateTimeFormat.AbbreviatedDayNames = lstDiasAbreviados.ToArray();
Thread.CurrentThread.CurrentCulture = oCultureInfoActual;
InitializeComponent();
Al volver a ejecutar nuestro proyecto, el control mostrará los nombres de los días de la semana en el orden deseado.

Reubicando los días del mes
Pero ahora nos encontraremos con un nuevo inconveniente: si bien hemos cambiado las posiciones de los nombres de los días, no hemos hecho lo propio con los números, los cuales ahora no se corresponden con las columnas del día de la semana, es decir, la primera columna de números pertenece todavía al domingo, la segunda al lunes y así sucesivamente.
El origen de este comportamiento se debe al control UniformGrid utilizado para visualizar los números de los días del mes. Dicho control se utiliza como plantilla de elementos dentro de otro control ListBox, como vemos en el siguiente código.
<!-- =============================================
DatePicker.xaml (c) 2006 by Charles Petzold
============================================= -->
<!--....-->
<ListBox Name="lstboxMonth"
SelectionChanged="ListBoxOnSelectionChanged">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Name="unigridMonth"
Columns="7" Rows="6"
IsItemsHost="True"
Background="{DynamicResource {x:Static SystemColors.ControlLightBrushKey}}" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBoxItem>dummy item</ListBoxItem>
</ListBox>
<!--....-->
El control UniformGrid dispone de una propiedad llamada FirstColumn, que como su nombre indica, nos permite especificar la columna a partir de la cual se van a comenzar a rellenar los valores del control. Dicha propiedad es utilizada en este ejemplo del libro en dos lugares: durante la carga del control -evento Loaded- y cada vez que se produzca un cambio en la fecha seleccionada -propiedad de dependencia Date-, momento en el que se producirá una llamada al método DateChangedCallback y desde este a OnDateChanged, que es donde se establece la columna para el UniformGrid. En ambos casos se toma el número de día de la semana que corresponde al primer día del mes utilizado por el control, asignando este valor a la propiedad UniformGrid.FirstColumn.
Para sincronizar correctamente los números del día del mes con las columnas de los días de la semana, lo que debemos hacer es restar uno a la operación anteriormente mencionada, de modo que el código quedaría de la siguiente manera.
//-------------------------------------------
// DatePicker.cs (c) 2006 by Charles Petzold
//-------------------------------------------
//....
public partial class DatePicker
{
//....
// Handler for window Loaded event.
void DatePickerOnLoaded(object sender, RoutedEventArgs args)
{
//....
// código modificado ---------------------
unigridMonth.FirstColumn =
(int)(new DateTime(dt.Year, dt.Month, 1).DayOfWeek) - 1;
// ----------------------------------------------
}
}
// OnDateChanged changes the visuals to match the new value of Date.
protected virtual void OnDateChanged(DateTime? dtOldValue,
DateTime? dtNewValue)
{
//....
// Set the first day of the month.
if (unigridMonth != null)
// código modificado ------------------
unigridMonth.FirstColumn =
(int)(new DateTime(dtNew.Year,
dtNew.Month, 1).DayOfWeek) - 1;
// ----------------------------------------------
//....
}
}
Al ejecutar de nuevo la aplicación comprobaremos cómo ahora los días del mes sí que coinciden adecuadamente con los de la semana.

Conclusión
El modelo de desarrollo de interfaces de usuario con WPF, basado en una clara separación entre la parte de presentación y la correspondiente a la lógica de funcionamiento, permite que podamos realizar modificaciones, como en este caso sobre la lógica, sin necesidad de alterar en absoluto el código XAML de presentación, lo que supone en muchos casos una ventaja en los procesos de mantenimiento de dichas interfaces.
Espero que os resulte interesante.
Un saludo.
En la primera entrega de este artículo iniciamos el desarrollo de una página Silverlight que incluía un control DataGrid, cuya principal característica debía consistir en la posibilidad de descargar archivos asociados a las celdas de una columna.
En esa primera parte se trataron aspectos tales como la creación del contexto de datos y el servicio WCF que proveía al DataGrid de la información a mostrar. También vimos la creación manual de columnas junto a los convertidores de tipo, elementos estos fundamentales en el proceso de formatear el dato original antes de ser presentado por el control.
Esta segunda entrega hará hincapié una vez más en el tema de los convertidores y explicará, tal y como habíamos prometido, los detalles relacionados con la columna para la descarga de archivos.
Un convertidor para tipos numéricos
La siguiente columna sobre la que tenemos que aplicar un formato es la que muestra el campo Importe. Se trata de valores numéricos que necesitamos visualizar con los separadores decimales y de millar, por lo que la operativa a seguir es prácticamente igual que la descrita para la columna de fecha: crear un convertidor de tipo, declararlo como recurso dentro del código XAML, e incluirlo en la definición de la columna del DataGrid, como vemos en los siguientes fragmentos de código.
public class NumeroConvertidor : IValueConverter
{
#region IValueConverter Members
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
decimal nNumero = (decimal)value;
string sNumero = nNumero.ToString("#,#.00", Thread.CurrentThread.CurrentCulture);
return sNumero;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return null;
}
#endregion
}
<UserControl.Resources>
<!--....-->
<propio:NumeroConvertidor x:Key="cnvNumero" />
</UserControl.Resources>
<!--....-->
<my:DataGrid.Columns>
<!--....-->
<my:DataGridTextColumn
Header="Tarifa"
DisplayMemberBinding="{Binding Importe, Converter={StaticResource cnvNumero}}" />
<!--....-->
</my:DataGrid.Columns>
En la siguiente imagen vemos la nueva columna numérica, formateada según acabamos de indicar.

Al observar el resultado en ejecución, el único inconveniente que en principio podemos achacar a esta columna radica en que sus valores no se encuentran alineados a la derecha, como sería lo deseable al tratarse de una columna de contenido numérico. Dado que entre las propiedades de la clase DataGridTextColumn no encontramos ninguna que nos permita realizar la mencionada alineación, debemos buscar otro medio para resolver el problema.
La solución en este caso la encontramos recurriendo al uso de una columna de tipo plantilla -clase DataGridTemplateColumn-la cual nos ofrece un alto grado de personalización ante el dato a mostrar.
Una vez añadida al código XAML la etiqueta correspondiente a este tipo de columna, dentro de la misma situaremos la plantilla de celda -propiedad/etiqueta CellTemplate-en cuyo interior añadiremos una plantilla de datos -etiqueta DataTemplate-, que será la que contenga aquellos controles que nos servirán para ajustar con mayor precisión el valor a mostrar.
El control en cuestión que incluiremos dentro del DataTemplate será un TextBlock sobre el que estableceremos la adecuada alinación de contenido mediante sus propiedades Margin, HorizontalAligment y VerticalAlignment.
<my:DataGrid.Columns>
<!--....-->
<my:DataGridTemplateColumn Header="Tarifa">
<my:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Importe, Converter={StaticResource cnvNumero}}"
Margin="0,0,10,0"
HorizontalAlignment="Right"
VerticalAlignment="Center" />
</DataTemplate>
</my:DataGridTemplateColumn.CellTemplate>
</my:DataGridTemplateColumn>
<!--....-->
</my:DataGrid.Columns>
A partir de este momento, la columna se mostrará con la alineación que queríamos.

Creando la columna para descargar archivos
Y llegamos a la parte del ejemplo en la que crearemos una columna que nos permitirá realizar la descarga de archivos. Como habíamos comentado al comienzo del artículo, esta columna debería mostrar una imagen, en la que al hacer clic, se realice la descarga del correspondiente archivo.
Vamos a asumir que el servidor Web de la agencia de viajes contiene los archivos a descargar en la siguiente ruta:
http://localhost/viajes/folletos/
Mientras que los archivos de imagen están en esta otra ruta:
http://localhost/viajes/paises/
Resulta obvio deducir, que dada la información y comportamiento que debe ofrecer esta columna, tendremos que implementarla utilizando un objeto DataGridTemplateColumn, debido a su mayor nivel de flexibilidad.
En lo que respecta a la funcionalidad de descarga de archivos, recurriremos al uso de un control HyperlinkButton, en cuya propiedad NavigateUri asignaremos la ruta del archivo a descargar, mientras que la propiedad Content nos permitirá mostrar el literal en forma de vínculo sobre el cual, cuando el usuario haga clic, provocará el comienzo de la descarga. Sin embargo, si enlazamos estas propiedades con la fuente de datos mediante un objeto Binding de la siguiente forma.
<my:DataGrid.Columns>
<!--....-->
<my:DataGridTemplateColumn Header="Folleto">
<my:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<HyperlinkButton NavigateUri="{Binding Folleto}" Content="{Binding Folleto}" />
</DataTemplate>
</my:DataGridTemplateColumn.CellTemplate>
</my:DataGridTemplateColumn>
<!--....-->
</my:DataGrid.Columns>
En la celda veremos el nombre del archivo, pero al hacer clic en él, no se producirá descarga alguna debido a que el vínculo no está correctamente construido; para colmo de males, el texto de dicho nombre de archivo tampoco se encontrará correctamente alineado dentro de la celda.

El motivo se debe a que la propiedad NavigateUri del control HyperlinkButton espera recibir un tipo Uri, y lo que nosotros le estamos pasando actualmente es una cadena simple, que ni tan siquiera contiene una ruta válida.
Puesto que conocemos la ruta en la que residen los archivos y el nombre del archivo seleccionado nos lo proporciona la fuente de datos a través de un objeto Binding, un modo de construir el vínculo pasaría, de nuevo, por recurrir a un convertidor de tipo que realice esta operación, lo cual vuelve a demostrarnos la potencia que este elemento tiene en la adaptación de los valores a visualizar dentro de la interfaz de usuario.
public class UriConvertidor : IValueConverter
{
#region IValueConverter Members
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
Uri oUri = new Uri("http://localhost/viajes/folletos/" + value.ToString());
return oUri;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return null;
}
#endregion
}
<UserControl.Resources>
<!--....-->
<propio:UriConvertidor x:Key="cnvUri" />
</UserControl.Resources>
<my:DataGrid.Columns>
<!--....-->
<my:DataGridTemplateColumn Header="Folleto">
<my:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<HyperlinkButton NavigateUri="{Binding Folleto, Converter={StaticResource cnvUri}}"
Content="{Binding Folleto}" />
</DataTemplate>
</my:DataGridTemplateColumn.CellTemplate>
</my:DataGridTemplateColumn>
<!--....-->
</my:DataGrid.Columns>
Tras agregar el anterior código a la aplicación, cuando a partir de ahora volvamos a hacer clic en las celdas de esta columna, se abrirá el conocido cuadro de diálogo para la descarga del archivo.

Añadiendo las imágenes
Para dar el toque final a este ejemplo solamente nos queda un detalle: reemplazar por imágenes el texto que en esta columna aparece en forma de vínculos.
El control HyperlinkButton, como ya hemos visto, dispone de la propiedad Content para albergar el contenido a visualizar, lo cual quiere decir que no estamos obligados a utilizar valores textuales para que el usuario interactúe con el control, sino que podemos asignarle una imagen para que reaccione ante un clic del usuario, navegando al destino especificado en la propiedad NavigateUri. El modo de establecer dicha imagen desde el código XAML pasa por utilizar la propiedad Content como una etiqueta, dentro de la cual situaremos un control Image.
<HyperlinkButton NavigateUri="{Binding Folleto, Converter={StaticResource cnvUri}}">
<HyperlinkButton.Content>
<!--....-->
<!--contenido-->
<!--....-->
</HyperlinkButton.Content>
</HyperlinkButton>
Centrando seguidamente nuestra atención sobre el control Image, su propiedad Source será la que usaremos para asignar la imagen a mostrar. El valor para dicha propiedad lo tomaremos de la fuente de datos mediante un objeto Binding como en las otras columnas, y como también ocurre con el control HyperlinkButton, debemos crear un convertidor que construya la ruta en la que se encuentra el archivo de imagen, a fin de que el control Image pueda acceder adecuadamente a ella para visualizarla.
public class RutaImagenConvertidor : IValueConverter
{
#region IValueConverter Members
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
string sRuta = "http://localhost/viajes/paises/" + value.ToString();
return sRuta;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
#endregion
}
<UserControl.Resources>
<!--....-->
<propio:UriConvertidor x:Key="cnvUri" />
<propio:RutaImagenConvertidor x:Key="cnvRutaImagen" />
</UserControl.Resources>
<my:DataGrid.Columns>
<!--....-->
<my:DataGridTemplateColumn Header="Folleto">
<my:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<HyperlinkButton
NavigateUri="{Binding Folleto, Converter={StaticResource cnvUri}}">
<HyperlinkButton.Content>
<Image
Source="{Binding Bandera, Converter={StaticResource cnvRutaImagen}}"
Width="30" Height="30" />
</HyperlinkButton.Content>
</HyperlinkButton>
</DataTemplate>
</my:DataGridTemplateColumn.CellTemplate>
</my:DataGridTemplateColumn>
<!--....-->
</my:DataGrid.Columns>
La siguiente imagen muestra finalmente el DataGrid resultante con toda la funcionalidad requerida.

Y con esto concluimos el presente artículo, en el que hemos explicado cómo añadir al control DataGrid de Silverlight una funcionalidad que no viene incorporada "de fábrica", pero que posiblemente necesitaremos implementar en diversas ocasiones sobre este control de cuadrícula. El código fuente, al igual que en la primera parte, está disponible en los enlaces C# y VB.
Espero que os resulte de utilidad.
Un saludo.
Durante el desarrollo de una aplicación para entorno Web, uno de los requerimientos con que nos podemos encontrar consiste en ofrecer al usuario la posibilidad de realizar la descarga de un archivo al seleccionar o hacer clic en uno de los controles que forman nuestra página, habitualmente un vínculo construido mediante la etiqueta <a> de HTML junto a su atributo href.
Sin embargo en esta ocasión vamos a ir un paso más allá, trasladando el escenario a una página Silverlight que contenga un control DataGrid; dicho control deberá ofrecer una columna cuyas celdas contengan imágenes en las que al hacer clic, darán al usuario la posibilidad de descargar un archivo a su máquina local.
El escenario a desarrollar
Supongamos que una agencia de viajes quiere ofrecer en su portal Web una página con su catálogo de destinos, de forma que los usuarios puedan visualizar la oferta disponible, seleccionar un destino, y descargar un archivo conteniendo el folleto en formato electrónico con la información sobre el destino elegido.
Para desarrollar esta característica podemos utilizar una página Web con contenido Silverlight, donde a través de un control DataGrid ofreceremos esta información que se encuentra contenida en la base de datos de la agencia, dentro de una tabla con la siguiente estructura.
CREATE TABLE [dbo].[Viajes](
[IDViaje] [bigint] NULL,
[Nombre] [varchar](50) NULL,
[FechaAlta] [datetime] NULL,
[Importe] [decimal](9, 4) NULL,
[Folleto] [varchar](100) NULL,
[Bandera] [varchar](100) NULL
) ON [PRIMARY]
En la siguiente imagen podemos ver una muestra de los registros contenidos en esta tabla.

Creación y configuración del entorno de datos
Comenzaremos por crear desde Visual Studio 2008 un nuevo proyecto de tipo Silverlight, donde estableceremos -desde el proyecto Web que compone la solución-en la ventana Server Explorer una conexión con la base de datos SQL Server AgenciaViajes, a fin de poder acceder a la tabla Viajes.

Los siguientes pasos para la creación de la fuente de datos utilizada por el DataGrid ya fueron explicados en este enlace, aunque a continuación los repasaremos brevemente.
Mediante la opción de menú de Visual Studio Project > Add New Item, añadiremos al proyecto Web de la solución un elemento LINQ to SQL Classes, con lo que obtendremos un contexto de datos al que llamaremos DCAgenciaViajes; la propiedad Serialization Mode del contexto de datos deberá tener el valor Unidirectional.
Arrastrando la tabla Viajes desde la ventana Server Explorer hasta el diseñador del contexto de datos, será creada una entidad de datos con el nombre Viaje, que representará los registros de la tabla Viajes, y nos servirá para poder transferir la información desde la fuente de datos hasta el control Silverlight como veremos próximamente.
También agregaremos al proyecto un servicio Web WCF con su correspondiente interfaz, donde escribiremos un método que devuelva, mediante LINQ, el contenido de la tabla Viajes a través de una colección genérica List<Viaje>.
[ServiceContract]
public interface IWSDatos
{
[OperationContract]
List<Viaje> ObtenerViajes();
}
//---------------------------------------
public class WSDatos : IWSDatos
{
#region IWSDatos Members
public List<Viaje> ObtenerViajes()
{
DCAgenciaViajesDataContext dcAgenciaViajes = new DCAgenciaViajesDataContext();
var lstLista = from tblViajes
in dcAgenciaViajes.Viajes
select tblViajes;
return lstLista.ToList();
}
#endregion
}
Para recuperar dicha colección estableceremos en el proyecto Silverlight una referencia hacia el servicio WCF, y desde el evento de carga de la página XAML crearemos una instancia del servicio, a través de la cual haremos una llamada a su método ObtenerViajes, el cual nos devolverá la colección en el evento de finalización de la llamada. Esta colección será asignada a la propiedad ItemsSource del control DataGrid, que la utilizará para visualizarla en formato tabular.
public partial class Page : UserControl
{
public Page()
{
InitializeComponent();
this.Loaded += new RoutedEventHandler(Page_Loaded);
}
void Page_Loaded(object sender, RoutedEventArgs e)
{
RefWSDatos.WSDatosClient wsDatos = new DescargaArchivosCeldasDataGridSL_CS.RefWSDatos.WSDatosClient();
wsDatos.ObtenerViajesCompleted += new EventHandler<DescargaArchivosCeldasDataGridSL_CS.RefWSDatos.ObtenerViajesCompletedEventArgs>(wsDatos_ObtenerViajesCompleted);
wsDatos.ObtenerViajesAsync();
}
void wsDatos_ObtenerViajesCompleted(object sender, DescargaArchivosCeldasDataGridSL_CS.RefWSDatos.ObtenerViajesCompletedEventArgs e)
{
this.grdDatos.ItemsSource = e.Result;
}
}
Comenzando a desarrollar la interfaz de usuario. Presentación básica
Pasemos ahora a la fase de creación de la interfaz de usuario, que como ya hemos mencionado estará compuesta principalmente por un control DataGrid, cuyo proceso de creación y puesta a punto iremos refinando progresivamente, a fin de que el lector pueda apreciar con mejor detalle las diferentes fases de construcción llevadas a cabo.
El modo más simple de presentación de los datos asignados al control en la propiedad ItemsSource consiste en establecer el valor True en su propiedad AutoGenerateColumns.
<my:DataGrid x:Name="grdDatos"
Width="575" Height="225"
AutoGenerateColumns="True"
IsReadOnly="True">
<!--....-->
<!--....-->
</my:DataGrid>
Esta acción visualizará la información con sus valores originales.

Creación manual de columnas
Para comenzar con la personalización de las columnas asignaremos el valor False a la propiedad AutoGenerateColumns, esto supone que tendremos que crear manualmente cada columna que necesitemos visualizar, lo cual haremos dentro de la colección Columns del control, utilizando alguna de las clases derivadas de DataGridBoundColumn: DataGridTextColumn para mostrar valores textuales, y DataGridCheckBoxColumn para mostrar valores lógicos a través de una casilla de marcado.
Nota. Dentro del contexto del presente artículo, emplearemos el término "campo" para referirnos a las columnas o campos de la tabla de la base de datos que vamos a visualizar en el DataGrid.
La forma de indicar a un objeto columna el elemento - campo- de la fuente de datos que debe mostrar, pasa por asignar a la propiedad DisplayMemberBinding una expresión de marcado extendida con la información del enlace a establecer utilizando el formato "{Binding NombreCampo}". El siguiente código fuente define varias columnas del origen de datos mediante objetos DataGridTextColumn; como podemos observar, no es necesario crear todas las columnas disponibles en la fuente de datos, sino simplemente aquellas que necesitemos.
<my:DataGrid x:Name="grdDatos"
Width="575" Height="225"
AutoGenerateColumns="False"
IsReadOnly="True">
<my:DataGrid.Columns>
<my:DataGridTextColumn Header="Código" DisplayMemberBinding="{Binding IDViaje}" />
<my:DataGridTextColumn Header="Descripción" DisplayMemberBinding="{Binding Nombre}" />
<my:DataGridTextColumn Header="Creado" DisplayMemberBinding="{Binding FechaAlta}" />
</my:DataGrid.Columns>
</my:DataGrid>
La siguiente imagen muestra el resultado obtenido.

Convertidores de tipo. Dando forma a los datos
Antes de entrar a resolver el objetivo principal de este artículo: la descarga de archivos desde el control de cuadrícula, observemos un notable inconveniente en la presentación de los datos de tipo fecha, consistente en que dicha información se muestra sin formato alguno, lo cual también es totalmente lógico, puesto que el control desconoce a priori el estilo de formato que queremos aplicar.
La técnica a seguir para establecer un formato personalizado en una columna del DataGrid consiste en la creación de un convertidor de tipo, el cual aplicaremos en aquel punto del código XAML donde se establezca el enlace a datos de la columna; a partir de ese momento, el dato enviado por el origen para ser mostrado en el grid, pasará primeramente por el convertidor, que lo "moldeará" según nuestras necesidades antes de mandarlo a la celda.
Para ello tenemos que añadir al proyecto Silverlight una clase que implemente la interfaz IValueConverter, que se compone de los métodos Convert y ConvertBack. En el primero escribiremos el código para formatear el valor que recibimos del origen de datos tal y como queremos visualizarlo en el control, mientras que en el segundo deberemos devolver el valor formateado al tipo original. Dado que en nuestro ejemplo no vamos a editar las celdas del DataGrid, en el método ConvertBack sencillamente retornaremos null.
using System;
using System.Windows.Data;
using System.Threading;
//....
public class FechaConvertidor:IValueConverter
{
#region IValueConverter Members
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
DateTime dtFecha = (DateTime)value;
return dtFecha.ToString("dd - MMMM - yyyy", culture);
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return null;
}
#endregion
}
El modo de utilizar este convertidor en nuestro código XAML pasa por declarar un espacio de nombres que haga referencia al ensamblado en el que reside la clase del convertidor -en este caso, el mismo ensamblado del proyecto Silverlight. Como nombre para el espacio de nombres utilizaremos la palabra "propio", tal y como vemos en la siguiente figura.

A continuación, dentro del apartado de recursos de la página, declararemos un nuevo recurso, que partiendo del espacio de nombres que acabamos de declarar, accederá a la clase del convertidor. También le asignaremos mediante el atributo Key, una clave para su posterior utilización.
<UserControl.Resources>
<propio:FechaConvertidor x:Key="cnvFecha" />
</UserControl.Resources>
Tanto en el momento de declarar el espacio de nombres como al crear el recurso contaremos con la asistencia de Intellisense, que nos evitará tener que escribir el valor, guiándonos además en su selección, como vemos en la siguiente figura.

En el caso de que Intellisense no ofrezca la lista de selección automáticamente, podemos invocarla pulsando la combinación Ctrl + J.
Finalmente, en la definición de la columna que muestra la fecha ampliaremos la expresión de marcado que habíamos asignado a la propiedad DisplayMemberBinding, incluyendo, mediante la propiedad Converter del enlace a datos -objeto Binding-una referencia hacia el convertidor de tipo que hemos declarado como recurso, utilizando la palabra clave StaticResource. La sintaxis empleada tendrá el siguiente formato: {Binding NombreCampo, Converter = {StaticResource ClaveRecurso}}.
<my:DataGrid.Columns>
<!--....-->
<my:DataGridTextColumn
Header="Creado"
DisplayMemberBinding="{Binding FechaAlta, Converter={StaticResource cnvFecha}}" />
<!--....-->
</my:DataGrid.Columns>
La siguiente imagen muestra el formato resultante sobre la columna de fecha.

No obstante seguimos observando una pequeña pega: el nombre de los meses está en inglés, dado que en el método FechaConvertidor.Convert utilizamos el objeto CultureInfo que este manipulador de evento recibe como parámetro. Para evitar tal inconveniente, lo que vamos a hacer es utilizar la información cultural del sistema que obtenemos de la hebra de ejecución de nuestra aplicación -Thread.CurrentThread.CurrentCulture- en forma de objeto CultureInfo. Sustituyendo este objeto por el que estábamos utilizando como segundo parámetro del método DateTime.ToString habremos solucionado el problema.
public class FechaConvertidor : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
DateTime dtFecha = (DateTime)value;
return dtFecha.ToString("dd - MMMM - yyyy", Thread.CurrentThread.CurrentCulture);
}
//....
}
Como vemos en la siguiente imagen, las fechas ahora sí que muestran el valor deseado.

Y llegados a este punto concluimos la primera parte de este artículo, en la siguiente entrega abordaremos algunos aspectos adicionales sobre los convertidores de tipo, así como la prometida funcionalidad de la columna para realizar la descarga de archivos a través de sus celdas. Para todos aquellos que quieran probar el ejemplo al completo, en los enlaces C# y VB se encuentran los proyectos para los respectivos lenguajes.
Un saludo.
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.
Si hace unos días nos encontrábamos con la liberación del Service Pack 1 para .NET Framework y Visual Studio 2008, ahora ocurre lo propio con el Ajax Control Toolkit, del que ya está disponible su versión para este SP1.
En el blog de Bertrand Le Roy tenéis el detalle acerca de esta nueva versión, en la que se incorpora un nuevo control llamado MultiHandleSlider.
La descarga de los bits del Toolkit la podéis realizar en CodePlex, desde este enlace.
Que ustedes lo disfruten ;-)
Un saludo
En la primera entrega de este artículo abordamos el desarrollo de un servicio WCF que permitía a los elementos de un control AJAX el acceso básico a la tabla Shippers de la base de datos Northwind, en esta segunda parte continuaremos con el resto de operaciones de edición necesarias para implementar un mantenimiento de datos sobre dicha tabla.
Métodos del servicio que reciben parámetros
La siguiente tarea a desarrollar consistirá en obtener el registro de la tabla Shippers relacionado con el elemento del DropDownList seleccionado por el usuario; para ello necesitamos añadir al servicio WSDatos un método que reciba como parámetro el valor del campo ShipperID a buscar, y que devuelva dicho registro como resultado.
[OperationContract]
public Shipper ObtenerShipper(int nShipperID)
{
SqlConnection cnConexion = new SqlConnection(
ConfigurationManager.ConnectionStrings["CadConexion"].ConnectionString);
SqlCommand cmdComando = new SqlCommand("SELECT * FROM Shippers WHERE ShipperID = @ShipperID",
cnConexion);
cmdComando.Parameters.AddWithValue("@ShipperID", nShipperID);
cnConexion.Open();
SqlDataReader drReader = cmdComando.ExecuteReader(CommandBehavior.SingleRow);
drReader.Read();
Shipper oShipper = new Shipper((int)drReader["ShipperID"],
drReader["CompanyName"].ToString(),
drReader["Phone"].ToString());
cnConexion.Close();
return oShipper;
}
Como hemos dicho, este método del servicio será llamado desde la página Web cada vez que se realice una nueva selección en el control ddlShippers, es decir, al producirse su evento cliente onchange. Puesto que se trata de un control de servidor, la manera más adecuada de asociar uno de sus eventos cliente con el nombre de la función Javascript que ejecutará, es desde el code-behind de la página -en el evento Load por ejemplo-, empleando su colección Attributes, como vemos a continuación.
public partial class _Default : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
this.ddlShippers.Attributes.Add("onchange", "ddlShippers_onchange()");
}
}
Pasando al bloque Javascript del WebForm, en la función ddlShippers_onchange llamaremos a este nuevo método del servicio, pasándole como primer parámetro el valor del elemento seleccionado en la lista desplegable, y como segundo el nombre de la función que será llamada si la ejecución del método tiene éxito; en dicha función recibiremos como parámetro el objeto Shipper seleccionado, pasando los valores de sus propiedades a los controles TextBox del cuadro de diálogo.
<script type="text/javascript" language="javascript"> 1:
2: //....
3: function ddlShippers_onchange()
4: { 5: WSDatos.ObtenerShipper(document.getElementById("ddlShippers").value, 6: ObtenerShipperCompleted);
7: }
8:
9: function ObtenerShipperCompleted(oShipper)
10: { 11: document.getElementById("txtCompanyName").value = oShipper.CompanyName; 12: document.getElementById("txtPhone").value = oShipper.Phone; 13: }
</script>
En la siguiente figura podemos apreciar un ejemplo de este caso.

Al pulsar el botón btnAceptar del diálogo, traspasaremos los valores elegidos al formulario principal; para ello, tendremos que asignar a la propiedad OnOkScript del ModalPopupExtender, el nombre de la función Javascript que se encargará de dicha tarea.
<cc1:ModalPopupExtender ID="mpeShippers" runat="server"
….
OnOkScript="mpeShippers_OnOk()" />
//....
<script type="text/javascript" language="javascript">
//....
function mpeShippers_OnOk()
{
document.getElementById("txtTransportista").value = document.getElementById("txtCompanyName").value + '-' +
document.getElementById("txtPhone").value;
}
//....

Métodos del servicio para la edición de datos. Insertar, modificar y borrar
El resto de métodos que nos queda por escribir son aquellos relacionados con las operaciones de edición sobre la tabla Shippers. Al igual que en los anteriores casos, en primer lugar añadiremos estos métodos al servicio, como vemos en el siguiente código fuente.
[OperationContract]
public void InsertarShipper(string sCompanyName, string sPhone)
{
SqlConnection cnConexion = new SqlConnection(
ConfigurationManager.ConnectionStrings["CadConexion"].ConnectionString);
SqlCommand cmdComando = new SqlCommand("INSERT INTO Shippers VALUES (@CompanyName, @Phone)",
cnConexion);
cmdComando.Parameters.AddWithValue("@CompanyName", sCompanyName);
cmdComando.Parameters.AddWithValue("@Phone", sPhone);
cnConexion.Open();
cmdComando.ExecuteNonQuery();
cnConexion.Close();
}
[OperationContract]
public void ModificarShipper(int nShipperID, string sCompanyName, string sPhone)
{
SqlConnection cnConexion = new SqlConnection(
ConfigurationManager.ConnectionStrings["CadConexion"].ConnectionString);
string sSQL = "UPDATE Shippers ";
sSQL += "SET CompanyName = @CompanyName, ";
sSQL += "Phone = @Phone ";
sSQL += "WHERE ShipperID = @ShipperID";
SqlCommand cmdComando = new SqlCommand(sSQL, cnConexion);
cmdComando.Parameters.AddWithValue("@CompanyName", sCompanyName);
cmdComando.Parameters.AddWithValue("@Phone", sPhone);
cmdComando.Parameters.AddWithValue("@ShipperID", nShipperID);
cnConexion.Open();
cmdComando.ExecuteNonQuery();
cnConexion.Close();
}
[OperationContract]
public void BorrarShipper(int nShipperID)
{
SqlConnection cnConexion = new SqlConnection(
ConfigurationManager.ConnectionStrings["CadConexion"].ConnectionString);
string sSQL = "DELETE FROM Shippers ";
sSQL += "WHERE ShipperID = @ShipperID";
SqlCommand cmdComando = new SqlCommand(sSQL, cnConexion);
cmdComando.Parameters.AddWithValue("@ShipperID", nShipperID);
cnConexion.Open();
cmdComando.ExecuteNonQuery();
cnConexion.Close();
}
Como detalle aclarativo para el método InsertarShipper, recordemos que el campo ShipperID de la tabla Shippers es autonumérico, por lo que no es necesario calcular el valor para dicho campo al insertar un nuevo registro en la tabla.
A continuación pasaremos al código de marcado de la página Web, asignando al evento onclick de los botones correspondientes, el nombre de la función que realizará la operación de edición pertinente.
….
<input id="btnInsertar" type="button" value="Insertar" onclick="btnInsertar_onclick()" />
….
<input id="btnModificar" type="button" value="Modificar" onclick="btnModificar_onclick()" />
….
<input id="btnBorrar" type="button" value="Borrar" onclick="btnBorrar_onclick()" />
….
Solamente queda ya, escribir las funciones Javascript que realizarán las llamadas a los métodos de edición del servicio WCF, como se muestra en el siguiente bloque de código.
<script type="text/javascript" language="javascript"> 1:
2: //....
3: function btnInsertar_onclick()
4: { 5: WSDatos.InsertarShipper(document.getElementById("txtCompanyName").value, 6: document.getElementById("txtPhone").value, 7: InsertarShipperOnSuccess);
8:
9: LimpiarCargarCombo();
10: }
11:
12: function InsertarShipperOnSuccess()
13: { 14: alert("Registro añadido"); 15: }
16:
17: function btnModificar_onclick()
18: { 19: var ddlShippers = document.getElementById("ddlShippers"); 20:
21: WSDatos.ModificarShipper(ddlShippers.value,
22: document.getElementById("txtCompanyName").value, 23: document.getElementById("txtPhone").value); 24:
25: LimpiarCargarCombo();
26: }
27:
28: function btnBorrar_onclick()
29: { 30: var resultado = confirm("¿Seguro que quiere borrar?"); 31:
32: if (resultado == true)
33: { 34: var ddlShippers = document.getElementById("ddlShippers"); 35:
36: WSDatos.BorrarShipper(ddlShippers.value);
37:
38: LimpiarCargarCombo();
39: }
40: }
41:
42: function LimpiarCargarCombo()
43: { 44: document.getElementById("txtCompanyName").value = ""; 45: document.getElementById("txtPhone").value = ""; 46:
47: var ddlShippers = document.getElementById("ddlShippers"); 48:
49: while (true)
50: { 51: if (ddlShippers.length == 0)
52: { 53: break;
54: }
55: else
56: { 57: ddlShippers.remove(0);
58: }
59: }
60:
61: WSDatos.ObtenerShippers(ObtenerShippersCompleted);
62: }
</script>
Y tras este conjunto de operaciones podemos dar por concluida la segunda entrega del artículo, en el que hemos ilustrado una técnica que permite a un control AJAX acceder y manipular una tabla existente en una base de datos desde código Javascript.
Para poder realizar las pruebas necesarias, al igual que en la primera parte, el código fuente queda disponible en los siguientes enlaces: C# y VB. Espero que os resulte interesante
Un saludo.
Más artículos
Página siguiente >