如何将WPF DataGrid绑定到可变数量的列?

我的WPF应用程序会生成一组数据,每次可能有不同数量的列。 在输出中包含将用于应用格式的每一列的描述。 输出的简化版本可能是这样的:

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

这个类被设置为WPF DataGrid上的DataContext,但我实际上是以编程方式创建列:

 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文件中的数据绑定代替这个代码?

这里是DataGrid中的Binding Columns的解决方法。 由于Columns属性是ReadOnly,就像所有人都注意到的那样,我创建了一个名为BindableColumns的附加属性,每当集合更改为CollectionChanged事件时,就更新DataGrid中的列。

如果我们有这个DataGridColumn的Collection

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

我继续我的研究,并没有找到任何合理的方式来做到这一点。 DataGrid上的Columns属性并不是我可以绑定的,实际上它是只读的。

Bryan建议使用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++)) }); } } // Eg myGrid.GenerateColumns(schema); 

我发现了Deborah Kurata的博客文章,介绍了如何在DataGrid中显示可变数量的列:

使用MVVM在Silverlight应用程序中使用动态列填充DataGrid

基本上,她创建一个DataGridTemplateColumn并把ItemsControl里面,显示多个列。

我设法使用这样一行代码动态地添加一列:

 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正常工作。

您可以使用网格定义创建一个用户控件,并使用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); } } 

孩子Xaml:


 <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:window.xaml中的名称):

 childGrid.deGrid.LoadGrid(); 

相关博客条目

制作了处理退订的接受答案的版本。

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

您可以使用AutoGenerateColumns和DataTemplate完成此操作。 如果没有很多工作,我就不积极,你将不得不玩弄它。 老实说,如果你已经有一个工作的解决方案,我不会做出改变,除非有很大的原因。 DataGrid控件变得非常好,但它仍然需要一些工作(而且我还有很多学习要做)能够轻松地完成这样的动态任务。

有一个我以编程方式做的样本:

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