Click here to Skip to main content
14,981,972 members
Articles / DevOps
Article
Posted 15 Apr 2016

Stats

53.9K views
513 downloads
32 bookmarked

WPF PropertyGrid

Rate me:
Please Sign up or sign in to vote.
4.97/5 (15 votes)
5 Feb 2017CPOL9 min read
How to show and edit an object's properties using a DataGrid​​​​​​​

Introduction

This article demonstrates how to show and edit an object's properties dynamically using the DataGrid control.

Preview

Image 1

Table Of Contents

Background

This article is the result of an experiment as I considered alternatives to a popular open source solution for implementing a PropertyGrid as provided with the WpfExtendedToolkit.

The WpfExtendedToolkit's PropertyGrid has a few caveats:

  • It's difficult to customize.
  • There is seemingly no way to style the grid cells.
  • It has a bug. Sometimes when clicking the sort button, I would have to click a second time in order to make it appear checked (though whether or not it actually is checked is still unclear).
  • No control over bugs

And so this project was born...

Features

  • Property changes are monitored internally (and externally, if the host object implements INotifyPropertyChanged).
  • Follows MVVM pattern.
  • Properties are fetched on background thread using thread-safe ObservableCollection.
  • Can represent a string property as a file/folder path, a password, or in multiple lines.
  • Can represent a long property as a file size.
  • Properties can be:
    • Categorized
    • Coerced to maximum or minimum
    • Excluded
    • Featured (i.e., sits above, and does not scroll with, other properties)
    • Readonly
    • Sorted by name/type, ascending/descending
  • Properties must define one of the following:
    • A public getter and no setter (readonly is implicit)
    • A public getter and a setter with any visibility (readonly is implicit if setter is not public)
  • Properties may also have:
    • Alternate display name
    • Description
  • The following types are supported:
    • System.Boolean
    • System.Byte
    • System.Collections.IList
    • System.DateTime
    • System.Decimal
    • System.Double
    • System.Enum
    • System.Guid
    • System.Int16
    • System.Int32
    • System.Int64
    • System.Windows.Media.LinearGradientBrush
    • System.Net.NetworkCredential
    • System.Windows.Point
    • System.Windows.Media.RadialGradientBrush
    • System.Windows.Size
    • System.Windows.Media.SolidColorBrush
    • System.String
    • System.Uri
    • System.Version
  • The nullable version of all supported types (except Enum and IList) are supported.

Cons

  • No unit tests performed.
  • Only tested using .NET 4.6 and Windows 10

Using the Code

Usage is as simple as this:

XML
<local:PropertyGrid SelectedObject="{Binding YourObject}"/>

Where the object can be...anything! Simple enough, right? Now let's explore what's going on behind the scenes...

The PropertyGrid Control

The PropertyGrid control itself is composed of two main elements:

  • A header
    • Contains search bar, which enables you to toggle visibility of properties based on characters typed (comparison is NOT case-sensitive and goes by the camel-case version of the property name, not the actual property name). Also includes facilities for grouping, sorting, collapsing groups, and resetting the value of all properties owned by the object.
  • A DataGrid
    • The DataGrid enables displaying and editing properties.

Retrieving Properties From An Object

So how do we get all of the properties of an object, you ask? Reflection! Though reflection does come with performance struggles (depending on the situation), reflection was my only hope for this solution and so I tried to reduce overhead as much as possible.

This is how we update the PropertyGrid when SelectedObject changes:

C#
public static DependencyProperty SelectedObjectProperty = 
     DependencyProperty.Register("SelectedObject", typeof(object), typeof(PropertyGrid), 
     new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, 
     OnSelectedObjectChanged));
public object SelectedObject
{
    get
    {
        return (object)GetValue(SelectedObjectProperty);
    }
    set
    {
        SetValue(SelectedObjectProperty, value);
    }
}
private static void OnSelectedObjectChanged(DependencyObject Object, 
     DependencyPropertyChangedEventArgs e)
{
    PropertyGrid PropertyGrid = (PropertyGrid)Object;
    PropertyGrid.OnSelectedObjectChanged();
}

OnSelectedObjectChanged is a method that handles updating the property items and setting a reference to the selected object.

