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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/320089/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-03 23:03:57  来源:igfitidea点击:

How do I bind a WPF DataGrid to a variable number of columns?

c#wpfxamldata-bindingdatagrid

提问by Generic Error

My WPF application generates sets of data which may have a different number of columns each time. Included in the output is a description of each column that will be used to apply formatting. A simplified version of the output might be something like:

我的 WPF 应用程序生成的数据集每次可能具有不同的列数。输出中包含对将用于应用格式设置的每一列的描述。输出的简化版本可能类似于:

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

This class is set as the DataContext on a WPF DataGrid but I actually create the columns programmatically:

此类设置为 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))
    });
}

Is there any way to replace this code with data bindings in the XAML file instead?

有没有办法用 XAML 文件中的数据绑定替换此代码?

采纳答案by Fredrik Hedblad

Here's a workaround for Binding Columns in the DataGrid. Since the Columns property is ReadOnly, like everyone noticed, I made an Attached Property called BindableColumns which updates the Columns in the DataGrid everytime the collection changes through the CollectionChanged event.

这是在 DataGrid 中绑定列的解决方法。由于 Columns 属性是只读的,就像每个人都注意到的那样,我创建了一个名为 BindableColumns 的附加属性,每次通过 CollectionChanged 事件更改集合时,它都会更新 DataGrid 中的列。

If we have this Collection of DataGridColumn's

如果我们有这个 DataGridColumn 的集合

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

Then we can bind BindableColumns to the ColumnCollection like this

然后我们可以像这样将 BindableColumns 绑定到 ColumnCollection

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

The Attached Property BindableColumns

附加属性 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);
    }
}

回答by Bryan Anderson

You might be able to do this with AutoGenerateColumns and a DataTemplate. I'm not positive if it would work without a lot of work, you would have to play around with it. Honestly if you have a working solution already I wouldn't make the change just yet unless there's a big reason. The DataGrid control is getting very good but it still needs some work (and I have a lot of learning left to do) to be able to do dynamic tasks like this easily.

您也许可以使用 AutoGenerateColumns 和 DataTemplate 来做到这一点。我不肯定它是否可以在没有大量工作的情况下工作,您将不得不玩弄它。老实说,如果你已经有了一个可行的解决方案,除非有很大的原因,否则我不会做出改变。DataGrid 控件变得非常好,但它仍然需要一些工作(我还有很多学习要做)才能轻松完成这样的动态任务。

回答by Generic Error

I've continued my research and have not found any reasonable way to do this. The Columns property on the DataGrid isn't something I can bind against, in fact it's read only.

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

Bryan suggested something might be done with AutoGenerateColumns so I had a look. It uses simple .Net reflection to look at the properties of the objects in ItemsSource and generates a column for each one. Perhaps I could generate a type on the fly with a property for each column but this is getting way off track.

Bryan 建议可以用 AutoGenerateColumns 做一些事情,所以我看了看。它使用简单的 .Net 反射来查看 ItemsSource 中对象的属性,并为每个对象生成一列。也许我可以动态生成一个类型,每个列都有一个属性,但这已经偏离了轨道。

Since this problem is so easily sovled in code I will stick with a simple extension method I call whenever the data context is updated with new columns:

由于这个问题在代码中很容易解决,我将坚持使用一个简单的扩展方法,每当数据上下文使用新列更新时,我都会调用它:

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

回答by Andy

You can create a usercontrol with the grid definition and define 'child' controls with varied column definitions in xaml. The parent needs a dependency property for columns and a method for loading the columns:

您可以使用网格定义创建用户控件,并在 xaml 中定义具有不同列定义的“子”控件。父级需要列的依赖属性和加载列的方法:

Parent:

家长:



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

Child Xaml:

子 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>


And finally, the tricky part is finding where to call 'LoadGrid'.
I am struggling with this but got things to work by calling after InitalizeComponentin my window constructor (childGrid is x:name in window.xaml):

最后,棘手的部分是找到在哪里调用“LoadGrid”。
我正在为此苦苦挣扎,但通过InitalizeComponent在我的窗口构造函数中调用 after 使事情开始工作(childGrid 是 x:name in window.xaml):

childGrid.deGrid.LoadGrid();

Related blog entry

相关博文

回答by Lukas Cenovsky

I have found a blog article by Deborah Kurata with a nice trick how to show variable number of columns in a DataGrid:

我找到了 Deborah Kurata 的一篇博客文章,其中有一个很好的技巧,即如何在 DataGrid 中显示可变数量的列:

Populating a DataGrid with Dynamic Columns in a Silverlight Application using MVVM

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

Basically, she creates a DataGridTemplateColumnand puts ItemsControlinside that displays multiple columns.

基本上,她创建了一个DataGridTemplateColumn并放入ItemsControl显示多列的内容。

回答by doblak

I managed to make it possible to dynamically add a column using just a line of code like this:

我设法只使用这样的一行代码就可以动态添加一列:

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

Regarding to the question, this is not a XAML-based solution (since as mentioned there is no reasonable way to do it), neither it is a solution which would operate directly with DataGrid.Columns. It actually operates with DataGrid bound ItemsSource, which implements ITypedList and as such provides custom methods for PropertyDescriptor retrieval. In one place in code you can define "data rows" and "data columns" for your grid.

关于这个问题,这不是基于 XAML 的解决方案(因为如上所述没有合理的方法来做到这一点),也不是直接与 DataGrid.Columns 一起操作的解决方案。它实际上与 DataGrid 绑定的 ItemsSource 一起操作,它实现了 ITypedList 并因此提供了用于 PropertyDescriptor 检索的自定义方法。在代码的一处,您可以为您的网格定义“数据行”和“数据列”。

If you would have:

如果你有:

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

you could use for example:

你可以使用例如:

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) 

and your grid using binding to MyItemsCollection would be populated with corresponding columns. Those columns can be modified (new added or existing removed) at runtime dynamically and grid will automatically refresh it's columns collection.

并且使用绑定到 MyItemsCollection 的网格将填充相应的列。这些列可以在运行时动态修改(新添加或现有删除),网格将自动刷新它的列集合。

DynamicPropertyDescriptor mentioned above is just an upgrade to regular PropertyDescriptor and provides strongly-typed columns definition with some additional options. DynamicDataGridSource would otherwise work just fine event with basic PropertyDescriptor.

上面提到的 DynamicPropertyDescriptor 只是对常规 PropertyDescriptor 的升级,并提供了带有一些附加选项的强类型列定义。否则,DynamicDataGridSource 将与基本 PropertyDescriptor 一起正常工作。

回答by Mikhail Orlov

Made a version of the accepted answer that handles unsubscription.

制作了处理取消订阅的已接受答案的版本。

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

回答by David Soler

There is a sample of the way I do programmatically:

有一个我以编程方式做的方式的示例:

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