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

时间:2013-06-02 17:51:28

标签: c# wpf data-binding mvvm datagrid

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

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

哪里

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收集显示为'(收藏)'。

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

非常感谢你的时间。


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

ResourceStudio

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

我还想知道如何在我的资源列中添加列名?也许我只能添加BindingHeader的{​​{1}}属性{1}}。

再次感谢您的时间。

2 个答案:

答案 0 :(得分:11)

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

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

双向约束(允许编辑)

The sample project (100KB)

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

网格和列类:

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

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

custom grid

  

<强>附录:

     

关于新问题和您的评论:

     

NullReferenceException 可能有几个原因,但很明显   解决了它。然而,它出现的线是一点意大利面   代码,我不会在生产代码中这样做。你需要   在任何情况下处理可能出错的事情...我已经修改了   代码并将该行重构为自己的方法。这会给你   当抛出异常时,想知道发生了什么。

     

您看到的空列是种子列,显然没有绑定任何内容。我的想法是将此列用作一种行   标头并将其绑定到资源的Name。如果你不需要   种子柱,只需将其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属性   种子列的可绑定也可以绑定到任何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}}" />

答案 1 :(得分: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

上的一些简单示例