Как связать WPF DataGrid с переменным числом столбцов?

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

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

Этот class задается как 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-файле?

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

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

 public ObservableCollection ColumnCollection { get; private set; } 

Затем мы можем привязать BindableColumns к ColumnCollection как это

  

Связанные свойства Связанные объекты

 public class DataGridColumnsBehavior { public static readonly DependencyProperty BindableColumnsProperty = DependencyProperty.RegisterAttached("BindableColumns", typeof(ObservableCollection), typeof(DataGridColumnsBehavior), new UIPropertyMetadata(null, BindableColumnsPropertyChanged)); private static void BindableColumnsPropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e) { DataGrid dataGrid = source as DataGrid; ObservableCollection columns = e.NewValue as ObservableCollection; 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 value) { element.SetValue(BindableColumnsProperty, value); } public static ObservableCollection GetBindableColumns(DependencyObject element) { return (ObservableCollection)element.GetValue(BindableColumnsProperty); } } 

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

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

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

 public static void GenerateColumns(this DataGrid dataGrid, IEnumerable 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++)) }); } } // Eg myGrid.GenerateColumns(schema); 

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

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

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

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

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

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

Если у вас есть:

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

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

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

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

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

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

 public class DataGridColumnsBehavior { public static readonly DependencyProperty BindableColumnsProperty = DependencyProperty.RegisterAttached("BindableColumns", typeof(ObservableCollection), typeof(DataGridColumnsBehavior), new UIPropertyMetadata(null, BindableColumnsPropertyChanged)); /// Collection to store collection change handlers - to be able to unsubscribe later. private static readonly Dictionary _handlers; static DataGridColumnsBehavior() { _handlers = new Dictionary(); } private static void BindableColumnsPropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e) { DataGrid dataGrid = source as DataGrid; ObservableCollection oldColumns = e.OldValue as ObservableCollection; 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 newColumns = e.NewValue as ObservableCollection; 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 value) { element.SetValue(BindableColumnsProperty, value); } public static ObservableCollection GetBindableColumns(DependencyObject element) { return (ObservableCollection)element.GetValue(BindableColumnsProperty); } } 

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

родитель:


 public ObservableCollection gridColumns { get { return (ObservableCollection)GetValue(ColumnsProperty); } set { SetValue(ColumnsProperty, value); } } public static readonly DependencyProperty ColumnsProperty = DependencyProperty.Register("gridColumns", typeof(ObservableCollection), typeof(parentControl), new PropertyMetadata(new ObservableCollection())); public void LoadGrid() { if (gridColumns.Count > 0) myGrid.Columns.Clear(); foreach (DataGridColumn c in gridColumns) { myGrid.Columns.Add(c); } } 

Ребенок Xaml:


       

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

 childGrid.deGrid.LoadGrid(); 

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

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

Существует пример того, как я программно:

 public partial class UserControlWithComboBoxColumnDataGrid : UserControl { private Dictionary _Dictionary; private ObservableCollection _MyItems; public UserControlWithComboBoxColumnDataGrid() { _Dictionary = new Dictionary(); _Dictionary.Add(1,"A"); _Dictionary.Add(2,"B"); _MyItems = new ObservableCollection(); 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; } } 
  • Разница между {Binding PropertyName} и {Binding Path = PropertyName}
  • как связать datatable с datagridview в c #
  • Связывание Datagrid в WPF
  • DataTrigger, где значение НЕ равно нулю?
  • WPF DataGrid: свойство CanContentScroll, вызывающее нечетное поведение
  • В .NET 4.0 привязка OneWayToSource
  • «Элемент коллекции должен быть пустым перед использованием ItemsSource».
  • ComboBox.SelectedValue не обновляется из источника привязки
  • Как сделать привязки в ItemContainerStyle в WinRT?
  • MVVM: привязка переключателей к модели просмотра?
  • Связывание WPF DataGrid с DataTable с использованием TemplateColumns
  • Давайте будем гением компьютера.