Как связать WPF DataGrid с переменным числом столбцов? - PullRequest
119 голосов
/ 26 ноября 2008

Мое приложение WPF генерирует наборы данных, которые могут каждый раз иметь разное количество столбцов. В выводе содержится описание каждого столбца, который будет использоваться для применения форматирования. Упрощенная версия вывода может выглядеть примерно так:

class Data
{
    IList<ColumnDescription> ColumnDescriptions { get; set; }
    string[][] Rows { get; set; }
}

Этот класс установлен как DataContext в WPF DataGrid, но я фактически создаю столбцы программно:

for (int i = 0; i < data.ColumnDescriptions.Count; i++)
{
    dataGrid.Columns.Add(new DataGridTextColumn
    {
        Header = data.ColumnDescriptions[i].Name,
        Binding = new Binding(string.Format("[{0}]", i))
    });
}

Можно ли заменить этот код привязками данных в файле XAML?

Ответы [ 8 ]

122 голосов
/ 07 декабря 2010

Вот обходной путь для связывания столбцов в DataGrid. Поскольку свойство Columns является ReadOnly, как все заметили, я создал свойство Attached под названием BindableColumns, которое обновляет столбцы в DataGrid каждый раз, когда коллекция изменяется посредством события CollectionChanged.

Если у нас есть эта коллекция DataGridColumn's

public ObservableCollection<DataGridColumn> ColumnCollection
{
    get;
    private set;
}

Затем мы можем привязать BindableColumns к ColumnCollection следующим образом

<DataGrid Name="dataGrid"
          local:DataGridColumnsBehavior.BindableColumns="{Binding ColumnCollection}"
          AutoGenerateColumns="False"
          ...>

Присоединенное свойство BindableColumns

public class DataGridColumnsBehavior
{
    public static readonly DependencyProperty BindableColumnsProperty =
        DependencyProperty.RegisterAttached("BindableColumns",
                                            typeof(ObservableCollection<DataGridColumn>),
                                            typeof(DataGridColumnsBehavior),
                                            new UIPropertyMetadata(null, BindableColumnsPropertyChanged));
    private static void BindableColumnsPropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
    {
        DataGrid dataGrid = source as DataGrid;
        ObservableCollection<DataGridColumn> columns = e.NewValue as ObservableCollection<DataGridColumn>;
        dataGrid.Columns.Clear();
        if (columns == null)
        {
            return;
        }
        foreach (DataGridColumn column in columns)
        {
            dataGrid.Columns.Add(column);
        }
        columns.CollectionChanged += (sender, e2) =>
        {
            NotifyCollectionChangedEventArgs ne = e2 as NotifyCollectionChangedEventArgs;
            if (ne.Action == NotifyCollectionChangedAction.Reset)
            {
                dataGrid.Columns.Clear();
                foreach (DataGridColumn column in ne.NewItems)
                {
                    dataGrid.Columns.Add(column);
                }
            }
            else if (ne.Action == NotifyCollectionChangedAction.Add)
            {
                foreach (DataGridColumn column in ne.NewItems)
                {
                    dataGrid.Columns.Add(column);
                }
            }
            else if (ne.Action == NotifyCollectionChangedAction.Move)
            {
                dataGrid.Columns.Move(ne.OldStartingIndex, ne.NewStartingIndex);
            }
            else if (ne.Action == NotifyCollectionChangedAction.Remove)
            {
                foreach (DataGridColumn column in ne.OldItems)
                {
                    dataGrid.Columns.Remove(column);
                }
            }
            else if (ne.Action == NotifyCollectionChangedAction.Replace)
            {
                dataGrid.Columns[ne.NewStartingIndex] = ne.NewItems[0] as DataGridColumn;
            }
        };
    }
    public static void SetBindableColumns(DependencyObject element, ObservableCollection<DataGridColumn> value)
    {
        element.SetValue(BindableColumnsProperty, value);
    }
    public static ObservableCollection<DataGridColumn> GetBindableColumns(DependencyObject element)
    {
        return (ObservableCollection<DataGridColumn>)element.GetValue(BindableColumnsProperty);
    }
}
18 голосов
/ 05 декабря 2008

Я продолжил свое исследование и не нашел никакого разумного способа сделать это. Свойство Columns в DataGrid - это не то, с чем я могу связываться, на самом деле оно доступно только для чтения.

Брайан предположил, что что-то может быть сделано с AutoGenerateColumns, поэтому я посмотрел. Он использует простое отражение .Net для просмотра свойств объектов в ItemsSource и генерирует столбец для каждого из них. Возможно, я мог бы на лету сгенерировать тип со свойством для каждого столбца, но это уже не в порядке.

Поскольку эту проблему так легко решить в коде, я буду придерживаться простого метода расширения, который я вызываю всякий раз, когда контекст данных обновляется новыми столбцами:

