Typically, a datagrid is used to display a list of items of the same type with a fixed set of properties for each item, where each column is a single property. Thus, each row is one element, each column is one element property. You are another case, because there is no fixed set of properties, but the collection you want to show, as if it were a fixed set of several properties.
The way much depends on whether you want to display the data or whether you want to allow the user to manipulate the data. While the former can be achieved relatively easily using value converters, the latter requires a bit more coding to extend the DataGrid class to allow this. The solutions I show are two thousand possibilities, and perhaps not the most elegant. In doing so, I will describe both methods and start with a two-way version.
DOUBLE AREA (LETS EDIT)
Sample project (100 KB)
I created a custom DataGrid and a custom "DataGridColumn" called "SeedColumn". SeedColumn works like a text column, but has a CollectionName property. DataGrid will add one new text column for each item in the collection you specify in CollectionName on the right side of the seed column. A second column only acts as a sort of placeholder to tell the DataGrid where to insert these columns. You can use several seedcolumns in one grid.
Grids and 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>(); } }
Using:
<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 ViewModels that I 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 appearance (old version without headers):

ADDITION:
Regarding new questions and your comment:
NullReferenceException may have several reasons, but you obviously resolved this. However, the line in which it occurred is a bit of spaghetti code, and I would not do it like in production code. You need to handle things that might go wrong anyway ... I changed the code and reorganized the line into my own method. This will give you an idea of ββwhat happens when an exception is thrown.
the empty column that you see is a seed column, which is obviously unrelated to anything. My idea was to use this column as a kind of row header and bind it to the Name resource. If you don't need a seed column at all, just set its Visibility to collapsing.
<loc:SeedColumn CollectionName="Strings" Visibility="Collapsed">
Adding column headers is not difficult, but you need to think about where you want to get this. When you store all your lines in a list, they are just lines, so they are not related to the second line, which you can use as a heading. I have implemented a way to secure columns in XAML, which may be enough for you: you can use it as follows:
<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 there are more elements in the collection than indicated in the headers, the column headings will be repeated "Header3", "Header4", "Header1", .. The implementation is straightforward. Note that the Headers property of the seed column can also be linked; you can bind it to any list.
ONE-SIDED COMMUNICATION (NO DATA EDIT)
The direct way is to implement a converter that formats your data in a table and returns a view in that table to which a DataGrid can bind. Disadvantage: it does not allow editing rows, because as soon as the table is created from the source data source, there is no logical connection between the displayed data and the source data. However, changes to the collection are reflected in the user interface because WPF performs the conversion every time the data source changes. In short: this solution is ideal if you want to display data.
How it works?
- Create your own value conversion class that implements
IValueConverter - Create an instance of this class in your XAML resources and give it a name
- Bind the
ItemsSource grid to this converter
Here's what it will look like (my IDE is StackOverflow, so please check and fix 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 the 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}}" />