This article is about creating and merging a general purpose PropertyGrid-like control in XAML/WPF, showing a ComboBox for enums, a CheckBox for bools and a TextBox for other types. Code is tested for .NET6 and .Net Framework 4.6.2
Introduction
This code helps (I hope) all the WPF/XAML beginner out there who are searching for a simple property grid almost ready to use in their code with little-no efforts and no additional dependency.
I've been asked several times to replace option pages of the application with a "PropertyGrid
" type control, but lack of time and a basic .NET Framework elements have always stopped me from doing so. In the end, however, the task could no longer be postponed and I had to invent (...) something that had the following characteristics:
- It has to show and edit the value of a field. The edit must be almost user friendly as possible, that means that booleans have to be selected with a
CheckBox
and Enum
s with ComboBox
. Numeric values and string
s can go with TextBox
es. - I don't want to rewrite dozens of view models or even the same control.
- The less dependencies I have, the better.
- If I have to use it again in another software, I have to have the possibility to style it without modifying the original code or using the existing style in the other application.
- Another rookie could one day use/modify it, so it has to be VERY simple.
Knowing this, let's go.
Background
As a beginner myself, there are virtually no requirements, but a little knowledge of XAML/WPF can come in handy.
Using the Code
To begin, let's write the User Control.
<UserControl
x:Class="IssamTp.Lib.Wpf.PropertyGridUC"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:IssamTp.Lib.Wpf"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006">
<UserControl.Resources>
<DataTemplate x:Key="EnumDataTemplate"
DataType="{x:Type local:PropertyGridRowVM}">
<ComboBox ItemsSource="{Binding Path=SelectableValues}"
SelectedItem="{Binding Value, Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged}"
Style="{Binding RelativeSource=
{RelativeSource Mode=FindAncestor,
AncestorType=local:PropertyGridUC},
Path=ComboBoxEditingStyle}" />
</DataTemplate>
<DataTemplate x:Key="BoolDataTemplate">
<CheckBox IsChecked="{Binding Path=Value, Mode=TwoWay,
UpdateSourceTrigger=LostFocus}"
Style="{Binding RelativeSource={RelativeSource Mode=FindAncestor,
AncestorType=local:PropertyGridUC}, Path=CheckBoxEditingStyle}" />
</DataTemplate>
<DataTemplate x:Key="IntegralDataTemplate">
<TextBox Text="{Binding Path=Value, Mode=TwoWay,
UpdateSourceTrigger=LostFocus}"
Style="{Binding RelativeSource=
{RelativeSource Mode=FindAncestor,
AncestorType=local:PropertyGridUC},
Path=TextBoxEditingStyle}" />
</UserControl.Resources>
<DataGrid
AutoGenerateColumns="False"
CanUserAddRows="False"
ItemsSource="{Binding Path=PropertiesValues}"
SelectionMode="Single">
<DataGrid.Columns>
<DataGridTextColumn
Binding="{Binding Path=Property, Mode=OneWay}"
IsReadOnly="True">
<DataGridTextColumn.Header>
<TextBlock DataContext="{Binding RelativeSource=
{RelativeSource Mode=FindAncestor,
AncestorType=local:PropertyGridUC}}"
Text="{Binding Path=HeaderLabelProperty}" />
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTemplateColumn>
<DataGridTemplateColumn.Header>
<TextBlock DataContext="{Binding RelativeSource=
{RelativeSource Mode=FindAncestor,
AncestorType=local:PropertyGridUC}}"
Text="{Binding Path=HeaderLabelValue}" />
</DataGridTemplateColumn.Header>
<DataGridTemplateColumn.CellTemplateSelector>
<local:TypeSelector
BoolDataTemplate=
"{StaticResource ResourceKey=BoolDataTemplate}"
EnumDataTemplate="{StaticResource ResourceKey=EnumDataTemplate}"
IntegralDataTemplate="{StaticResource
ResourceKey=IntegralDataTemplate}" />
</DataGridTemplateColumn.CellTemplateSelector>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</UserControl>
As you can see, there's nothing very special in the XAML apart of the value bindings which we'll see in a moment.
The control contains a simple DataGrid
with two columns, one with non-editable value binded with the name of a property (or a description of it) and the other with the value of it (again, we'll see in a moment where those bindings will come from).
The headers and the styles of DataTemplates
are binded with Dependency Properties to let the final user to customize them.
The xaml.cs file is this:
using System.Windows;
using System.Windows.Controls;
namespace IssamTp.Lib.Wpf
{
public partial class PropertyGridUC : UserControl
{
#region Dependency Properties
#region HeaderLabelProperty
public string HeaderLabelProperty
{
get { return (string)GetValue(HeaderLabelPropertyProperty); }
set { SetValue(HeaderLabelPropertyProperty, value); }
}
public static readonly DependencyProperty HeaderLabelPropertyProperty =
DependencyProperty.Register("HeaderLabelProperty", typeof(string),
typeof(PropertyGridUC), new PropertyMetadata("Property"));
#endregion
#region HeaderLabelValue
public string HeaderLabelValue
{
get { return (string)GetValue(HeaderLabelValueProperty); }
set { SetValue(HeaderLabelValueProperty, value); }
}
public static readonly DependencyProperty HeaderLabelValueProperty =
DependencyProperty.Register("HeaderLabelValue", typeof(string),
typeof(PropertyGridUC), new PropertyMetadata("Value"));
#endregion
#region ComboBoxEditingStyle
public Style ComboBoxEditingStyle
{
get { return (Style)GetValue(ComboBoxEditingStyleProperty); }
set { SetValue(ComboBoxEditingStyleProperty, value); }
}
public static readonly DependencyProperty ComboBoxEditingStyleProperty =
DependencyProperty.Register("ComboBoxEditingStyle", typeof(Style),
typeof(PropertyGridUC), new PropertyMetadata(new Style(typeof(ComboBox))));
#endregion
#region CheckBoxEditingStyle
public Style CheckBoxEditingStyle
{
get { return (Style)GetValue(CheckBoxEditingStyleProperty); }
set { SetValue(CheckBoxEditingStyleProperty, value); }
}
public static readonly DependencyProperty CheckBoxEditingStyleProperty =
DependencyProperty.Register("CheckBoxEditingStyle", typeof(Style),
typeof(PropertyGridUC), new PropertyMetadata(new Style(typeof(CheckBox))));
#endregion
#region TextBoxEditingStyle
public Style TextBoxEditingStyle
{
get { return (Style)GetValue(TextBoxEditingStyleProperty); }
set { SetValue(TextBoxEditingStyleProperty, value); }
}
public static readonly DependencyProperty TextBoxEditingStyleProperty =
DependencyProperty.Register("TextBoxEditingStyle", typeof(Style),
typeof(PropertyGridUC), new PropertyMetadata(new Style(typeof(TextBox))));
#endregion
#endregion
#region Ctor
public PropertyGridUC()
{
InitializeComponent();
}
#endregion
}
}
Choosing the right edit control is probably, for a newbie like me, the hardest part of the whole code: we have to specify a CellTemplateSelector
and this is done with the following simple code.
using System;
using System.Windows.Controls;
using System.Windows;
namespace IssamTp.Lib.Wpf
{
public class TypeSelector : DataTemplateSelector
{
#region Proprietà
public DataTemplate? BoolDataTemplate
{
get;
set;
}
public DataTemplate? EnumDataTemplate
{
get;
set;
}
public DataTemplate? IntegralDataTemplate
{
get;
set;
}
#endregion
public override DataTemplate SelectTemplate
(object item, DependencyObject container)
{
if (item is PropertyGridRowVM rowVM)
{
if (rowVM.Value is Enum && EnumDataTemplate != null)
{
return EnumDataTemplate;
}
else if (rowVM.Value is bool && BoolDataTemplate != null)
{
return BoolDataTemplate;
}
else if (IntegralDataTemplate != null)
{
return IntegralDataTemplate;
}
else
{
throw new MemberAccessException("No data template set.");
}
}
else if (IntegralDataTemplate != null)
{
return IntegralDataTemplate;
}
else
{
throw new MemberAccessException("No data template set.");
}
}
}
}
See? We just declare all the DataTemplate
properties you need (in our case, three) and override the SelectTemplate
method to pick you the one you want.
Now, we have to feed this control with some data, that means that we're going to see what is the source of the bindings. This is the "conceptual" part, but again it's nothing special.
Remembering the requirements "keep it simple" and "don't waste what you already have" I've written these two classes:
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace IssamTp.Lib.Wpf
{
public interface IPropertyGridVM
{
ObservableCollection<PropertyGridRowVM> PropertiesValues
{
get;
}
}
public class PropertyGridRowVM : INotifyPropertyChanged
{
public object? Value
{
get => _Value;
set
{
if (_Value == null || !_Value.Equals(value))
{
_Valore = value;
NotifyPropertyChanged();
}
}
}
private object? _Value;
public string Property
{
get => _Property;
set
{
if (!_Property.Equals(value, StringComparison.Ordinal))
{
_Property = value;
NotifyPropertyChanged();
}
}
}
private string _Property = string.Empty;
public ObservableCollection<object?> SelectableValues
{
get;
private set;
} = new ObservableCollection<object?>();
public Type? PropertyType
{
get;
private set;
}
#region INotifyPropertyChanged
public event PropertyChangedEventHandler? PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
#region Ctors
internal PropertyGridRowVM()
: base()
{
}
public PropertyGridRowVM(string propertyName, object value)
: base()
{
Value = value;
Property = propertyName;
PropertyType = Valore.GetType();
if (Value is Enum)
{
foreach (object? enumValue in PropertyType.GetEnumValues())
{
SelectableValues.Add(enumValue);
}
}
}
#endregion
}
}
Why an interface? Well, we don't have multiple inheritance in C#, so that's the way I found to make the control usable with all my existing View Models, while the PropertyGridRowVM
makes possible the magic of binding (my original idea was to use named tuples, you can find here why I switched to this solution).
And that's it! We only need now some code to test it:
<Window x:Class="Test.Wpf.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Test.Wpf"
xmlns:issam="clr-namespace:IssamTp.Lib.Wpf;assembly=IssamTp.Lib.Wpf"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.Resources>
<Style TargetType="{x:Type ComboBox}" x:Key="ComboBoxStyle">
<Setter Property="BorderBrush" Value="Black" />
<Setter Property="BorderThickness" Value="2" />
</Style>
<Style TargetType="{x:Type CheckBox}" x:Key="CheckBoxStyle">
<Setter Property="BorderBrush" Value="Black" />
<Setter Property="BorderThickness" Value="2" />
</Style>
<Style TargetType="{x:Type TextBox}" x:Key="TextBoxStyle">
<Setter Property="BorderBrush" Value="Black" />
<Setter Property="BorderThickness" Value="2" />
</Style>
</Window.Resources>
<Grid>
<issam:PropertyGridUC HeaderLabelProperty="Props" HeaderLabelValue="Vals"
DataContext="{Binding RelativeSource=
{RelativeSource Mode=FindAncestor,
AncestorType=local:MainWindow}}"
ComboBoxEditingStyle="{Binding Source=
{StaticResource ResourceKey=ComboBoxStyle}}"
CheckBoxEditingStyle="{Binding Source=
{StaticResource ResourceKey=ComboBoxStyle}}"
TextBoxEditingStyle="{Binding Source=
{StaticResource ResourceKey=TextBoxStyle}}"/>
</Grid>
</Window>
using IssamTp.Lib.Wpf;
using System.Collections.ObjectModel;
using System.Windows;
namespace Test.Wpf
{
public partial class MainWindow : Window, IPropertyGridVM
{
enum GetSome
{
One,
Two,
Three,
};
public ObservableCollection<PropertyGridRowVM> PropVals
{
get;
private set;
} = new ObservableCollection<PropertyGridRowVM>();
public MainWindow()
{
InitializeComponent();
PropVals.Add(new PropertyGridRowVM("A text value", "Hello world!"));
PropVals.Add(new PropertyGridRowVM("Count!", GetSome.Three));
PropVals.Add(new PropertyGridRowVM("Is it true?", true));
}
}
}
As you can see, all I had to do is to implement my interface and to add some data. Here, I used the code behind as View Model, but that does not matter, you can add it everywhere.
Conclusions
The code is simple and probably someone out there did it better but after years of using others' solutions I wanted to give something (helpful) back to Universe.
Feel free to use it if you need, improve it and tell me if I could have done it better.As you can see from my git repo, I write my code in Italian, so I may have left out or misspelled some variable/method name, let me know if you find some errors in the article.