public static void GenerateColumns(this DataGrid dataGrid, IEnumerable<ColumnSchema> columns)
{
    dataGrid.Columns.Clear();

    int index = 0;
    foreach (var column in columns)
    {
        dataGrid.Columns.Add(new DataGridTextColumn
        {
            Header = column.Name,
            Binding = new Binding(string.Format("[{0}]", index++))
        });
    }
}

// E.g. myGrid.GenerateColumns(schema);
9 голосов
/ 29 января 2011

Я нашел статью в блоге Деборы Курата с хорошим трюком, как показывать переменное количество столбцов в DataGrid:

Заполнение таблицы данных динамическими столбцами в приложении Silverlight с использованием MVVM

По сути, она создает DataGridTemplateColumn и помещает внутрь ItemsControl, который отображает несколько столбцов.

5 голосов
/ 17 января 2012

Мне удалось сделать возможным динамическое добавление столбца, используя только строку кода, подобную этой:

MyItemsCollection.AddPropertyDescriptor(
    new DynamicPropertyDescriptor<User, int>("Age", x => x.Age));

Что касается вопроса, то это не решение на основе XAML (поскольку, как уже упоминалось, разумного способа сделать это), ни решение, которое будет работать напрямую с DataGrid.Columns. На самом деле он работает с привязанным к DataGrid ItemsSource, который реализует ITypedList и, как таковой, предоставляет пользовательские методы для извлечения PropertyDescriptor. В одном месте кода вы можете определить «строки данных» и «столбцы данных» для вашей сетки.

Если бы вы имели:

IList<string> ColumnNames { get; set; }
//dict.key is column name, dict.value is value
Dictionary<string, string> Rows { get; set; }

вы можете использовать, например:

var descriptors= new List<PropertyDescriptor>();
//retrieve column name from preprepared list or retrieve from one of the items in dictionary
foreach(var columnName in ColumnNames)
    descriptors.Add(new DynamicPropertyDescriptor<Dictionary, string>(ColumnName, x => x[columnName]))
MyItemsCollection = new DynamicDataGridSource(Rows, descriptors) 

и ваша сетка, использующая привязку к MyItemsCollection, будет заполнена соответствующими столбцами. Эти столбцы могут быть изменены (добавлены новые или удалены существующие) во время выполнения динамически, и сетка автоматически обновит свою коллекцию столбцов.

DynamicPropertyDescriptor, упомянутый выше, является просто обновлением до обычного PropertyDescriptor и предоставляет строгое типизированное определение столбцов с некоторыми дополнительными опциями. В противном случае DynamicDataGridSource работал бы просто отлично с базовым PropertyDescriptor.

3 голосов
/ 09 марта 2016

Сделан вариант принятого ответа, который обрабатывает отписку.

public class DataGridColumnsBehavior
{
    public static readonly DependencyProperty BindableColumnsProperty =
        DependencyProperty.RegisterAttached("BindableColumns",
                                            typeof(ObservableCollection<DataGridColumn>),
                                            typeof(DataGridColumnsBehavior),
                                            new UIPropertyMetadata(null, BindableColumnsPropertyChanged));

    /// <summary>Collection to store collection change handlers - to be able to unsubscribe later.</summary>
    private static readonly Dictionary<DataGrid, NotifyCollectionChangedEventHandler> _handlers;

    static DataGridColumnsBehavior()
    {
        _handlers = new Dictionary<DataGrid, NotifyCollectionChangedEventHandler>();
    }

    private static void BindableColumnsPropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
    {
        DataGrid dataGrid = source as DataGrid;

        ObservableCollection<DataGridColumn> oldColumns = e.OldValue as ObservableCollection<DataGridColumn>;
        if (oldColumns != null)
        {
            // Remove all columns.
            dataGrid.Columns.Clear();

            // Unsubscribe from old collection.
            NotifyCollectionChangedEventHandler h;
            if (_handlers.TryGetValue(dataGrid, out h))
            {
                oldColumns.CollectionChanged -= h;
                _handlers.Remove(dataGrid);
            }
        }

        ObservableCollection<DataGridColumn> newColumns = e.NewValue as ObservableCollection<DataGridColumn>;
        dataGrid.Columns.Clear();
        if (newColumns != null)
        {
            // Add columns from this source.
            foreach (DataGridColumn column in newColumns)
                dataGrid.Columns.Add(column);

            // Subscribe to future changes.
            NotifyCollectionChangedEventHandler h = (_, ne) => OnCollectionChanged(ne, dataGrid);
            _handlers[dataGrid] = h;
            newColumns.CollectionChanged += h;
        }
    }

