Ir para o conteúdo
XamDataGrid – Crie dinamicamente e vincule colunas de dados com um editor de sua escolha

XamDataGrid – Crie dinamicamente e vincule colunas de dados com um editor de sua escolha

Recentemente, um bom amigo meu me enviou um email perguntando como gerar colunas dinamicamente com base em uma coleção de objetos com o XamDataGrid.  Esta parece ser uma tarefa muito comum que quase todos os aplicativos provavelmente precisarão fazer.  Definitivamente, não é uma tarefa incomum.

10min de leitura

Recentemente, um bom amigo meu me enviou um email perguntando como gerar colunas dinamicamente com base em uma coleção de objetos com o XamDataGrid.  Esta parece ser uma tarefa muito comum que quase todos os aplicativos provavelmente precisarão fazer.  Definitivamente, não é uma tarefa incomum.

Nesse caso específico, precisamos nivelar uma coleção de objetos a serem representados como colunas na grade.  Por exemplo; Você pode ter um objeto que tenha um número de N níveis de propriedades ou atributos que não são conhecidos até o tempo de execução, mas deseja editar o objeto em uma única linha em uma grade. Você não quer adicionar um monte de propriedades em seu objeto como Prop1, Prop2, Prop3, etc., apenas para poder vinculá-lo à sua grade.  Você não tem ideia de quantos serão. Você deseja adicionar colunas dinamicamente à sua grade e associar essas colunas ao objeto correto na coleção filho em tempo de execução.

Nesse cenário, estou criando um aplicativo de equipe e tenho um objeto "StaffMember" que tem uma coleção de objetos "Period" como uma propriedade filho. Meus objetos são mais ou menos assim:

public class StaffMember
{
    public String Department { get; set; }
    public String Name { get; set; }
    public IList<Period> Periods { get; set; }

    public StaffMember()
    {
        this.Periods = new List<Period>();
    }
}

public class Period
{
    public string Title { get; set; }
    public int Hours { get; set; }
}

Esses são apenas POCOs simples que atualmente não implementam INotifyPropertyChanged.  Para este aplicativo de demonstração, não preciso de notificações de propriedade.  Em um aplicativo de produção, você provavelmente precisará implementar a interface INotifyPropertyChanged para notificações de alteração.  Em seguida, precisamos de um ViewModel.

public class StaffMemberViewModel : INotifyPropertyChanged
{
    ObservableCollection<StaffMember> _staffMembers;
    public ObservableCollection<StaffMember> StaffMembers
    {
        get { return _staffMembers; }
        set
        {
            _staffMembers = value;
            RaisePropertyChanged("StaffMembers");
        }
    }

    public StaffMemberViewModel()
    {
        PopulateStaffMembers();
    }

    void PopulateStaffMembers()
    {
        var list = new ObservableCollection<StaffMember>();
        var rand = new Random();

        for (Int32 i = 1; i < 4; i++)
        {
            var member = new StaffMember { Name = String.Format("Name {0}", i), Department = String.Format("Department {0}", i) };
            for (int j = 1; j < 5; j++)
                member.Periods.Add(new Period { Title = String.Format("Period {0}", j), Hours = rand.Next(0, 160) });
            list.Add(member);
        }

        StaffMembers = list;
    }

    public event PropertyChangedEventHandler PropertyChanged;
    void RaisePropertyChanged(String propertyName)
    {
        var handler = this.PropertyChanged;
        if (handler != null)
            handler(this, new PropertyChangedEventArgs(propertyName));
    }
}

Esse ViewModel implementa a interface INotifyPropertyChanged, assim como todos os ViewModels.  Como você pode ver, temos uma única propriedade que expõe uma coleção de StaffMembers.  Também temos um método que gera alguns dados fictícios para nós.  A próxima coisa que precisamos é de uma visão.

<Window x:Class="XamDataGridDynamicColumns.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:igWPF="http://schemas.infragistics.com/xaml/wpf"
        xmlns:local="clr-namespace:XamDataGridDynamicColumns"
        Title="MainWindow" Height="350" Width="525">

    <Window.DataContext>
        <local:StaffMemberViewModel />
    </Window.DataContext>

    <Grid>
        <igWPF:XamDataGrid DataSource="{Binding Path=StaffMembers}"
                           FieldLayoutInitialized="xamDataGrid_FieldLayoutInitialized">
            <igWPF:XamDataGrid.FieldLayoutSettings>
                <igWPF:FieldLayoutSettings AutoGenerateFields="False"/>
            </igWPF:XamDataGrid.FieldLayoutSettings>
            <igWPF:XamDataGrid.FieldLayouts>
                <igWPF:FieldLayout>
                    <igWPF:Field Name="Name"/>
                    <igWPF:Field Name="Department"/>
                </igWPF:FieldLayout>
            </igWPF:XamDataGrid.FieldLayouts>
        </igWPF:XamDataGrid>
    </Grid>
</Window>

