niedziela, 13 listopada 2011

WPF DataGrid zmienna ilość kolumn

Problem: W WPF DataGrid chcemy tworzyć dynamicznie kolumny tj. zarówno kolumny jak i dane. Dodatkowo chcemy mieć możliwość bindowania do różnych typów danych.
Uczestnicząc w obecnym projekcie napotkałem na opisany powyżej problem.
Rozwiązanie problemu rozpocząłem od przeszukania internetu - a nuż ktoś już rozwiązał podobny problem.
A więc reasumując będziemy chcieli osiągnąć taki wygląd:



Nasz grid ma się składać z następujących elementów:

  1. Standardowo nagłówek z nazwami kolumn - zwykły tekst
  2. 1 kolumna zawiera dane tekstowe (np. źródła)
  3. Elementy to checkbox + textbox 
Tak więc jak widać tworzenie danych na pewno musi być dynamiczne - jak i tworzenie całej struktury DataGrid-a.
Na początek od razu zaznaczam że nie biorę tutaj pod uwagę pracy ze wzorcem MVVM - implementację w MVVM zostawiam jako deser dla Was :)

Problemem jest to iż wiersze w DataGrid są typu object. Najczęściej wykorzystujemy bindowanie do gotowej kolekcji obiektów które wcześniej przygotujemy. Tworzymy w tym celu klasę, następnie wypełniamy kolekcję obiektami stworzonej klasy. Ostatecznie bindujemy kolekcję do właściwości ItemsSource DataGrid-a.
Nasz przypadek mówi o tym, iż nie mamy podanej ilości kolumn ani ich typów.
Zostaje więc pytanie do czego zbindować, żeby mieć możliwość dodawania kolumn w trakcie działania aplikacji? Do tego zadania nadaje się świetnie DataTable.

Tworzymy więc klasę która będzie służyła jako kolumny naszego grida, tworzone dynamicznie:


Code:
using System.Windows.Media;

namespace WpfApplication13
{
    public class CellWithValue
    {
        public double Value { get; set; }
        public SolidColorBrush Color { get; set; }
    }
}

Klasa resetująca binding:


Code:
using System.Windows.Controls;
using System.Windows.Data;

namespace WpfApplication13
{
    public class MyDataGridTemplateColumn : DataGridTemplateColumn
    {
        public string ColumnName
        {
            get;
            set;
        }

        protected override System.Windows.FrameworkElement GenerateElement(DataGridCell cell, object dataItem)
        {
            var cp = (ContentPresenter)base.GenerateElement(cell, dataItem);
            BindingOperations.SetBinding(cp, ContentPresenter.ContentProperty, new Binding(this.ColumnName));
            return cp;
        }
    }
}

Binding resetujemy z powodu tego iż domyślnie mielibyśmy zbindowane obiekty typu DataRowView.

Kolejnym etapem jest stworzenie w XAMLu odpowiedniego szablonu danych:


Code:
<Window x:Class="WpfApplication13.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <DataTemplate x:Key="Wzor" DataType="DataGridCell">
            <StackPanel Orientation="Horizontal" Background="{Binding Color}">
                <CheckBox VerticalAlignment="Center" />
                <TextBox Width="50" Text="{Binding Value}" Opacity="0.8" />
            </StackPanel>
        </DataTemplate>
    </Window.Resources>
    <Grid>
        <DataGrid Name="dgData" AutoGeneratingColumn="dgData_AutoGeneratingColumn" />
    </Grid>
</Window>

W kodzie (code behind) definiujemy binding:


Code:
using System.Data;
using System.Windows;
using System.Windows.Media;

namespace WpfApplication13
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            var dataTable = new DataTable();
            dataTable.Columns.Add("Source");
            dataTable.Columns.Add(new DataColumn { ColumnName = "CellWithValue", DataType = typeof(CellWithValue) });

            var dr = dataTable.NewRow();
            dr[0] = "Źródło";
            dr[1] = new CellWithValue {Color = Brushes.Red, Value = 5.26};
            dataTable.Rows.Add(dr);

            dgData.ItemsSource = dataTable.DefaultView;
        }

        private void dgData_AutoGeneratingColumn(object sender, System.Windows.ControlsDataGridAutoGeneratingColumnEventArgs e)
        {
            if (e.PropertyType == typeof(CellWithValue))
            {
                var col = new MyDataGridTemplateColumn();
                col.ColumnName = e.PropertyName; 
                col.CellTemplate = (DataTemplate) FindResource("Wzor");
                e.Column = col;
                e.Column.Header = e.PropertyName;
            }
        }
    }
}

Mamy tutaj do czynienia z następującymi elementami:
  1. Tworzymy obiekt DataTable który następnie bindujemy do DataGrid poprzez właściwość DefaultView
  2. W zdarzeniu AutoGeneratingColumn nadajemy odpowiedni styl naszej kolumnie. 
Sposób prosty i przyjemny. Co prawda nie jest to MVVM ale spełnia swoją rolę w przypadku dynamicznego tworzenia DataGrid-a.

Efekt końcowy:

1 komentarz: