wpf 如何在运行时使用 MVVM 将 List<object> 绑定到 DataGrid

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/16885577/
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-09-13 08:54:13  来源:igfitidea点击:

How to Bind a List<object> to DataGrid using MVVM at Runtime

c#wpfdata-bindingmvvmdatagrid

提问by MoonKnight

All, I have an View model that is bound to a DataGridusing MVVM.

所有,我有一个绑定到DataGrid使用 MVVM的视图模型。

<DataGrid ItemsSource="{Binding Path=Resources}">...</DataGrid>

Where

在哪里

public ObservableCollection<ResourceViewModel> Resources { get; private set; }

in the ResourceViewModelclass I have the following properties

ResourceViewModel课堂上我有以下属性

public string ResourceName
{
    get { return this.resource.ResourceName; }
    set { 
        ...
    }
}

public ObservableCollection<string> ResourceStringList
{
    get { return this.resource.ResourceStringList; }
    set {
        ...
    }
}

All properties are displayed in the DataGridbut the ResourceStringListcolletion is being displayed as '(Collection)'.

所有属性都显示在,DataGridResourceStringList集合显示为“(集合)”。

How can I get the DataGridto display each of the strings contained in the ResourceStringListin its own column?

我怎样才能让DataGrid显示ResourceStringList在它自己的列中包含的每个字符串?

Thanks very much for your time.

非常感谢你花时间陪伴。



Edit. I have implemented the suggestion by @Marc below. I now have the following screenshot to illustrate what I now require:

编辑。我已经在下面实施了@Marc 的建议。我现在有以下屏幕截图来说明我现在需要的内容:

ResourceStudio

资源工作室

The blank column before my resources column index 3 (zero indexed) is not required, how do I remove this column?.

我的资源列索引 3(零索引)之前的空白列不是必需的,如何删除此列?.

I would also like to know how to add column names to my resource columns?Perhaps I can just add a Bindingto Headerproperty of the SeedColumn.

我还想知道如何将列名添加到我的资源列中?也许我可以再补充一个BindingHeader的财产SeedColumn

Again thanks for your time.

再次感谢您的时间。

回答by Marc

A datagrid is usually used to display a list of items of the same type with a fixed set of properties per item where each column is one property. So each row is one item, each column is one property on the item. You're case is different, as there is no fixed set of properties but a collection you want to show as if it were a fixed set of a number of properties.

数据网格通常用于显示相同类型的项目列表,每个项目具有一组固定的属性,其中每一列是一个属性。所以每一行是一个项目,每一列是项目的一个属性。您的情况有所不同,因为没有固定的属性集,但是您想要显示的集合就好像它是一组固定的属性集。

The way to go greatly depends on whether you only want to display the data or whether you want to allow the user to manipulate the data. While the first can be achieved relatively easy using value converters, the latter requires a little more coding to extend the DataGrid class to allow for this behavior. The solutions I show are two of a thousand possibilities and probably not the most elegant ones. That being said, I will describe both ways and start with the two-way version.

要走的路很大程度上取决于您是只想显示数据还是要允许用户操作数据。虽然使用值转换器可以相对容易地实现第一个,但后者需要更多的编码来扩展 DataGrid 类以允许这种行为。我展示的解决方案是一千种可能性中的两种,可能不是最优雅的。话虽如此,我将描述两种方式并从双向版本开始。

TWO-WAY BINDING (ALLOWS EDITING)

双向绑定(允许编辑)

The sample project (100KB)

示例项目 (100KB)

I created a custom DataGridand a custom 'DataGridColumn', called 'SeedColumn'. SeedColumnworks just as a textcolumn, but has a property CollectionName. The DataGridwill add one new text column per item in the collection you've specified in CollectionNameon the right hand side of the seed column. The seed column only works as a kind of placeholder to tell the DataGrid where to insert which columns. You could use multiple Seedcolumns in one grid.

