This question has been asked in some form in some old threads, but having scoured them for hours and not found any satisfactory answers, I thought I would resurrect the question and see if I can get a fresh perspective.
For context - I have a DataGrid
which is to display an indeterminate number of properties from an indeterminate object (sometimes of different types) which the view model will supply to it. My view model provides this object by wrapping it in a ElementWrapper
class which at runtime will find all the properties of the object using element.GetType().GetProperties()
. It will use these properties to construct a PropertyWrapper
class like the below:
public class PropertyWrapper(string name, Type type, object value) : ObservableObject
{
public string Name { get; } = name;
public Type Type { get; } = type;
public object Value { get; set; } = value;
}
The ElementWrapper
will compile these into a dictionary of Dictionary<string, PropertyWrapper>
as shown below:
public class ElementWrapper
{
public static string PropertyNotFound { get; } = "#N/A";
public Element Element { get; }
public Dictionary<string, PropertyWrapper> Properties { get; } = [];
public ElementWrapper(Element element)
{
Element = element;
var propInfo = element?.GetType().GetProperties();
foreach (var p in propInfo)
{
object value = p.GetValue(element);
var pWrap = new PropertyWrapper(p.Name, p.PropertyType, value, true);
Properties.Add(p.Name, pWrap);
}
}
}
Now my DataGrid
is going to display an ObservableCollection<ElementWrapper>
. Each column will be constructed at run-time by the view model (not using AutoGenerateColumns
because I need more control in the View Model - I only want properties selected by user to be displayed, as there may be 50+ properties). The view model keeps a list of properties that is exposed to the view to select which properties to display. Whenever one is added, the below method of the View Model is called, which adds a new DataGridColumn
to an ObservableCollection<DataGridColumn>
called DataColumns
which is bound to the DataGrid
in xaml using a behavior.
private void AddColumn(PropertyInfo propInfo)
{
DataColumns.Add(new DataGridTextColumn()
{
Header = propInfo.Name,
Binding = new PriorityBinding()
{
Bindings =
{
new System.Windows.Data.Binding("Properties[propInfo.Name].Value"),
new System.Windows.Data.Binding("PropertyNotFound")) { Mode = BindingMode.OneTime },
}
}
});
}
As you can see, I'm using a PriorityBinding
so that for elements which do not have the property, it will fallback to the "#N/A" string supplied by PropertyNotFound
.
That was a lot of context to get to the real question ... now the real question is, when I add columns, if a property is a boolean I want to display a CheckBox
type of column. Adding a DataGridCheckBoxColumn
in similar fashion to the above works pretty readily OOTB, but for elements which do not have the boolean property, I want to display the fallback "#N/A" text instead of a CheckBox
. That forces me to possibly use a TemplateColumn
and swap the DataTemplate
using a CellTemplateSelector
, or create a custom class deriving from DataGridBoundColumn
. The latter approach seemed the cleanest, but I have not been able to get it work. In the former approach, I have found it difficult to construct the DataTemplate
s in my Resources in such a way that I can set their bindings at runtime. Sadly the best I have been able to get work is in my view model I have a block of a xaml code string defining a DataTemplate
that contains a Grid
with a CheckBox
and a TextBlock
in it that have DataTriggers
that toggle their visibility based on the return value of the binding to the target property.
This is working, but seems very much anti-MVVM. It is also a real pain in my code-behind to access the CheckBox
es when they are added to the DataGrid
so that I can subscribe to their Checked
and Unchecked
events. This too I have gotten to work, but feel like I am having to trick the system. Is there a good way to handle this need?
My current XAML string which my View Model is using:
string bindingPath = "Properties[propInfo.Name].Value"
string cellTemplateXaml = $@"
<DataTemplate xmlns=''
xmlns:x=''>
<Grid>
<CheckBox IsChecked='{{Binding {bindingPath}}}'>
<CheckBox.Style>
<Style TargetType='CheckBox'>
<Style.Triggers>
<DataTrigger Binding='{{Binding {bindingPath}}}' Value='{{x:Null}}'>
<Setter Property='Visibility' Value='Collapsed'/>
</DataTrigger>
</Style.Triggers>
</Style>
</CheckBox.Style>
</CheckBox>
<TextBlock Text='{{Binding {nameof(ElementWrapper.PropertyNotFound)}, Mode=OneTime}}'>
<TextBlock.Style>
<Style TargetType='TextBlock'>
<Style.Triggers>
<DataTrigger Binding='{{Binding {bindingPath}}}' Value='True'>
<Setter Property='Visibility' Value='Collapsed'/>
</DataTrigger>
<DataTrigger Binding='{{Binding {bindingPath}}}' Value='False'>
<Setter Property='Visibility' Value='Collapsed'/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</Grid>
</DataTemplate>";