¿Cómo puedo vincular un WGD DataGrid con un número variable de columnas?

Mi aplicación WPF genera conjuntos de datos que pueden tener un número diferente de columnas cada vez. Se incluye en el resultado una descripción de cada columna que se utilizará para aplicar el formato. Una versión simplificada de la salida podría ser algo como:

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

Esta clase se establece como DataContext en WPF DataGrid, pero en realidad creo las columnas mediante progtwigción:

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

¿Hay alguna forma de reemplazar este código con enlaces de datos en el archivo XAML?

Aquí hay una solución alternativa para Columnas de unión en DataGrid. Dado que la propiedad Columns es ReadOnly, como todos notaron, hice una propiedad adjunta llamada BindableColumns que actualiza las columnas en el DataGrid cada vez que la colección cambia a través del evento CollectionChanged.

Si tenemos esta Colección de DataGridColumn’s

 public ObservableCollection ColumnCollection { get; private set; } 

Entonces podemos unir BindableColumns a ColumnCollection como este

  

La propiedad adjunta Bindable Columnas

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

He continuado mi investigación y no he encontrado ninguna forma razonable de hacerlo. La propiedad Columns en DataGrid no es algo contra lo que pueda vincularme, de hecho es de solo lectura.

Bryan sugirió que se podría hacer algo con AutoGenerateColumns, así que eché un vistazo. Utiliza una simple reflexión .Net para observar las propiedades de los objetos en ItemsSource y genera una columna para cada uno. Tal vez podría generar un tipo sobre la marcha con una propiedad para cada columna, pero esto se está desviando.

Dado que este problema es tan fácil de codificar en el código, me quedaré con un método de extensión simple al que llamo siempre que el contexto de datos se actualice con nuevas columnas:

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

He encontrado un artículo de blog de Deborah Kurata con un buen truco de cómo mostrar el número variable de columnas en un DataGrid:

Poblar un DataGrid con Columnas dinámicas en una aplicación de Silverlight usando MVVM

Básicamente, crea una DataGridTemplateColumn y coloca ItemsControl dentro que muestra varias columnas.

Pude hacer que sea posible agregar dinámicamente una columna usando solo una línea de código como esta:

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

Con respecto a la pregunta, esta no es una solución basada en XAML (ya que como se mencionó no hay una forma razonable de hacerlo), tampoco es una solución que funcionaría directamente con DataGrid.Columns. En realidad, funciona con DataSource enlazado ItemsSource, que implementa ITypedList y, como tal, proporciona métodos personalizados para la recuperación de PropertyDescriptor. En un lugar del código, puede definir “filas de datos” y “columnas de datos” para su cuadrícula.

Si tuvieras:

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

podrías usar por ejemplo:

 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) 

y su cuadrícula que utiliza el enlace a MyItemsCollection se completará con las columnas correspondientes. Esas columnas se pueden modificar (nuevas añadidas o eliminadas) en tiempo de ejecución de forma dinámica y la cuadrícula actualizará automáticamente su colección de columnas.

DynamicPropertyDescriptor mencionado anteriormente es solo una actualización de PropertyDescriptor regular y proporciona una definición de columnas fuertemente tipadas con algunas opciones adicionales. DynamicDataGridSource de lo contrario funcionaría bien evento con PropertyDescriptor básico.

Hizo una versión de la respuesta aceptada que maneja la cancelación de la suscripción.

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

Puede crear un control de usuario con la definición de cuadrícula y definir controles ‘secundarios’ con definiciones de columna variadas en xaml. El padre necesita una propiedad de dependencia para las columnas y un método para cargar las columnas:

Padre:


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

Niño Xaml:


       

Y finalmente, la parte difícil es encontrar dónde llamar a ‘LoadGrid’.
Estoy luchando con esto pero hice que las cosas funcionen llamando a InitalizeComponent en mi constructor de ventana (childGrid es x: name en window.xaml):

 childGrid.deGrid.LoadGrid(); 

Entrada de blog relacionada

Es posible que pueda hacer esto con AutoGenerateColumns y DataTemplate. No estoy seguro si funcionaría sin mucho trabajo, tendrías que jugar con eso. Honestamente, si ya tienes una solución de trabajo, yo no haría el cambio por ahora a menos que haya una gran razón. El control DataGrid se está volviendo muy bueno, pero aún necesita algo de trabajo (y todavía me queda mucho por aprender) para poder hacer tareas dinámicas como esta fácilmente.

Hay una muestra de la forma en que hago programáticamente:

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