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