C#
protected virtual void OnSelectedObjectChanged(object Value)
{
    if (Value != null)
    {
        Properties.Reset(Value);

        IsLoading = true;
        await SetObject(Value);
        IsLoading = false;
    }
    else if (AcceptsNullObjects)
        Properties.Reset(null);

    SelectedObjectChanged?.Invoke(this, new EventArgs<object>(Value));
}

How we actually go about getting the properties depends. The method SetObject is responsible for getting the properties and, by default, uses reflection to enumerate the object's properties. In other scenarios, we may require getting these items another way. E.g., ResourceDictionaryEditor (included in this project), must enumerate the dictionary entries of a resource dictionary. Enumerating the object's properties, in this case, does not work because it would not let us edit the dictionary's entries, only the properties. If one wished to edit the properties, PropertyGrid alone would satisfy that goal.

That method is as follows:

C#
protected virtual async Task SetObject(object Value)
{
    await Properties.BeginFromObject(Value);
}

Properties is the collection of properties bound to the DataGrid. This collection is a special type called, PropertyModelCollection and takes care of reading and extracting each property; in addition, it inherits from ConcurrentObservableCollection<PropertyModel> (a thread-safe variant of ObservableCollection), which allows us to refresh the collection on a background thread (this is important because the collection is bound to UI).

BeginFromObject

To enumerate the properties of an object, we do the following:

C#
public async Task BeginFromObject(object Object, Action Callback = null)
{
    await Task.Run(() =>
    {
        var Properties = Object.GetType().GetProperties();

        for (int i = 0, Length = Properties.Length; i < Length; i++)
        {
            var Property = Properties[i];

            if (!IsSupported(Property))
                continue;

            var Attributes = new PropertyAttributes()
            {
                { "Browsable", "Browsable", true },
                { "Category", "Category", string.Empty },
                { "Constraint", null, null },
                { "Description", "Description", string.Empty },
                { "DisplayName", "DisplayName", string.Empty },
                { "Featured", "IsFeatured", false },
                { "Int64Kind", "Kind", Int64Kind.Default },
                { "ReadOnly", "IsReadOnly", false },
                { "StringKind", "Kind", StringKind.Regular },
                { "DisplayFormat", "DataFormatString", string.Empty },
            };

            Attributes.ExtractFrom(Property);

            if ((bool)Attributes["Browsable", false])
            {
                var Model = PropertyModel.New(Object, Property, Attributes);

                if (Model != null)
                    Add(Model);
            }
        }
    });

    Callback.InvokeIf(x => !x.IsNull());
}

What we're doing in simpler terms:

  • Enumerate all object properties.
  • For each, check if it is supported (i.e., is public and can both get and set).
  • If a property is supported, extract available (and supported) attributes; attributes allow customizing a property even further.
  • If the property is browsable (i.e., either specifies attribute BrowsableAttribute with true value, or not at all), generate a model for it and add to the collection.

Showing Different Controls Based On Type

To show a particular control for a particular type, we use a DataTemplateSelector. Anywhere we need to display a property, we use the following:

XML
<ContentControl
    Content="{Binding ThePropetyModel}"
    ContentTemplate="{StaticResource PropertyModelTemplate}"/>

The resource PropertyModelTemplate is defined like:

XML
<DataTemplate x:Key="PropertyModelTemplate">
    <ContentControl Content="{Binding}">
        <ContentControl.ContentTemplateSelector>
            <local:PropertyTemplateSelector Resources="{StaticResource Resources.Templates}"/>
        </ContentControl.ContentTemplateSelector>
    </ContentControl>
</DataTemplate

And defines a dictionary containing every available DataTemplate. Each DataTemplate has a key corresponding to its model's primitive type (e.g., if PropertyModel<bool>, the primitive type (and key) is System.Boolean).

When the property changes, the DataTemplateSelector searches the dictionary and returns a template based on the model's corresponding type.

C#
public class PropertyTemplateSelector : DataTemplateSelector
{
    public ResourceDictionary Resources
    {
        get; set;
    }

    public override DataTemplate SelectTemplate(object item, DependencyObject container)
    {
        if (item != null)
        {
            foreach (DictionaryEntry i in Resources)
            {
                if (i.Key.As<Type>() == item.As<PropertyModel>().Primitive)
                    return i.Value as DataTemplate;
            }
        }
        return base.SelectTemplate(item, container);
    }
}

