Observação: Esse post sugere que o leitor possua um conhecimento intermediário de técnicas de binding e estilos no WPF. Estarei à disposição para eventuais dúvidas ou sugestão de leitura de links ou materiais sobre o assunto.
Essa semana no fórum de WPF do MSDN o
Márcio Fábio Althmann postou uma pergunta interessante em uma thread e resolvi compartilhar aqui a minha experiência tentando resolver o problema. Na verdade eu consegui encontrar uma maneira de resolvê-lo, mas quando fui postar a resposta, vi que o
Roberto Sonnino postou uma solução melhor que a minha. Vou demonstrar neste post duas maneiras de resolver o problema.
A dúvida do Márcio era como alterar o background das células (DataGridCell) de um DataGrid com base no valor da célula e sem conhecer o nome da coluna associada à célula, ou seja, desconhecendo o Binding Path. A única especificação seria que quando a célula tivesse o valor “Livre” o fundo deveria ser verde e quando o valor fosse “Reservado” o fundo deveria ser vermelho.
A thread pode ser consultada no link abaixo.
Pintar célula
http://bit.ly/cjvkyv
Vou mostrar primeiro a solução quando conhecemos o nome da propriedade, ou seja, o Binding Path.
O que vamos fazer primeiramente é criar uma classe que implementa a interface
IValueConverter e realiza a conversão de uma String em um SolidColorBrush por meio de uma lógica qualquer, no caso a cor será determinada pelos valores “Livre” ou “Reservado”. Um Converter (classe que implementa IValueConverter) é uma classe que realiza uma lógica customizada de binding.
A classe então fica da seguinte maneira:
[ValueConversion(typeof(string), typeof(SolidColorBrush))]
public class DataGridCellBackgroundColorConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
string valor = value.ToString();
if (valor == "Livre")
return new SolidColorBrush(Colors.Green);
else if (valor == "Reservado")
return new SolidColorBrush(Colors.Red);
return new SolidColorBrush(Colors.White);
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
No arquivo XAML teríamos então que incluir o Converter como um Resource da Window.
<Window.Resources>
<local:DataGridCellBackgroundColorConverter x:Key="DataGridCellConverter" />
</Window.Resources>
No DataGrid poderíamos então incluir um estilo com um setter para a propriedade Background usando o Binding para uma propriedade chamada NomePropriedade e também convertendo essa propriedade usando o Converter. Um tanto complicado né? Não é não, WPF é assim mesmo e com o tempo a gente acostuma.
<DataGrid AutoGenerateColumns="True"
Name="seuDataGrid"
ItemsSource="{Binding}">
<DataGrid.CellStyle>
<Style TargetType="{x:Type DataGridCell}">
<Setter Property="Background"
Value="{Binding NomePropriedade, Converter={StaticResource DataGridCellConverter}}" />
</Style>
</DataGrid.CellStyle>
</DataGrid>
Tudo ok e tudo funcionando, mas e quando não temos o nome da coluna? Ou seja, no XAML acima imagine que não sabemos qual é o NomePropriedade a ser utilizado. Para isso, uma das soluções é realizar o binding por meio do código, usando o data source para incluir as colunas (DataGridTextColumn no caso). Essa foi a solução que encontrei para o problema do Márcio.
Assim, o DataGrid agora fica da seguinte maneira no XAML.
<DataGrid AutoGenerateColumns="False"
Name="seuDataGrid"
ItemsSource="{Binding}" />
No evento Loaded da Window vamos popular um DataSet que funcionará como o data source e então vamos adicionar colunas do tipo DataGridTextColumn com os respectivos estilos e bindings. Os comentários do código servem como explicação do que está sendo realizado.
void SuaWindow_Loaded(object sender, RoutedEventArgs e)
{
// criação do DataSet
var dataSet = new DataSet();
dataSet.Tables.Add("Tabela1");
dataSet.Tables[0].Columns.Add("Coluna1");
dataSet.Tables[0].Columns.Add("Coluna2");
dataSet.Tables[0].Rows.Add(new object[] { "Reservado", "Livre" });
dataSet.Tables[0].Rows.Add(new object[] { "Teste", "Livre" });
dataSet.Tables[0].Rows.Add(new object[] { "Livre", "Livre" });
dataSet.Tables[0].Rows.Add(new object[] { "Livre", "Reservado" });
dataSet.Tables[0].Rows.Add(new object[] { "Reservado", "Reservado" });
// conversor para as células
var backColorConverter = new DataGridCellBackgroundColorConverter();
foreach (DataColumn dc in dataSet.Tables[0].Columns)
{
// cria coluna
var dataGridTextColumn = new DataGridTextColumn();
// header e data binding
dataGridTextColumn.Header = dc.ColumnName;
dataGridTextColumn.Binding = new Binding(dc.ColumnName);
// estilo da coluna
var columnStyle = new Style();
columnStyle.TargetType = typeof(DataGridCell);
// setter para a propriedade Background
var setter = new Setter();
setter.Property = DataGridCell.BackgroundProperty;
// binding do setter com o nome da coluna e converter
var setterBinding = new Binding(dc.ColumnName);
setterBinding.Converter = backColorConverter;
setter.Value = setterBinding;
// adiciona setter na coleção de setters do estilo
columnStyle.Setters.Add(setter);
// define o estilo da coluna
dataGridTextColumn.CellStyle = columnStyle;
// adiciona coluna no DataGrid
this.seuDataGrid.Columns.Add(dataGridTextColumn);
}
this.seuDataGrid.DataContext = dataSet.Tables[0];
}
Acho uma solução simples, mas que ainda deve ser revista, visto que o tipo da coluna do DataGrid depende do tipo da coluna do data source.
Vamos agora então à solução do Roberto que sugere a criação de um Converter para pegar o valor da célula usando Reflection.
public class CelulaParaValorConverter: IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
// célula
var cell = value as DataGridCell;
if (cell != null)
{
// linha e coluna
var row = DataGridRow.GetRowContainingElement(cell);
var column = cell.Column as DataGridBoundColumn;
if (column != null)
{
// binding associado à coluna
var binding = column.Binding as Binding;
if (binding != null)
{
// nome da propriedade (Binding Path)
string boundPropertyName = binding.Path.Path;
// data item que a row representa
object data = row.Item;
// propriedades do item
var properties = TypeDescriptor.GetProperties(data);
// verifica total de propriedades
if (properties.Count > 0)
{
// propriedade associada ao binding
var property = properties[boundPropertyName];
// valor da propriedade associado ao item
return property.GetValue(data);
}
}
}
}
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
Adicionamos o Converter aos Resources da Window.
<Window.Resources>
<local:CelulaParaValorConverter x:Key="CelulaParaValor" />
</Window.Resources>
Agora usamos um DataTrigger no estilo do DataGridCell para definir um Setter e também a propriedade RelativeSource para realizar binding com o próprio elemento (RelativeSource Self). O XAML fica da seguinte maneira:
<DataGrid AutoGenerateColumns="False"
Name="seuDataGrid"
ItemsSource="{Binding}">
<DataGrid.CellStyle>
<Style TargetType="{x:Type DataGridCell}">
<Style.Triggers>
<DataTrigger Value="Livre"
Binding="{Binding RelativeSource, RelativeSource={RelativeSource Self}, Converter={StaticResource CelulaParaValor}}">
<Setter Property="Background"
Value="Green" />
</DataTrigger>
<DataTrigger Value="Reservado"
Binding="{Binding RelativeSource, RelativeSource={RelativeSource Self}, Converter={StaticResource CelulaParaValor}}">
<Setter Property="Background"
Value="Red" />
</DataTrigger>
</Style.Triggers>
</Style>
</DataGrid.CellStyle>
</DataGrid>
Agora é só realizar o binding do DataGrid com o data source.
this.seuDataGrid.DataContext = MetodoQueRetornaUmDataSource();
A solução do Roberto é realmente mais simples e elegante.
Até a próxima !
Binding Class
http://bit.ly/dr7GrM
DataTrigger Class
http://bit.ly/bL6rKS