Primeiro, observe que definimos dois namespaces em nossa View.  Um é para nossos objetos locais e o outro é para os controles Infragistics que usaremos.  Defina o DataContext da exibição como uma instância de nosso StaffMemberViewModel.  Você pode fazer isso como quiser.  Aconteceu de eu fazer isso em XAML.  Agora precisamos declarar um XamDataGrid e associá-lo aos dados à nossa coleção de StaffMembers em nosso ViewModel.  Agora a ideia é que saibamos que nosso StaffMember tem um Nome e um Departamento que estarão sempre disponíveis.  Essas não são colunas dinâmicas, portanto, podemos seguir em frente e criar nossa exibição declarando-as em nosso FieldLayout.  e queremos ter certeza de que definimos AutoGenerateField = false, porque seremos responsáveis por quais colunas criar.

Então, como começamos a gerar colunas?  Bem, primeiro adicione um manipulador de eventos ao evento XamDataGrid.FieldLayoutInitialized.  É aqui que toda a mágica acontecerá para criar, vincular dados, escolher um editor e adicionar as colunas.  Eu adicionei uma pequena propriedade ao nosso ViewModel para me dar acesso a quaisquer dados que eu possa precisar.

public StaffMemberViewModel ViewModel
{
    get { return this.DataContext as StaffMemberViewModel; }
}

private void xamDataGrid_FieldLayoutInitialized(object sender, Infragistics.Windows.DataPresenter.Events.FieldLayoutInitializedEventArgs e)
{

}

A propriedade ViewModel simplesmente me dá o DataContext do View na forma de nosso StaffMemberViewModel (usarei isso para trapacear em um minuto).  Precisamos criar nossas colunas com base no número de períodos na coleção de períodos do StaffMember.  Lembre-se, não sabemos quantos períodos teremos até o tempo de execução, então precisamos obter essas informações primeiro.  Aqui é onde vou trapacear para fins de demonstração.

private void xamDataGrid_FieldLayoutInitialized(object sender, Infragistics.Windows.DataPresenter.Events.FieldLayoutInitializedEventArgs e)
{
    //a cheat to get the number of columns to create.
    var staffMember = this.ViewModel.StaffMembers.First();

    for (Int32 i = 0; i < staffMember.Periods.Count; i++)
    {
        var field = new UnboundField
        {
            Name = staffMember.Periods[i].Title,
            BindingMode = BindingMode.TwoWay,
            BindingPath = new PropertyPath(String.Format("Periods[{0}].Hours", i))
        };

        e.FieldLayout.Fields.Add(field);
    }
}

Nesta demonstração, estou usando o primeiro StaffMember da coleção para determinar o número de colunas a serem criadas.  Agora, é claro, no mundo real, você não gostaria de usar o primeiro índice da coleção filho para descobrir quantas colunas criar. Eu recomendaria algum tipo de objeto de definição que definirá quais colunas e quantas colunas construir.  Em seguida, criamos um loop com o número correto de interações a serem feitas e criamos um novo UnboundField.  Queremos definir três propriedades importantes.  O primeiro é o Nome.  Isso nos dará o cabeçalho da nossa coluna.  Em seguida, é o BindingMode.  Queremos que nossas associações de dados sejam TwoWay.  Por fim, criamos um BindingPath.  Observe como estou criando um caminho de associação que usa um indexador ([ ]).  Isso nos permite criar associações para os objetos em índices específicos da coleção que estamos nivelando.  Finalmente, tudo o que precisamos fazer é adicionar o campo recém-criado ao nosso FieldLayout.  Execute o aplicativo e é isso que você obtém.

Execute o aplicativo e é isso que você obtém.

Muito bom.  Nivelamos com sucesso nosso grafo de objeto e criamos as ligações de dados adequadas para cada célula para cada propriedade do objeto subjacente.

Choosing Your Editor

Agora eu sei o que você está pensando.  Mas Brian, o editor padrão é um TextBlock.  E se eu quiser usar um editor diferente como o XamNumericEditor.  Bem, felizmente para você, isso é tão simples de fazer.  Só precisamos adicionar algum código e um pouco de XAML.

private void xamDataGrid_FieldLayoutInitialized(object sender, Infragistics.Windows.DataPresenter.Events.FieldLayoutInitializedEventArgs e)
{
    //a cheat to get the number of columns to create.
    var staffMember = this.ViewModel.StaffMembers.First();

    for (Int32 i = 0; i < staffMember.Periods.Count; i++)
    {
        var field = new UnboundField
        {
            Name = staffMember.Periods[i].Title,
            BindingMode = BindingMode.TwoWay,
            BindingPath = new PropertyPath(String.Format("Periods[{0}].Hours", i))
        };

        field.Settings.EditAsType = typeof(Int32);
        field.Settings.EditorStyle = (Style)Resources["HoursFieldStyle"];

        e.FieldLayout.Fields.Add(field);
    }
}

