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

5

大家好,我有一个视图模型,使用MVVM绑定到一个DataGrid

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

Where

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

ResourceViewModel类中,我有以下属性。
public string ResourceName
{
    get { return this.resource.ResourceName; }
    set { 
        ...
    }
}

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

所有属性都显示在 DataGrid 中,但是 ResourceStringList 集合被显示为“(Collection)”。

我该如何使 DataGrid 显示 ResourceStringList 中包含的每个字符串在其自己的列中?

非常感谢您的时间。


编辑。 我已经实现了 @Marc 的建议。 我现在有以下截图来说明我现在需要什么:

ResourceStudio

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

我还想知道如何向我的资源列添加列名称?也许我可以只将 SeedColumnHeader 属性绑定到一个 Binding 上。

再次感谢您的时间。


1
好的,那么这是固定列数吗?这些列中会有什么内容?它们会有标题吗?如果有,它们需要按特定顺序吗? - dkozl
1
@Killercam,很高兴你已经成功运行了它。我已经编辑了我的答案以回答你的问题,并上传了我的示例项目。希望这个头文件解决方案对你有用? - Marc
1
谢谢,希望它能为您解决问题。我去拿杯咖啡... - Marc
1
别担心,学习WPF非常困难,特别是如果你是自学的。我也是这样做的,花了几个月的时间,说实话... - Marc
1
忘了告诉你,我已经将修复后的项目上传到你的SkyDrive上了。 - Marc
显示剩余10条评论
2个回答

12

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

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

双向绑定(允许编辑)

示例项目(100KB)

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

网格和列类:

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

使用方法:

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

我使用过的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"};
    }
}

并且外观(旧版没有标题):

自定义网格

ADDENDUM:

Regarding the new questions and your comment:

The NullReferenceException can 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 column that 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 Name of the resource. If you don't need the seedcolumn at all, just set its Visibility to collapsed.

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

Adding column headers is 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 Headers property of the seed column is bindable as well, you can bind it to any 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();
    }
}

然后在您的XAML中定义一个资源,如下所示,其中“loc”是您的命名空间:

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

然后像这样使用它:
<DataGrid ItemsSource="{Binding Resources, Converter={StaticResource Converter}}" />

@Killercam 你好!抱歉,我还没有来得及看。你在这段时间里解决了吗?否则我会尽快查看。 - Marc
1
@Killercam:我已经看过了。界面看起来很不错,干得好。我稍微改动了ResourceDataGrid,使其在策略上引发相应事件时进行更新。你应该再调整一下:目前还没有处理哪个资源已经更改,因此可能有点低效。希望现在对你有所帮助。告诉我一声。干杯!:https://dl.dropboxusercontent.com/u/14429255/ResourceStudio_revMarc.zip - Marc
非常感谢Marc。我会学习你所做的,并在周末进行测试。非常感激... - MoonKnight
快速问题。我正在尝试实现撤销功能,进展顺利。但是,我似乎无法找到自定义“DataGrid”(“ResourceDataGrid”)中包含的项设置在哪里 - 只有绑定集合的获取操作似乎被激发。该值确实已更新,但我迷失了在实际发生时如何发生的。绑定工作正常 - 迷失是一个轻描淡写的说法。如果我问一个问题,你能看一下吗?我会提供奖励...:] - MoonKnight
谢谢Marc。我已经通过代码走了很长的路程,看起来很棒。然而,以下链接是你上次给我的最后一份代码,因为那时候也出现了这个问题...http://sdrv.ms/14PNYim 'ResourceStudio130809.zip'。问题在于,如果您更改第二列和第三列的单元格值,则会引发OnPropertyChanged事件。但是,如果您更改了您设计的动态列,则实际值将更改为绑定集合中的值,但似乎没有调用任何“set”;值如何更新,以及如何引发值更改事件? - MoonKnight
显示剩余15条评论

1
我认为没有现成的解决方案可以解决你的问题,你需要手动创建网格列。在我的情况下,我会在加载我的 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);
   }
}

这里有一个关于Dropbox的简单示例


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接