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

Moo*_*ght 5 c# data-binding wpf datagrid mvvm

全部,我有一个绑定到DataGrid使用MVVM 的View模型.

<DataGrid ItemsSource="{Binding Path=Resources}">...</DataGrid>
Run Code Online (Sandbox Code Playgroud)

哪里

public ObservableCollection<ResourceViewModel> Resources { get; private set; }
Run Code Online (Sandbox Code Playgroud)

ResourceViewModel课堂上我有以下属性

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

public ObservableCollection<string> ResourceStringList
{
    get { return this.resource.ResourceStringList; }
    set {
        ...
    }
}
Run Code Online (Sandbox Code Playgroud)

所有属性都显示在该集合中,DataGridResourceStringList集合显示为"(集合)".

如何才能DataGrid显示ResourceStringList其自己列中包含的每个字符串?

非常感谢你花时间陪伴.


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

ResourceStudio

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

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

再次感谢您的时间.

Mar*_*arc 11

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

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

双向约束(允许编辑)

样本项目(100KB)

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

Grid和列类:

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>();
    }
}
Run Code Online (Sandbox Code Playgroud)

用法:

<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>
Run Code Online (Sandbox Code Playgroud)

以及我用于测试的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"};
    }
}
Run Code Online (Sandbox Code Playgroud)

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

自定义网格

附录:

关于新问题和您的评论:

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

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

<loc:SeedColumn CollectionName="Strings" Visibility="Collapsed">
Run Code Online (Sandbox Code Playgroud)

添加列标题并不困难,但您需要考虑从哪里获取.当您将所有字符串存储在列表中时,它们只是字符串,因此与您可以用作标题的第二个字符串无关.我已经实现了一种纯粹在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>
Run Code Online (Sandbox Code Playgroud)

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

单向绑定(无数据编辑)

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

它是如何工作的

  • 创建一个实现的自定义值转换器类 IValueConverter
  • 在XAML资源中创建此类的实例并为其命名
  • ItemsSource用这个转换器绑定网格

这就是它的样子(我的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();
    }
}
Run Code Online (Sandbox Code Playgroud)

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

<loc:ResourceConverter x:Key="Converter" />
Run Code Online (Sandbox Code Playgroud)

然后像这样使用它:

<DataGrid ItemsSource="{Binding Resources, Converter={StaticResource Converter}}" />
Run Code Online (Sandbox Code Playgroud)