Mostrando postagens com marcador Reflection. Mostrar todas as postagens
Mostrando postagens com marcador Reflection. Mostrar todas as postagens

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

Um método pode retornar um tipo anônimo?

Os tipos anônimos (Anonymous Types) são uma característica da linguagem C# onde criamos diversas propriedades para um objeto sem ter que definir explicitamente seu tipo. Essa novidade surgiu na versão 3.0 junto com diversas outras tais como: propriedades automáticas, inferência de tipos, inicializadores de objetos, expressões lambda, métodos de extensão e outros.

Um tipo anônimo pode ser declarado da seguinte maneira:

var c = new { Nome = "Ari", Idade = 28 };

Neste caso a variável c é um tipo de possui duas propriedades: uma propriedade do tipo System.String chamada Nome e uma propriedade do tipo System.Int32 chamada Idade.

Os tipos anônimos são muito úteis quando realizamos consultas usando o LINQ pois normalmente temos que retornar um tipo com um conjunto de propriedades diferente dos tipos já existentes. Por exemplo, vejam as duas consultas abaixo:

var q1 = from c in dc.Customers
         where c.City == "London"
         select new { c.FirstName, c.LastName, c.City };

var q2 = from c in dc.Customers
         where c.City == "London"
         select new { c.FirstName, c.City };

A primeira consulta retorna um tipo que contém 3 propriedades (FirstName, LastName e City) e a segunda um tipo com 2 propriedades (FirstName e City). Sem os tipos anônimos teríamos que definir explicitamente duas classes para representar os tipos retornados.

Mas, se o tipo é anônimo, como podemos fazer para um método retorná-lo?

public ???? ListCustomers()
{
    var q1 = from c in dc.Customers
             where c.City == "London"
             select new { c.FirstName, c.LastName, c.City };

    return q1;
}

Bom, podemos resolver essa questão pensando da seguinte maneira: se um tipo anônimo é um tipo do .NET, então esse herda de System.Object, vamos então retornar um object.

public object ListCustomers() { ... }

Esse raciocínio está correto, o problema é que transformando para object perdemos a possibilidade de utilizarmos as propriedades do tipo anônimo diretamente (strong typing).

var c = ListCustomers();
c.FirstName = "Anders"; // erro

Bom, para resolver esse problema temos 3 possíveis soluções:

  1. Reflection.
  2. Dynamic Programming.
  3. Realizar um cast do object com o tipo anônimo.

A primeira opção é lenta e muito código deve ser criado, portanto será descartada. A segunda opção é interessante mas vou limitar meu escopo com as versões anteriores à 4.0. Vou mostrar então a terceira opção.

Para realizarmos um cast de um object para um tipo anônimo podemos, com auxílio de Generics, criar o método abaixo:

public T CastObjectToType<T>(object objeto, T tipo)
{
    return (T)objeto;
}

Podemos então converter o objeto em um tipo anônimo da seguinte maneira:

object o = ListCustomers();
var c = CastObjectToType(o, new { FirstName = String.Empty, 
                                  LastName = String.Empty, 
                                  City = String.Empty});
c.FirstName = "Anders";  // OK

Isso é possível graças à inferência de tipo, que nos permite fazer um cast sem conhecer o nome do tipo. É o que foi realizado no método CastObjectToType.

Finalizado, a resposta é SIM. Podemos retornar um tipo anônimo de um método.

Links de ajuda:

Anonymous Types (C# Programming Guide)
http://bit.ly/69HYJd

Generic Methods (C# Programming Guide)
http://bit.ly/6wFbas

Getting Started with LINQ in C#
http://bit.ly/4BOIUU