Adicionamos duas linhas de código ao nosso manipulador de eventos.  Ao definir a propriedade Field.Settings.EditAsType, estamos instruindo o editor que estamos usando sobre como lidar com o tipo de dados.  Como nossa propriedade Hours é do tipo Int, definimos a propriedade adequadamente.  Agora, isso não vai nos dar o XamNumericEditor automaticamente.  Para isso, precisamos fornecer um EditorStyle.  Portanto, adicionamos uma linha de código informando à propriedade Field.Settings.EditorStyle para obter seu valor de um recurso que estamos prestes a criar chamado "HoursFieldStyle".

<Window.Resources>
    <Style x:Key="HoursFieldStyle" TargetType="{x:Type igWPF:XamNumericEditor}">
        <Setter Property="Mask" Value="###" />
    </Style>
</Window.Resources>

Abra nosso XAML e defina um estilo dentro da propriedade Window.Resources de nosso Modo de Exibição.  O que estamos fazendo aqui é especificar que usaremos o XamNumericEditor e, em seguida, definir a propriedade Mask nesse editor para 3 dígitos.  Agora, lembre-se de que nossos dados aleatórios têm menos de 3 dígitos, mas isso está sendo usado apenas como exemplo.  Execute o aplicativo e vamos ver o que obtemos.

Execute o aplicativo e vamos ver o que obtemos.

Agora estamos usando o XamNumericEditor com uma máscara de ###. O que acontece se tentarmos inserir um valor que não corresponda à máscara ### que definimos?

O que acontece se tentarmos inserir um valor que não corresponda à máscara ### que definimos?

Está correto!  Nosso editor funciona como deveria, avisando que o novo valor não corresponde à máscara necessária para uma entrada válida.  Muito legal, certo!?  E é tão fácil.

Bem, que tal um ComboBox?

Ok Brian, você nos mostrou as coisas simples, mas e quanto a algo mais complicado?  Quero usar um XamComboEditor e preenchê-lo com valores para escolher o formulário para cada célula.  Você está tentando me desafiar?  Trazer. Isso.

Tudo bem, a primeira coisa é a primeira.  Precisamos de uma fonte de dados para nosso XamComboEditor.  Existem algumas abordagens para isso, então vou escolher apenas uma.  Preciso de um objeto que represente os valores selecionados e uma classe para usar como fonte de dados.  Pode ser um ViewModel separado ou dentro do mesmo ViewModel que temos atualmente.  Vou usar um separado.  Aqui estão as classes que representam minha fonte de dados para o XamComboEditor:

public class HoursDataSource
{
    public ObservableCollection<DataItem> Hours { get; set; }

    public HoursDataSource()
    {
        PopulateHours();
    }

    private void PopulateHours()
    {
        var list = new ObservableCollection<DataItem>();

        for (int i = 1; i < 160; i++)
        {
            list.Add(new DataItem() { Name = i.ToString(), Value = i });
        }

        Hours = list;
    }
}

public class DataItem
{
    public string Name { get; set; }
    public int Value { get; set; }
}

Bem simples.  Não há muito nisso.  Agora precisamos adicionar algum código ao nosso XAML.

<Window.Resources>
    <local:HoursDataSource x:Key="hoursDataSource" />
    <igWPF:ComboBoxItemsProvider x:Key="hoursProvider" ItemsSource="{Binding Hours, Source={StaticResource hoursDataSource}}" DisplayMemberPath="Name" ValuePath="Value" />
    <Style x:Key="HoursFieldStyle" TargetType="{x:Type igWPF:XamComboEditor}">
        <Setter Property="ItemsProvider" Value="{StaticResource hoursProvider}" />
    </Style>
    <!--<Style x:Key="HoursFieldStyle" TargetType="{x:Type igWPF:XamNumericEditor}">
        <Setter Property="Mask" Value="###" />
    </Style>-->
</Window.Resources>

Começamos comentando o primeiro Estilo que criamos.  Em seguida, precisamos adicionar uma instância do nosso objeto HoursDataSource.  Em seguida, crie um ComboBoxItemsProvider.  Os dados associam o ItemsSource à propriedade Hours que existe em nossa instância HoursDataSource.  Não se esqueça de definir o DsiplayMemberPath=Name e o ValuePath=Value.  Em seguida, substitua o estilo que tínhamos anteriormente por um novo que defina o TargetType como um XamComboEditor.  Defina um Setter que defina a propriedade ItemsProvider como nosso recurso ComboBoxItemsProvider que acabamos de criar.  Execute o aplicativo e vamos ver o que acontece.

Execute o aplicativo e vamos ver o que acontece.

Claro, você sempre pode tornar isso mais funcional incluindo lógica para determinar qual editor usar para qual coluna ou que tipo o valor deve ser editado para colunas diferentes.

Sinta-se à vontade para baixar o código-fonte.  Se você tiver alguma dúvida, pode entrar em contato comigo através do meu blog no http://brianlagunas.com, no Twitter (@BrianLagunas), ou apenas deixar um comentário abaixo.

Solicite uma demonstração