The downside is each key must be defined in XAML, requiring the need to define additional namespace mappings that may otherwise be difficult to find (System.Windows.Media, e.g.; I couldn't tell you which assembly it belongs to, but I wouldn't have to in code-behind, only XAML!).

The Objects that Represent Type Properties

There are four main "models" for managing a single property, each with unique capabilities:

  1. PropertyModel
    • A base class, which provides necessary facilities for all; this is abstract and should never be used on its own.
  2. PropertModel<T>
    1. The generic verison of PropertyModel used to model a specific property type; all models should inherit this, but can be, and is often, used on it's own.
  3. CoercedPropertModel<T>
    1. Inherits PropertyModel<T> and is used for types that should be coerced (i.e., have both a minimum and a maximum value). Note, just because a type is modelled with this does not mean the type should always be coerced, just that it supports coercion.
  4. CoercedVariantPropertyModel<T>
    • Slightly more complicated than its inherited type, CoercedPropertyModel<T>, it enables editing types that should be coerced and also lack binding support. By "lack binding support", I mean the type's internal members cannot be bound to a control and must be edited using another type (a defined variant type).
      • To better understand this, consider the type, System.Windows.Point: You cannot edit this type's members (X, Y) using bindings; therefore, we have a couple options...
        a) Define a control that allows editing this type (e.g., PointBox), or
        b) define a bindable version of the type and when the bindable version changes, update its underlying type, also. For System.Windows.Point, there is a Position type defined in Imagin.Common.Primitives namespace, which inherits IVariant<System.Windows.Point> and enables converting from one to another with great ease. We'd simply bind the Position type's members to the UI (also, X, Y) and the underlying CoercedVariantPropertyModel<T> takes care of ensuring both values are always the same. Because we decided to go with option b, a control like PointBox is unnecessary and we can translate this logic to many more types. The downside is you can edit these types in a PropertyGrid, but not anywhere else.
      • This model differs from in its inherited type in one MAJOR way: It makes use of TWO generic types, versus just one. It's important to understand which is which and which does what.
        • T1 specifies a bindable version of T2.
        • T1 is IVariant<T2>.
        • IVariant<T2> exposes facilities for converting from T1 to T2 and vice versa.
        • The model is ultimately defined like so:
      • C#
        public class CoercedVariantPropertyModel<T1, T2> : 
             CoercedPropertyModel<T2> where T1 : IVariant<T2>
        {
            bool ValueChangeHandled = false;
        
            T1 variant = default(T1);
            public T1 Variant
            {
                get
                {
                    return variant;
                }
                set
                {
                    variant = value;
                    OnPropertyChanged("Variant");
                }
            }
        
            internal CoercedVariantPropertyModel() : base()
            {
                Variant = typeof(T1).TryCreate<T1>();
                Variant.Changed += OnVariantChanged;
            }
        
            protected override void OnValueChanged(object Value)
            {
                base.OnValueChanged(Value);
                if (!ValueChangeHandled)
                    Variant.Set(Value.As<T2>());
            }
        
            protected virtual void OnVariantChanged(object sender, EventArgs<T2> e)
            {
                ValueChangeHandled = true;
                Value = e.Value;
                ValueChangeHandled = false;
            }
        }

Editing Source Object's Properties

If you've made it this far and are still wondering how values are reflected in the source object without use of binding, consider the following:

Each property's value is stored in a property defined in PropertyModel class:

C#
object _value = null;
public object Value
{
    get
    {
        return _value;
    }
    set
    {
        _value = OnPreviewValueChanged(_value, value);
        OnPropertyChanged("Value");
        OnValueChanged(_value);
    }
}

Which is bound to a control that allows editing it. Unfortunately, this alone will only update the value stored in the property model, but not in the object itself. Hence, we hook up some events when the value changes.

The first event is, OnPreviewValueChanged: This accepts the old and original values and returns a new value. This allows you to modify the original value to ensure it is not invalid before storing a reference to it. By default, if the value is null, return null; else, the value.

C#
protected virtual object OnPreviewValueChanged(object OldValue, object NewValue)
{
    return NewValue == null ? default(object) : NewValue;
}

In inherited generic types, it becomes:

C#
protected override object OnPreviewValueChanged(object OldValue, object NewValue)
{
    if (NewValue == null)
    {
        return Default;
    }
    else
    {
        var Result = default(object);
        switch (typeof(T).Name.ToString().ToLower())
        {
            case "byte":
                Result = NewValue.ToString().ToByte();
                break;
            //...
            default:
                Result = (T)NewValue;
                break;
        }
        return Result.To<T>();
    }
}

