数据网格通常用于显示具有每个项目固定属性集的相同类型项目列表,其中每一列都是一个属性。因此,每一行都是一个项目,每一列都是项目上的一个属性。你的情况不同,因为没有固定的属性集,而是要显示一个集合,就像它是一组固定数量的属性一样。
解决方案在很大程度上取决于您是只想显示数据还是允许用户操作数据。虽然第一个可以通过使用值转换器相对容易地实现,但后者需要更多的编码来扩展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;
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});
}
}
}
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)
{
var propertyInfo = collectionHolder.GetType().GetProperty(collectionName);
var propertyValue = propertyInfo.GetValue(collectionHolder, null);
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:
xmlns:x="http:
xmlns:system="clr-namespace:System;assembly=mscorlib" xmlns:sample="clr-namespace:Sample"
Title="MainWindow" Height="350" Width="525">
<Grid>
<sample:HorizontalGrid ItemsSource="" AutoGenerateColumns="False">
<sample:HorizontalGrid.Columns>
<sample:SeedColumn CollectionName="Strings" Binding="" 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;
var columns = resources[0].ResourceStringList.Count;
var t = new DataTable();
t.Columns.Add(new DataColumn("ResourceName"));
for (var c = 0; c < columns; c++)
{
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}}" />