WPF – Alterando o Background de um DataGridCell sem conhecer o Binding Path

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

Nenhum comentário:

Postar um comentário