如何绑定列表< object>到运行时使用MVVM的DataGrid [英] How to Bind a List<object> to DataGrid using MVVM at Runtime

查看:79
本文介绍了如何绑定列表< object>到运行时使用MVVM的DataGrid的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

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

其中

  public ObservableCollection< ResourceViewModel>资源{get;私人集合在$ code> ResourceViewModel 类中,

以下属性

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

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

所有属性都显示在 DataGrid 中,但$ ResourceStringList colletion显示为(Collection)。



如何获取 DataGrid 以显示$ code> ResourceStringList中包含的每个字符串



非常感谢您的时间。






编辑。我已经执行了@Marc的建议。我现在有以下屏幕截图来说明我现在需要的:





我的资源列索引3(零索引)之前的空白栏不需要,删除此列?



我也想知道如何在列表中添加列名称? c $ c> c $ c> c / code>。



再次感谢您的时间。

解决方案

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



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



双向绑定(允许编辑) p>

示例项目(100KB)



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



网格和列类:

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

//检查ItemsSource是否为IEnumerable< object>
var data = ItemsSource as IEnumerable< object> ;;
if(data == null)return;

//复制到列表以允许多次迭代
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});
}
}
}

私有静态字符串GetHeader(IList< string> headerList,int index)
{
var listIndex = index %headerList.Count;
return headerList [listIndex];
}

private static IEnumerable< object> GetCollection(string collectionName,object collectionHolder)
{
//反映拥有集合的属性
var propertyInfo = collectionHolder.GetType()。GetProperty(collectionName);
//获取收藏夹上的属性的属性值
var propertyValue = propertyInfo.GetValue(collectionHolder,null);
//将值
var collection = propertyValue转换为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),新的PropertyMetadata(默认(List< string>)) );

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

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

public SeedColumn()
{
标题=新列表< string>();
}
}

用法:

 < Window x:Class =WpfApplication1.MainWindow
xmlns =http://schemas.microsoft.com/winfx/2006/xaml / presentation
xmlns:x =http://schemas.microsoft.com/winfx/2006/xamlxmlns:loc =clr-namespace:WpfApplication1
xmlns:system =clr-命名空间:System; assembly = mscorlibxmlns:sample =clr-namespace:Sample
Title =MainWindowHeight =350Width =525>
< Grid>
< sample:Horizo​​ntalGrid ItemsSource ={Binding Resources}AutoGenerateColumns =False>
< sample:Horizo​​ntalGrid.Columns>
< sample:SeedColumn CollectionName =StringsBinding ={Binding Name}Header =NameVisibility =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:Horizo​​ntalGrid.Columns>
< / sample:Horizo​​ntalGrid>
< / Grid>
< / Window>

和我用于测试的ViewModels:

  public class MainViewModel 
{
public ObservableCollection< ResourceViewModel>资源{get;私人集合}

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

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

public ObservableCollection< string>字符串{get;私人集合}

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

和外观(旧版本无标题):




ADDENDUM:



关于新问题和您的评论:



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



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

 < loc:SeedColumn CollectionName =StringsVisibility =Collapsed> 

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

 < loc:Horizo​​ntalGrid ItemsSource ={Binding Resources}AutoGenerateColumns =False> 
< loc:Horizo​​ntalGrid.Columns>
< loc:SeedColumn CollectionName =StringsBinding ={Binding Name}Header =NameVisibility =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:Horizo​​ntalGrid.Columns>
< / loc:Horizo​​ntalGrid>

如果集合中有更多的元素指定了标题,
列标题将重复Header3,Header4,Header1,..
实现是直截了当的。请注意,种子列的标头属性
也是可绑定的,您可以将其绑定到任何列表。




单向绑定(无数据编辑)



一种直接的方式是实现一个转换器,它将数据格式化在一个表中,并返回一个可以绑定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 =值为IEnumerable< ResourceViewModel> ;;
if(resources == null)return null;

//更好地播放所有项目的最大计数的安全和serach
var columns = resources [0] .ResourceStringList.Count;

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

(var c = 0; c< columns; c ++)
{
//将创建标题0,1,2等对于字符串
t.Columns.Add(new DataColumn(c.ToString()));
}

foreach(资源中的var r)
{
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);
}


返回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}}/> 


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

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

Where

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

in the ResourceViewModel class I have the following properties

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 DataGrid but the ResourceStringList colletion is being displayed as '(Collection)'.

How can I get the DataGrid to display each of the strings contained in the ResourceStringList in its own column?

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:

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

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

Again thanks for your time.

解决方案

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.

TWO-WAY BINDING (ALLOWS EDITING)

The sample project (100KB)

I created a custom DataGrid and a custom 'DataGridColumn', called 'SeedColumn'. SeedColumn works just as a textcolumn, but has a property CollectionName. The DataGrid will add one new text column per item in the collection you've specified in CollectionName on 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.

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:

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

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.

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.

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 ItemsSource with this converter

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

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:

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

and then use it like this:

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

这篇关于如何绑定列表&lt; object&gt;到运行时使用MVVM的DataGrid的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