我创建了一个自定义DataGrid和一个自定义的“DataGridColumn”,称为“SeedColumn”。SeedColumn就像一个文本列一样工作,但有一个属性CollectionName。该DataGrid将你在指定的集合中添加每个项目一个新的文本列CollectionName上的种子列的右侧。种子列仅用作一种占位符来告诉 DataGrid 在哪里插入哪些列。您可以在一个网格中使用多个 Seedcolumns。

The Grid and the column classes:

网格和列类:

public class HorizontalGrid : DataGrid
{
    protected override void OnItemsSourceChanged(System.Collections.IEnumerable oldValue, System.Collections.IEnumerable newValue)
    {
        base.OnItemsSourceChanged(oldValue, newValue);
        foreach (var seed in Columns.OfType<SeedColumn>().ToList())
        { 
            var seedColumnIndex = Columns.IndexOf(seed) + 1;
            var collectionName = seed.CollectionName;
            var headers = seed.Headers;

            // Check if ItemsSource is IEnumerable<object>
            var data = ItemsSource as IEnumerable<object>;
            if (data == null) return;

            // Copy to list to allow for multiple iterations
            var dataList = data.ToList();
            var collections = dataList.Select(d => GetCollection(collectionName, d));
            var maxItems = collections.Max(c => c.Count());

            for (var i = 0; i < maxItems; i++)
            {
                var header = GetHeader(headers, i);
                var columnBinding = new Binding(string.Format("{0}[{1}]" , seed.CollectionName , i));
                Columns.Insert(seedColumnIndex + i, new DataGridTextColumn {Binding = columnBinding, Header = header});
            }
        }
    }

    private static string GetHeader(IList<string> headerList, int index)
    {
        var listIndex = index % headerList.Count;
        return headerList[listIndex];
    }

    private static IEnumerable<object> GetCollection(string collectionName, object collectionHolder)
    {
        // Reflect the property which holds the collection
        var propertyInfo = collectionHolder.GetType().GetProperty(collectionName);
        // Get the property value of the property on the collection holder
        var propertyValue = propertyInfo.GetValue(collectionHolder, null);
        // Cast the value
        var collection = propertyValue as IEnumerable<object>;
        return collection;
    }
}

public class SeedColumn : DataGridTextColumn
{
    public static readonly DependencyProperty CollectionNameProperty =
        DependencyProperty.Register("CollectionName", typeof (string), typeof (SeedColumn), new PropertyMetadata(default(string)));

    public static readonly DependencyProperty HeadersProperty =
        DependencyProperty.Register("Headers", typeof (List<string>), typeof (SeedColumn), new PropertyMetadata(default(List<string>)));

    public List<string> Headers
    {
        get { return (List<string>) GetValue(HeadersProperty); }
        set { SetValue(HeadersProperty, value); }
    }

    public string CollectionName
    {
        get { return (string) GetValue(CollectionNameProperty); }
        set { SetValue(CollectionNameProperty, value); }
    }

    public SeedColumn()
    {
        Headers = new List<string>();
    }
}

The usage:

用法:

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:loc="clr-namespace:WpfApplication1"
        xmlns:system="clr-namespace:System;assembly=mscorlib" xmlns:sample="clr-namespace:Sample"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <sample:HorizontalGrid ItemsSource="{Binding Resources}" AutoGenerateColumns="False">
            <sample:HorizontalGrid.Columns>
                <sample:SeedColumn CollectionName="Strings" Binding="{Binding Name}" Header="Name" Visibility="Collapsed">
                    <sample:SeedColumn.Headers>
                        <system:String>Header1</system:String>
                        <system:String>Header2</system:String>
                        <system:String>Header3</system:String>
                        <system:String>Header4</system:String>
                    </sample:SeedColumn.Headers>
                </sample:SeedColumn>
            </sample:HorizontalGrid.Columns>
        </sample:HorizontalGrid>
    </Grid>
</Window>

and the ViewModels I've used for testing:

以及我用于测试的 ViewModels:

public class MainViewModel
{
    public ObservableCollection<ResourceViewModel> Resources { get; private set; }