By default, if null, return default(T); else, check the type as a lowercase string and modify the value accordingly. Any type not otherwise specified is the original value cast to T. It is necessary to check for individual types because sometimes the original value is not the type we expect. For instance, if binding double to TextProperty on DoubleUpDown, the value returned will be string (except we can't cast double to string implicitly or explicitly, it must be converted); therefore, we convert the double to string the correct way, then return that value. Note, by exposing the old value in addition to the new, you can take both into account when returning a valid value.

The second is, OnValueChanged: Once the value changes and we know without a shadow of a doubt that it is valid, we attempt to update the corresponding property on the object itself using reflection:

C#
protected virtual void OnValueChanged(object Value)
{
    if (Host is ResourceDictionary)
    {
        if (Host.As<ResourceDictionary>().Contains(Name))
            Host.As<ResourceDictionary>()[Name] = Value;
    }
    else if (Info != null)
        Info.SetValue(Host, Value, null);
}

If the host object is ResourceDictionary and it contains a key of the property name, set the value of the entry via key. If not, assume it's a property on an object and set the value using reflection.

Additional Applications

While developing this control, I came up with an interesting new idea: What if I wanted to be able to edit an entire resource dictionary? And not just any resource dictionary, but perhaps one used for themes, specifically?

ResourceDictionaryEditor

Meet, ResourceDictionaryEditor. This new control is derived from PropertyGrid. The idea was to give users the ability to edit a theme in real-time, assuming the theme is a ResourceDictionary. The only real difference here is the way we extract properties. Instead of enumerating the properties, we enumerate the keys. Note, to edit the dictionary's properties, you'd just use PropertyGrid.

In the demo, we are able to display and edit a ResourceDictionary used to theme the demo itself; unfortunately, it does not seem to change the original dictionary values when the dictionary is compiled and referenced from another assembly and, thus, changes to the theme's actual appearance are not apparent.

History

Please refer to the open source project, Imagin.NET, for all future revisions.

  • 18th of April, 2016
    • Initial post
  • 25th of April, 2016
    • Realized searching was checking the non-camel-case version of the property name. That can be fixed by adding a single string extension to the following method in PropertyGrid.xaml.cs:
      C#
      private void BeginSearch()
      {
          foreach (PropertyItem Item in this.Properties)
          {
              Item.IsVisible = this.SearchTextBox.Text == string.Empty ? 
      		true : Item.Name.SplitCamelCase().ToLower().StartsWith(this.SearchTextBox.Text) ? 
      		true : false;
          }
      }
    • Added support for updating integer types (double, int) in the original source. Modified SetValue method so that the value is cast appropriately.
  • 20th of July, 2016
    • Major article/project update (v1).
  • 16th of January, 2017
    • Major article update.
  • 18th of January, 2017
    • Minor code changes (v2.7.6).
  • 5th of February, 2017
    • Major code changes; minor article changes (v2.8).

Future

The code in this article is now part of the open source project, Imagin.NET.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

James J M
Software Developer Imagin
United States United States
No Biography provided

Comments and Discussions

 
BugLooks very interresting to me, but ... Pin
creimers24-May-18 1:34
Membercreimers24-May-18 1:34 
GeneralRe: Looks very interresting to me, but ... Pin
bh_10-Jan-20 3:01
Memberbh_10-Jan-20 3:01 
QuestionWhere is the code? Pin
Thomas van der Ploeg26-Jan-17 6:07
MemberThomas van der Ploeg26-Jan-17 6:07 
AnswerRe: Where is the code? Pin
James J M27-Jan-17 13:46
MemberJames J M27-Jan-17 13:46 
QuestionA few drawbacks Pin
Thornik29-Aug-16 12:49
MemberThornik29-Aug-16 12:49 
AnswerRe: A few drawbacks Pin
James J M29-Aug-16 13:39
MemberJames J M29-Aug-16 13:39 
AnswerRe: A few drawbacks Pin
Thomas van der Ploeg26-Jan-17 6:10
MemberThomas van der Ploeg26-Jan-17 6:10 
SuggestionRe: A few drawbacks Pin
BeSkeptical21-May-17 8:35
MemberBeSkeptical21-May-17 8:35 

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.