El día de hoy hicieron una pregunta muy interesante en el grupo de La Liga Silverlight en Facebook. Aquí la pongo textual:
“AYUDA!!!
Tengo un DataGrid en Silverlight con los siguientes campos {idhorario, horainicio, horafin, dia, idseccion}.
necesito que los campos {horainicio y horafin}, cambien de color en una Hora indicada, me explico, tengo una materia de 12:00 a 2:00pm, y son las 1:00pm en mi reloj, que estos campos aparezcan en otro COLOR… o que llamen la atencion de alguna manera.
COMO HAGO ESTO???”
Se me ocurren unas 3 o 4 maneras para resolver esto, pero decidí irme por la que creo yo la más eficiente: usar la característica de enlace en los setters de los estilos, disponible a partir de Silverlight 5.
¡Manos a la obra!
El Proyecto
Iniciaremos creando un proyecto regular de Silverlight 5 llamado Horarios usando la plantilla de Silverlight Application.
Clase Materia
La primera tarea será modelar la clase para cada materia. A esta clase la llamaremos Materia. A continuación podrán observar la implementación completa:
1: public class Materia : INotifyPropertyChanged
2: {
3:
4: public SolidColorBrush ColorMateria
5: {
6: get
7: {
8: SolidColorBrush colorMateria = new SolidColorBrush(Colors.Black);
9:
10: DateTime inicio = Helper.GetDateTimeFromHourMinuteString(HoraInicio);
11: DateTime fin = Helper.GetDateTimeFromHourMinuteString(HoraFin);
12:
13: if (DateTime.Now >= inicio && DateTime.Now <= fin)
14: {
15: colorMateria = new SolidColorBrush(Colors.Red);
16: }
17:
18: return colorMateria;
19: }
20: }
21:
22:
23: DispatcherTimer timer;
24: public Materia()
25: {
26: if (timer == null)
27: {
28: timer = new DispatcherTimer() { Interval = TimeSpan.FromSeconds(1) };
29: timer.Tick += (s, a) =>
30: {
31: OnPropertyChanged("ColorMateria");
32: };
33:
34: timer.Start();
35: }
36: }
37:
38:
39: string nombre;
40:
41: public string Nombre
42: {
43: get { return nombre; }
44: set { nombre = value;
45: OnPropertyChanged("Nombre");
46: }
47: }
48: string horaInicio;
49:
50: public string HoraInicio
51: {
52: get { return horaInicio; }
53: set { horaInicio = value;
54: OnPropertyChanged("HoraInicio");
55: }
56: }
57: string horaFin;
58:
59: public string HoraFin
60: {
61: get { return horaFin; }
62: set { horaFin = value;
63: OnPropertyChanged("HoraFin");
64: }
65: }
66:
67: void OnPropertyChanged(string propertyName)
68: {
69: if (PropertyChanged != null)
70: {
71: PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
72: }
73: }
74:
75: public event PropertyChangedEventHandler PropertyChanged;
76: }
Como podrán apreciar, esta clase implementa la intefaz INotifyPropertyChanged ya que estoy interesado en notificar a los bindings enlazados a sus propiedades que algún valor de dichas propiedades ha cambiado.
En el constructor, estoy inicializando un objeto de tipo DispatcherTimer, con un intervalo de 1 segundo. En el manejador del evento Tick de este objeto notifico a la infraestructura de bindings de Silverlight que la propiedad ColorMateria ha cambiado.
Propiedad ColorMateria
Esta propiedad es de tipo SolidColorBrush, pero pudiera ser de cualquier tipo concreto de la familia de clases Brush. El objetivo de esta propiedad será obtener los valores de las propiedades HoraInicio y HoraFin (ambas de tipo string), las cuales indican la hora de inicio y hora de finalización de dicha materia respectivamente. En la implementación estoy esperando un formato HH:MM ya que considero que el tipo DateTime no expresaría adecuadamente este valor (pero se los dejo a su consideración). Ahora bien, podrás darte cuenta que se invoca un método llamado GetDateTimeFromHourMinuteString(). Este método estático está implementado en una clase llamada Helper para no mezclar esta implementación dentro de la clase de nuestro modelo (Materia). El algoritmo para determinar el cambio de color es bastante sencillo: simplemente se evalúa si la hora actual está dentro de ese lapso.
GetDateTimeFromHourMinuteString()
Este método es súmamente sencillo, ya que su único objetivo es regresarnos un DateTime a partir de la cadena HH:MM que le pasemos en su único parámetro. Este DateTime es necesario para evaluar si la hora actual está dentro del lapso de duración de la materia, y de esa manera se dispare el cambio de color adecuadamente. A continuación podrás ver la implementación completa de la clase Helper.
1: public class Helper
2: {
3: public static DateTime GetDateTimeFromHourMinuteString(string cadena)
4: {
5: try
6: {
7: var valores = cadena.Split(':');
8: int hora = int.Parse(valores[0]);
9: int minuto = int.Parse(valores[1]);
10:
11: return new DateTime(DateTime.Now.Year,
12: DateTime.Now.Month,
13: DateTime.Now.Day,
14: hora,
15: minuto,
16: 0);
17: }
18: catch
19: {
20: return DateTime.Now;
21: }
22:
23: }
24:
25: }
Datos
Claro está, necesitamos una lista de datos. La clase Datos fungirá como nuestro ViewModel de la aplicación, y expondrá una sola propiedad llamada ListaMaterias de tipo ObservableCollection<Materia>. En este caso y a falta de una fuente de datos real he implementado datos “dummy” en el get{} de la propiedad. No obstante, para efectos de prueba es una excelente opción para corroborar la funcionalidad que estamos buscando. ListaMaterias decidí implementarla de tipo ObservableCollection para que sus datos posteriormente puedan ser cargados dinámicamente y que automáticamente el control o los controles enlazados sean notificados del cambio de la colección.
1: public class Datos
2: {
3: ObservableCollection<Materia> listaMaterias;
4: public ObservableCollection<Materia> ListaMaterias
5: {
6: get
7: {
8: if (listaMaterias == null)
9: {
10: listaMaterias = new ObservableCollection<Materia>();
11:
12: for (int i = 0; i < 5; i++)
13: {
14: listaMaterias.Add(new Materia()
15: {
16: Nombre = "Materia 1 " + i.ToString(),
17: HoraInicio = string.Format("{0}:{1}", DateTime.Now.Hour, DateTime.Now.Minute + 2),
18: HoraFin = string.Format("{0}:{1}", DateTime.Now.Hour, DateTime.Now.Minute + 3),
19: });
20:
21: listaMaterias.Add(new Materia()
22: {
23: Nombre = "Materia 2 " + i.ToString(),
24: HoraInicio = string.Format("{0}:{1}", DateTime.Now.Hour, DateTime.Now.Minute + 3),
25: HoraFin = string.Format("{0}:{1}", DateTime.Now.Hour, DateTime.Now.Minute + 5),
26: });
27:
28: listaMaterias.Add(new Materia()
29: {
30: Nombre = "Materia 3 " + i.ToString(),
31: HoraInicio = string.Format("{0}:{1}", DateTime.Now.Hour, DateTime.Now.Minute + 4),
32: HoraFin = string.Format("{0}:{1}", DateTime.Now.Hour, DateTime.Now.Minute + 7),
33: });
34: }
35:
36: }
37:
38: return listaMaterias;
39:
40: }
41: }
42:
43: }
Para que los datos de prueba sean interesantes, he puesto 15 materias las cuales unas vencen en los próximos 2 minutos en el momento de que ejecutes la aplicación, otros en 3 y otros en 4 minutos.
MainPage.xaml
Finalmente, la Vista. En el diccionario de recursos del elemento UserControl en MainPage.xaml he declarado el siguiente estilo:
1: <Style x:Key="EstiloMateria"
2: TargetType="TextBlock">
3: <Setter Property="Foreground"
4: Value="{Binding ColorMateria}" />
5: <Setter Property="VerticalAlignment"
6: Value="Center" />
7: </Style>
Nota el enlace en el <Setter> de la propiedad Foreground. Aquí, el estilo estará obligado a enlazar a la propiedad Foreground cualquiera que fuese el valor ColorMateria; y como ColorMateria está siendo modificado cada segundo por la lógica que expliqué al inicio, pues bueno, ya saben cuál será el desenlace de esto
. El poder enlazar el valor de una propiedad dentro de un Estilo es una de las características nuevas en Silverlight 5 y sin duda alguna una bastante poderosa.
DataGrid
En el control DataGrid de Silverlight, para que podamos tener un control detallado sobre la creación de sus columnas, debemos establecer su propiedad AutoGenerateColumns a false, por lo que la responsabilidad de definir cada una de sus columnas recaerá sobre nostros. Para mostrar el cambio de color en el nombre de la materia, he decidido implementar como primera columna un DataGridTemplateColumn, cuyo DataTemplate incluye un TextBlock enlazado precisamente a la propiedad ColorMateria que ya he detallado en este artículo. A continuación podrás ver la implementación de esta columna:
1: <sdk:DataGridTemplateColumn Width="2*" Header="Nombre de la materia">
2: <sdk:DataGridTemplateColumn.CellTemplate>
3: <DataTemplate>
4: <TextBlock Style="{StaticResource EstiloMateria}" Text="{Binding Nombre}" />
5: </DataTemplate>
6: </sdk:DataGridTemplateColumn.CellTemplate>
7: </sdk:DataGridTemplateColumn>
El siguiente código muestra la implementación completa de MainPage.xaml. Nota que adicionalmente al Estilo que indiqué anteriormente, estoy declarando una instancia de Datos, para posteriormente enlazarla como DataContext del Grid raíz.
1: <UserControl x:Class="Horarios.MainPage"
2: xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3: xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4: xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
5: xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
6: mc:Ignorable="d"
7: xmlns:local="clr-namespace:Horarios"
8: d:DesignHeight="300" d:DesignWidth="400"
9: xmlns:sdk="http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk">
10: <UserControl.Resources>
11: <local:Datos x:Key="datos" />
12: <Style x:Key="EstiloMateria"
13: TargetType="TextBlock">
14: <Setter Property="Foreground"
15: Value="{Binding ColorMateria}" />
16: <Setter Property="VerticalAlignment"
17: Value="Center" />
18: </Style>
19: </UserControl.Resources>
20:
21: <Grid x:Name="LayoutRoot" Background="White"
22: DataContext="{Binding Source={StaticResource datos}}"
23: >
24: <sdk:DataGrid AutoGenerateColumns="False"
25: ItemsSource="{Binding ListaMaterias}"
26: Name="dataGrid1">
27: <sdk:DataGrid.Columns>
28: <sdk:DataGridTemplateColumn Width="2*" Header="Nombre de la materia">
29: <sdk:DataGridTemplateColumn.CellTemplate>
30: <DataTemplate>
31: <TextBlock Style="{StaticResource EstiloMateria}" Text="{Binding Nombre}" />
32: </DataTemplate>
33: </sdk:DataGridTemplateColumn.CellTemplate>
34: </sdk:DataGridTemplateColumn>
35: <sdk:DataGridTextColumn Width="1*" Header="Hora inicio" Binding="{Binding HoraInicio, StringFormat='0:00'}" />
36: <sdk:DataGridTextColumn Width="1*" Header="Hora fin" Binding="{Binding HoraFin, StringFormat='0:00'}" />
37: </sdk:DataGrid.Columns>
38: </sdk:DataGrid>
39: </Grid>
40: </UserControl>
La Ejecución
Inicialmente, al momento de ejecutar la aplicación veremos el DataGrid con las materias en color negro.

Después de 2 minutos, el resultado de esta implementación se hará evidente ¡de manera automática!

Resumen
El enlace dentro de los Estilos es una característica nueva presente en Silverlight 5. Usando esta característica podemos crear estilos cuyos valores estén en función de alguna propiedad enlazada, y de esa manera lograr Interfaces de Usuario altamente dinámicas.
El código fuente completo lo pueden descargar de esta dirección.
Salu2!