C
C#15mo ago
JakenVeina

❔ WPF Dynamic DataGrid

I need to be able to build a DataGrid to support a dynamic (I.E. determined at runtime) set of columns. I'm hoping to be able to use a DataGrid instead of building a custom control of some kind that's just build on top of a Grid, because there's a lot of nice stuff that DataGrid supports, like sorting. Reason being, I'm trying to build a UI where the user can import CSV files. I want the UI to display the CSV file (at least in part) in its raw form, I.E. just a pure table of nothing but strings. This gives the user a chance to look at it, and then assign which columns have which functional meaning, for further parsing. It will also be able to give feedback on validation, E.G. highlight cells as invalid in the column they've designated as "Date", if those cells contain invalid date values. Setting up the columns was fairly straightforward, I wrote a Behavior<T> that gives me a HeadersSource property I can bind to, to define how many columns there are, and what their names are (parsed out of the CSV document).
public class DraftsCsvImportWorkspaceModel
{
public IReadOnlyList<DraftsCsvImportColumnHeaderModel> ColumnHeaders { get; }
public IReadOnlyList<IReadOnlyList<DraftsCsvImportColumnValueViewModel>> Records { get; }
}
public class DraftsCsvImportWorkspaceModel
{
public IReadOnlyList<DraftsCsvImportColumnHeaderModel> ColumnHeaders { get; }
public IReadOnlyList<IReadOnlyList<DraftsCsvImportColumnValueViewModel>> Records { get; }
}
<DataGrid ItemsSource="{Binding Records, Mode=OneTime}">
<b:Interaction.Behaviors>
<Interactions:DynamicColumnsBehavior HeadersSource="{Binding ColumnHeaders, Mode=OneTime}"/>
</b:Interaction.Behaviors>
</DataGrid>
<DataGrid ItemsSource="{Binding Records, Mode=OneTime}">
<b:Interaction.Behaviors>
<Interactions:DynamicColumnsBehavior HeadersSource="{Binding ColumnHeaders, Mode=OneTime}"/>
</b:Interaction.Behaviors>
</DataGrid>
protected override void OnAttached()
{
AssociatedObject.AutoGenerateColumns = false;
AssociatedObject.Columns.Clear();
if (HeadersSource is IEnumerable headersSource)
{
foreach(var header in headersSource)
{
AssociatedObject.Columns.Add(new DataGridTemplateColumn()
{
Header = header
});
}
}
}
protected override void OnAttached()
{
AssociatedObject.AutoGenerateColumns = false;
AssociatedObject.Columns.Clear();
if (HeadersSource is IEnumerable headersSource)
{
foreach(var header in headersSource)
{
AssociatedObject.Columns.Add(new DataGridTemplateColumn()
{
Header = header
});
}
}
}
I'm not sure how I can proceed from here, though. Ultimately, what I suppose I need is a way to pass down the column index to each cell, for use in binding. I think in theory what I could do with DataGridTemplateColumn is....
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ContentPresenter Content="{Binding [0]}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ContentPresenter Content="{Binding [0]}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
That is, DataGrid.ItemsSource is bound to an array/list of the cell ViewModels, and each column needs to bind to the item at the appropriate index of the array. From there, the ContentPresenter should be able to retrieve the appropriate View for the ViewModel type from resources, and everything should be good. But I don't know how to do the programmatic equivalent of that CellTemplate
20 Replies
a coding witch
a coding witch15mo ago
Why, instead of having a dynamic don't you use a DataTable or create a data structure to support that? If you have a data structure you can directly bind the data - so you will not need to make loops and populate the table manually
JakenVeina
JakenVeina15mo ago
there is no use of dynamic anywhere in that code building a data structure to model this is precisely what I'm doing that's what DraftsCsvImportWorkspaceModel is
a coding witch
a coding witch15mo ago
Ah understood I mean a data structure where you can bind it directly to a DataGrid like a list (not sure if all IEnumerable implementations)
JakenVeina
JakenVeina15mo ago
because DataGrid doesn't support that it supports binding a set of rows
a coding witch
a coding witch15mo ago
BindingList?
JakenVeina
JakenVeina15mo ago
with you explicitly defining columns
a coding witch
a coding witch15mo ago
Are u sure? Im not sure abt that, like, I thought you could put a common list as data source
JakenVeina
JakenVeina15mo ago
or it supports the AutoGenerateColumns=True option, which generates columns based on object properties, not based on the columns being an arbitrary array isn't BindingList a WinForms thing?
a coding witch
a coding witch15mo ago
So, do you also have dynamic columns? I dont remember I used to do desktop a long ago
JakenVeina
JakenVeina15mo ago
I have dynamic columns in the sense that I don't know how many there are or what their names are until runtime
a coding witch
a coding witch15mo ago
Understood well, my thoughts are pretty cursed about that (reflection) I mean have you tried to use DataTable.DefaultView to populate the grid?
JakenVeina
JakenVeina15mo ago
there is no reflection going on here
a coding witch
a coding witch15mo ago
no no forget about the reflection
JakenVeina
JakenVeina15mo ago
no, I am not using a DataTable
a coding witch
a coding witch15mo ago
is not related to what i said now
JakenVeina
JakenVeina15mo ago
this is not coming from a database
a coding witch
a coding witch15mo ago
I know but you dont need to have a database to use datatable you can make your own DataTable instance with your data then, put the instance as your DataGrid ItemsSource I mean, I think it is a dirty solution but that all that I can think @ReactiveVeina
JakenVeina
JakenVeina15mo ago
yeah, I don't see how that's going to work there doesn't appear to be any way for me to stick a ViewModel into a DataColumn I can only create a DataColumn with a string columnName value, and a couple other metadata parameters in theory, I could just setup the HeaderTemplate to be the same for every column, and have it bind to stuff on the WorkspaceModel but then I'd have no way to identify which column was calling into WorkspaceModel we're right back to the original question, really so, yeah, this works
<DataGridTemplateColumn Header="{Binding DataContext[0], Source={StaticResource ColumnHeadersBindingProxy}}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ContentControl Content="{Binding [0], Mode=OneTime}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="{Binding DataContext[0], Source={StaticResource ColumnHeadersBindingProxy}}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ContentControl Content="{Binding [0], Mode=OneTime}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
(bullshit binding hack aside) all I need is a way to programmatically generate that DataTemplate cause my programmatic generation of the <DataGridTemplateColumn>s, with Header values is already working
private static DataGridColumn CreateColumn(int columnIndex, object header)
{
var contentControlFactory = new FrameworkElementFactory(typeof(ContentControl));
contentControlFactory.SetBinding(ContentControl.ContentProperty, new Binding($"[{columnIndex}]")
{
Mode = BindingMode.OneTime
});

return new DataGridTemplateColumn()
{
Header = header,
CellTemplate = new DataTemplate()
{
VisualTree = contentControlFactory
},
};
}
private static DataGridColumn CreateColumn(int columnIndex, object header)
{
var contentControlFactory = new FrameworkElementFactory(typeof(ContentControl));
contentControlFactory.SetBinding(ContentControl.ContentProperty, new Binding($"[{columnIndex}]")
{
Mode = BindingMode.OneTime
});

return new DataGridTemplateColumn()
{
Header = header,
CellTemplate = new DataTemplate()
{
VisualTree = contentControlFactory
},
};
}
bingo
a coding witch
a coding witch15mo ago
boyah good
Accord
Accord15mo ago
Was this issue resolved? If so, run /close - otherwise I will mark this as stale and this post will be archived until there is new activity.