Click here to Skip to main content
15,886,071 members
Articles / Desktop Programming / WPF

A Simple Property Grid Control

Rate me:
Please Sign up or sign in to vote.
5.00/5 (4 votes)
11 Apr 2023Public Domain3 min read 4.2K   4   3
A .NET XAML user control to get a simple PropertyGrid
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:

  1. 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 Enums with ComboBox. Numeric values and strings can go with TextBoxes.
  2. I don't want to rewrite dozens of view models or even the same control.
  3. The less dependencies I have, the better.
  4. 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.
  5. 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.

XAML
<!--PropertyGridUC.xaml-->
<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:

C#
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.

C#
using System;
using System.Windows.Controls;
using System.Windows;

namespace IssamTp.Lib.Wpf
{
    /// <summary>Permette di scegliere tra tre DataTemplate distinguendo tra bool, 
    /// Enum e tutti gli altri.</summary>
    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:

C#
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
        /// <inheritdoc />
        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:

XAML
<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>
C#
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.

Image 1

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.

License

This article, along with any associated source code and files, is licensed under A Public Domain dedication


Written By
Software Developer (Senior)
Italy Italy
Married with C++.

Comments and Discussions

 
PraiseLess is more Pin
Member 136836296-Aug-23 21:30
Member 136836296-Aug-23 21:30 
QuestionXAML and property grids are two of the evils in the world... Pin
Marc Clifton12-Apr-23 1:02
mvaMarc Clifton12-Apr-23 1:02 
PraiseRe: XAML and property grids are two of the evils in the world... Pin
IssamTP12-Apr-23 5:51
IssamTP12-Apr-23 5:51 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.