Come bind un DataGrid WPF a un numero variabile di colonne?

La mia applicazione WPF genera insiemi di dati che possono avere un numero diverso di colonne ogni volta. Incluso nell’output è una descrizione di ogni colonna che verrà utilizzata per applicare la formattazione. Una versione semplificata dell’output potrebbe essere qualcosa del tipo:

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

Questa class è impostata come DataContext su un WGrf DataGrid ma in realtà creo le colonne a livello di codice:

 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)) }); } 

Esiste un modo per sostituire questo codice con i binding di dati nel file XAML?

Ecco una soluzione alternativa per ribind le colonne in DataGrid. Poiché la proprietà Columns è ReadOnly, come tutti hanno notato, ho creato una proprietà associata denominata BindableColumns che aggiorna le colonne in DataGrid ogni volta che la raccolta cambia attraverso l’evento CollectionChanged.

Se abbiamo questa raccolta di DataGridColumn’s

 public ObservableCollection ColumnCollection { get; private set; } 

Quindi possiamo associare BindableColumns a ColumnCollection in questo modo

  

La proprietà allegata BindableColumns

 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); } } 

Ho continuato la mia ricerca e non ho trovato alcun modo ragionevole per farlo. La proprietà Columns su DataGrid non è qualcosa a cui posso bind, in effetti è di sola lettura.

Bryan ha suggerito che qualcosa potrebbe essere fatto con AutoGenerateColumns quindi ho dato un’occhiata. Usa semplice riflessione .Net per esaminare le proprietà degli oggetti in ItemsSource e genera una colonna per ognuno. Forse potrei generare un tipo al volo con una proprietà per ogni colonna, ma questo sta andando fuori strada.

Poiché questo problema è così facilmente sovledato nel codice, rimarrò con un semplice metodo di estensione che chiamo ogni volta che il contesto dei dati viene aggiornato con nuove colonne:

 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); 

Ho trovato un articolo del blog di Deborah Kurata con un bel trucco su come mostrare il numero variabile di colonne in un DataGrid:

Compilazione di un controllo DataGrid con colonne dinamiche in un’applicazione Silverlight mediante MVVM

Fondamentalmente, crea un DataGridTemplateColumn e inserisce ItemsControl all’interno che visualizza più colonne.

Sono riuscito a rendere ansible aggiungere dynamicmente una colonna usando solo una riga di codice come questa:

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

Riguardo alla domanda, questa non è una soluzione basata su XAML (poiché come accennato non esiste un modo ragionevole per farlo), né è una soluzione che opererebbe direttamente con DataGrid.Columns. Funziona in realtà con ItemsSource associato a DataGrid, che implementa ITypedList e in quanto tale fornisce metodi personalizzati per il recupero di PropertyDescriptor. In un posto nel codice puoi definire “righe di dati” e “colonne di dati” per la tua griglia.

Se tu avessi:

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

potresti usare per esempio:

 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) 

e la griglia che utilizza l’associazione a MyItemsCollection verrà popolata con le colonne corrispondenti. Queste colonne possono essere modificate (nuove aggiunte o esistenti rimosse) in fase di esecuzione in modo dinamico e la griglia aggiornerà automaticamente la raccolta delle colonne.

DynamicPropertyDescriptor di cui sopra è solo un aggiornamento al normale PropertyDescriptor e fornisce una definizione delle colonne fortemente tipizzata con alcune opzioni aggiuntive. DynamicDataGridSource avrebbe altrimenti funzionato correttamente con PropertyDescriptor di base.

Crea una versione della risposta accettata che gestisce la disiscrizione.

 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); } } 

Puoi creare un usercontrol con la definizione della griglia e definire i controlli “figlio” con varie definizioni di colonna in xaml. Il genitore ha bisogno di una proprietà di dipendenza per le colonne e un metodo per caricare le colonne:

Genitore:


 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); } } 

Bambino Xaml:


       

E infine, la parte difficile è trovare dove chiamare “LoadGrid”.
Sto lottando con questo, ma ho fatto funzionare le cose chiamando InitalizeComponent nel mio costruttore di windows (childGrid è x: name in window.xaml):

 childGrid.deGrid.LoadGrid(); 

Post di blog correlati

Potresti riuscire a farlo con AutoGenerateColumns e DataTemplate. Non sono sicuro che se funzionasse senza molto lavoro, dovresti giocarci. Sinceramente, se già disponi di una soluzione funzionante, non apporterei ancora il cambiamento a meno che non ci sia un grosso motivo. Il controllo DataGrid sta diventando molto buono, ma ha ancora bisogno di un po ‘di lavoro (e ho ancora molto da imparare) per essere in grado di svolgere facilmente compiti dinamici come questo.

C’è un esempio del mio modo di programmazione:

 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; } }