数据网格通常用于显示相同类型的项目列表,每个项目具有一组固定的属性,其中每一列都是一个属性。因此,每一行都是一个项目,每一列都是该项目的一个属性。您的情况有所不同,因为没有固定的属性集,而是您想要显示的集合,就好像它是多个属性的固定集一样。
采用的方法很大程度上取决于您是否只想显示数据或是否希望允许用户操作数据。虽然第一个可以使用值转换器相对容易地实现,但后者需要更多的编码来扩展 DataGrid 类以允许这种行为。我展示的解决方案是一千种可能性中的两种,而且可能不是最优雅的。话虽这么说,我将描述这两种方式,并从双向版本开始。
双向绑定(允许编辑)
示例项目 (100KB) http://www.einmb.de/GridSample.zip
我创建了一个自定义DataGrid
和一个自定义的“DataGridColumn”,称为“SeedColumn”。SeedColumn
就像文本列一样工作,但有一个属性CollectionName
. The 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>
以及我用于测试的 ViewModel:
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"};
}
}
和外观(没有标题的旧版本):
附录:
关于新问题和您的评论:
The 空引用异常可能有几个原因,但你显然已经
解决了它。然而,它发生的地方有点像意大利面条
代码,我不会在生产代码中这样做。你需要
处理在任何情况下都可能出错的事情...我已经修改了
代码并将该行重构为它自己的方法。这会给你
当异常被抛出时,知道发生了什么。
The 空栏你看到的是种子列,它显然没有绑定到任何东西。我的想法是使用这一列作为一种行
标头并将其绑定到Name
的资源。如果你不需要
根本没有种子列,只需设置它的Visibility
到崩溃。
<loc:SeedColumn CollectionName="Strings" Visibility="Collapsed">
Adding 列标题并不难,但你需要思考
关于你想从哪里获取。当您存储所有字符串时
在列表中,它们只是字符串,因此与第二个字符串无关
您可以将其用作标题。我已经实现了一种方法来指定
纯粹在 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
财产
种子列的 也是可绑定的,您可以将其绑定到任何列表。
单向绑定(无需编辑数据)
一种直接的方法是实现一个转换器,该转换器格式化表中的数据并返回该表上的视图,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}}" />