    public MainViewModel()
    {
        Resources = new ObservableCollection<ResourceViewModel> {new ResourceViewModel(), new ResourceViewModel(), new ResourceViewModel()};
    }
}

public class ResourceViewModel
{
    private string _name;
    public string Name
    {
        get { return _name; }
        set { _name = value; }
    }

    public ObservableCollection<string> Strings { get; private set; }

    public ResourceViewModel()
    {
        Name = "Resource";
        Strings = new ObservableCollection<string> {"s1", "s2", "s3"};
    }
}

and the look (old version without headers):

和外观(没有标题的旧版本):

custom grid

自定义网格

ADDENDUM:

Regarding the new questions and your comment:

The NullReferenceExceptioncan have several reasons, but you've obviously solved it. However, the line where it occured is a bit of spaghetti code and I wouldn't do it like this in production code. You need to handle the things that can go wrong in any case... I've modified the code and refactored the line into its own method. This will give you an idea of what's going on, when the exception is thrown.

The empty columnthat you see is the seed column, which is obviously not bound to anything. My idea was to use this column as a kind of row header and bind it to the Nameof the resource. If you don't need the seedcolumn at all, just set its Visibilityto collapsed.

<loc:SeedColumn CollectionName="Strings" Visibility="Collapsed">

Adding column headersis not difficult, but you need to think about where you want to take the from. As you store all your strings in a list, they are just strings, so not related to a second string which you could use as a header. I've implemented a way to sepcify the columns purely in XAML, which might be enough for you for now: You can use it like this:

<loc:HorizontalGrid ItemsSource="{Binding Resources}" AutoGenerateColumns="False">
    <loc:HorizontalGrid.Columns>
        <loc:SeedColumn CollectionName="Strings" Binding="{Binding Name}" Header="Name" Visibility="Collapsed">
            <loc:SeedColumn.Headers>
                <system:String>Header1</system:String>
                <system:String>Header2</system:String>
                <system:String>Header3</system:String>
                <system:String>Header4</system:String>
            </loc:SeedColumn.Headers>
        </loc:SeedColumn>
    </loc:HorizontalGrid.Columns>
</loc:HorizontalGrid>

If you have more elements in the collection than headers specified, the column headers will be repeated "Header3", "Header4", "Header1",.. The implementation is straight forward. Note that the Headersproperty of the seed column is bindable as well, you can bind it to any List.

附录:

关于新问题和您的评论:

NullReferenceException异常可以有几个原因,但你显然解决它。然而,它发生的那一行是一些意大利面条式的代码,我不会在生产代码中这样做。你需要处理在任何情况下都可能出错的事情......我已经修改了代码并将该行重构为它自己的方法。这将使您了解在引发异常时发生了什么。

您看到的空列是种子列,它显然没有绑定任何东西。我的想法是将此列用作一种行标题并将其绑定到Name资源的 。如果您根本不需要seedcolumn,只需将其设置Visibility为折叠。

<loc:SeedColumn CollectionName="Strings" Visibility="Collapsed">

添加列标题并不困难,但您需要考虑要从哪里获取。当您将所有字符串存储在列表中时,它们只是字符串,因此与您可以用作标题的第二个字符串无关。我已经实现了一种纯粹在 XAML 中分离列的方法,这对您现在来说可能已经足够了:您可以像这样使用它:

<loc:HorizontalGrid ItemsSource="{Binding Resources}" AutoGenerateColumns="False">
    <loc:HorizontalGrid.Columns>
        <loc:SeedColumn CollectionName="Strings" Binding="{Binding Name}" Header="Name" Visibility="Collapsed">
            <loc:SeedColumn.Headers>
                <system:String>Header1</system:String>
                <system:String>Header2</system:String>
                <system:String>Header3</system:String>
                <system:String>Header4</system:String>
            </loc:SeedColumn.Headers>
        </loc:SeedColumn>
    </loc:HorizontalGrid.Columns>
</loc:HorizontalGrid>

如果集合中的元素多于指定的标题,则列标题将重复“Header3”、“Header4”、“Header1”等。实现很简单。请注意,Headers种子列的属性也是可绑定的,您可以将其绑定到任何列表。