    static void OnCollectionChanged(NotifyCollectionChangedEventArgs ne, DataGrid dataGrid)
    {
        switch (ne.Action)
        {
            case NotifyCollectionChangedAction.Reset:
                dataGrid.Columns.Clear();
                foreach (DataGridColumn column in ne.NewItems)
                    dataGrid.Columns.Add(column);
                break;
            case NotifyCollectionChangedAction.Add:
                foreach (DataGridColumn column in ne.NewItems)
                    dataGrid.Columns.Add(column);
                break;
            case NotifyCollectionChangedAction.Move:
                dataGrid.Columns.Move(ne.OldStartingIndex, ne.NewStartingIndex);
                break;
            case NotifyCollectionChangedAction.Remove:
                foreach (DataGridColumn column in ne.OldItems)
                    dataGrid.Columns.Remove(column);
                break;
            case NotifyCollectionChangedAction.Replace:
                dataGrid.Columns[ne.NewStartingIndex] = ne.NewItems[0] as DataGridColumn;
                break;
        }
    }

    public static void SetBindableColumns(DependencyObject element, ObservableCollection<DataGridColumn> value)
    {
        element.SetValue(BindableColumnsProperty, value);
    }

    public static ObservableCollection<DataGridColumn> GetBindableColumns(DependencyObject element)
    {
        return (ObservableCollection<DataGridColumn>)element.GetValue(BindableColumnsProperty);
    }
}
2 голосов
/ 11 сентября 2009

Вы можете создать пользовательский элемент управления с определением сетки и определить «дочерние» элементы управления с различными определениями столбцов в xaml. Родителю необходимо свойство зависимости для столбцов и метод загрузки столбцов:

Родитель:


public ObservableCollection<DataGridColumn> gridColumns
{
  get
  {
    return (ObservableCollection<DataGridColumn>)GetValue(ColumnsProperty);
  }
  set
  {
    SetValue(ColumnsProperty, value);
  }
}
public static readonly DependencyProperty ColumnsProperty =
  DependencyProperty.Register("gridColumns",
  typeof(ObservableCollection<DataGridColumn>),
  typeof(parentControl),
  new PropertyMetadata(new ObservableCollection<DataGridColumn>()));

public void LoadGrid()
{
  if (gridColumns.Count > 0)
    myGrid.Columns.Clear();

  foreach (DataGridColumn c in gridColumns)
  {
    myGrid.Columns.Add(c);
  }
}

Ребенок Ксамл:


<local:parentControl x:Name="deGrid">           
  <local:parentControl.gridColumns>
    <toolkit:DataGridTextColumn Width="Auto" Header="1" Binding="{Binding Path=.}" />
    <toolkit:DataGridTextColumn Width="Auto" Header="2" Binding="{Binding Path=.}" />
  </local:parentControl.gridColumns>  
</local:parentControl>

И, наконец, сложная задача - найти, где вызвать 'LoadGrid'.
Я борюсь с этим, но заставил вещи работать, вызвав после InitalizeComponent в моем конструкторе окна (childGrid - это x: name в window.xaml):

childGrid.deGrid.LoadGrid();

Связанные записи в блоге

1 голос
/ 26 ноября 2008

Вы можете сделать это с помощью AutoGenerateColumns и DataTemplate. Я не уверен, если это будет работать без большой работы, вам придется поиграть с этим. Честно говоря, если у вас уже есть работающее решение, я бы не стал вносить изменения, пока нет веской причины. Элемент управления DataGrid становится очень хорошим, но ему все еще нужно немного поработать (и мне еще многое предстоит сделать), чтобы можно было легко выполнять такие динамические задачи.

0 голосов
/ 21 октября 2016

Вот пример того, как я делаю программно:

public partial class UserControlWithComboBoxColumnDataGrid : UserControl
{
    private Dictionary<int, string> _Dictionary;
    private ObservableCollection<MyItem> _MyItems;
    public UserControlWithComboBoxColumnDataGrid() {
      _Dictionary = new Dictionary<int, string>();
      _Dictionary.Add(1,"A");
      _Dictionary.Add(2,"B");
      _MyItems = new ObservableCollection<MyItem>();
      dataGridMyItems.AutoGeneratingColumn += DataGridMyItems_AutoGeneratingColumn;
      dataGridMyItems.ItemsSource = _MyItems;

    }
private void DataGridMyItems_AutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
        {
            var desc = e.PropertyDescriptor as PropertyDescriptor;
            var att = desc.Attributes[typeof(ColumnNameAttribute)] as ColumnNameAttribute;
            if (att != null)
            {
                if (att.Name == "My Combobox Item") {
                    var comboBoxColumn =  new DataGridComboBoxColumn {
                        DisplayMemberPath = "Value",
                        SelectedValuePath = "Key",
                        ItemsSource = _ApprovalTypes,
                        SelectedValueBinding =  new Binding( "Bazinga"),   
                    };
                    e.Column = comboBoxColumn;
                }

            }
        }

}
public class MyItem {
    public string Name{get;set;}
    [ColumnName("My Combobox Item")]
    public int Bazinga {get;set;}
}

  public class ColumnNameAttribute : Attribute
    {
        public string Name { get; set; }
        public ColumnNameAttribute(string name) { Name = name; }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...