ONE-WAY BINDING (NO EDITING OF THE DATA)

单向绑定(不编辑数据)

A straight-forward way is to implement a converter which formats your data in a table and returns a view on this table to which the DataGrid can be bound. The disadvantage: It does not allow editing the strings, because once the table is created from the original data source, no logical connection between the displayed data and the original data exists. Still, changes on the collection are reflected in the UI, as WPF performs the conversion every time the data source changes. In short: This solution is perfectly fine if you only want to display the data.

一种直接的方法是实现一个转换器,该转换器在表中格式化您的数据并返回该表上的视图,DataGrid 可以绑定到该视图。缺点:不允许编辑字符串,因为一旦从原始数据源创建表,显示的数据和原始数据之间就不存在逻辑连接。尽管如此,集合上的更改仍会反映在 UI 中,因为每次数据源更改时,WPF 都会执行转换。简而言之:如果您只想显示数据,此解决方案非常好。

How does it work

它是如何工作的

  • Create a custom value converter class, which implements IValueConverter
  • Create an instance of this class in your XAML resources and give it a name
  • Bind the grid's ItemsSourcewith this converter
  • 创建一个自定义值转换器类,它实现 IValueConverter
  • 在 XAML 资源中创建此类的实例并为其命名
  • ItemsSource用这个转换器绑定网格

This is how it would look like (my IDE is StackOverflow, so please check and correct, if necessary):

这是它的样子(我的 IDE 是 StackOverflow,所以如果需要,请检查并更正):

public class ResourceConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        var resources = value as IEnumerable<ResourceViewModel>;
        if (resources== null) return null;

        // Better play safe and serach for the max count of all items
        var columns = resources[0].ResourceStringList.Count;

        var t = new DataTable();
        t.Columns.Add(new DataColumn("ResourceName"));

        for (var c = 0; c < columns; c++)
        {
            // Will create headers "0", "1", "2", etc. for strings
            t.Columns.Add(new DataColumn(c.ToString()));
        }

        foreach (var r in resources)
        {
            var newRow = t.NewRow();

            newRow[0] = resources.ResourceName;

            for (var c = 0; c < columns; c++)
            {
                newRow[c+1] = r.ResourceStringList[c];
            }

            t.Rows.Add(newRow);
        }


        return t.DefaultView;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

Then define a resource in your XAML like this, where loc is your namespace:

然后像这样在 XAML 中定义一个资源,其中 loc 是您的命名空间:

<loc:ResourceConverter x:Key="Converter" />

and then use it like this:

然后像这样使用它:

<DataGrid ItemsSource="{Binding Resources, Converter={StaticResource Converter}}" />

回答by dkozl

I don't think there is a out of the box solution for your problem and your grid columns will have to be created manually. In my case I do it when my DataGridis loaded. I worked on assumption that number of columns is fixed for each element, 10 in my example, and that they are in correct order:

我认为您的问题没有现成的解决方案,您的网格列必须手动创建。在我的情况下,我DataGrid在加载时这样做。我假设每个元素的列数是固定的,在我的示例中为 10,并且它们的顺序正确:

private void DataGrid_Loaded(object sender, RoutedEventArgs e)
{
   var dataGrid = sender as DataGrid;
   dataGrid.Columns.Clear();
   DataGridTextColumn resourceName = new DataGridTextColumn();
   resourceName.Header = "Name";
   resourceName.Binding = new Binding("ResourceName");
   dataGrid.Columns.Add(resourceName);
   for (int i = 0; i < 10; i++)
   {
       var resourceColumn = new DataGridTextColumn();
       resourceColumn.Header = "Resource " + i;
       resourceColumn.Binding = new Binding(String.Format("ResourceStringList[{0}]", i)) { Mode = BindingMode.TwoWay, UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged };
       dataGrid.Columns.Add(resourceColumn);
   }
}

here is some simple example on Dropbox

这是Dropbox上的一些简单示例