Make a Property Read-Only in PropertyGrid

Sometimes you want to make a property read-only in PropertyGrid based on some criteria at run-time. For example, let’s suppose you have a SampleClass containing two properties, Editable and StringProperty and you want to make StringProperty read-only if Editable is false.

We know that by adding a [ReadOnly(true)] attribute to a property we can make it read-only. But how to do it at run-time based on some criteria?

In fact we can not add Attribute to properties at run-time, but since PropertyGrid uses Type Descriptor we can provide some solutions using type descriptor mechanism.

PropertyGrid control uses the type descriptor of the object to extract information about its properties to show. The type descriptor, returns a list of PropertyDescriptor objects as list of properties. Each PropertyDescriptor contains some methods and properties to return display name, description, and other information about the property. IsReadOnly property of the PropertyDescriptor is responsible to inform the PropertyGrid if the property should be read only.

Regarding to above fact, here are solutions which we can provide to make a property read-only at run-time in PropertyGrid:

  1. Solution 1 – Provide a new TypeDescriptor for the class which returns a PropertyDescriptor having ReadOnly attribute for that specific property.

  2. Solution 2 – Add ReadOnly attribute to AttributeCollection of the ProperetyDescriptor by handling PropertyChanged and SelectedObjcetsChanged event of PropertyGrid.

  3. Solution 3 – Add ReadOnlyattribute to AttributeCollection of the ProperetyDescriptor in the class implementation based on some criteria.

Example

Let’s suppose we have a class containing two properties. Editable and StringProperty. If the Editable is true then StringProperty should be editable, otherwise it would be read-only and will be shown as gray in PropertyGrid.

This is the main goal: Making the property read-only in PropertyGrid.

PropertyGrid ReadOnly Property

Here is the class:

using System;
public class SampleClass
{
    public bool Editable { get; set; }

    string stringProperty;
    public string StringProperty
    {
        get { return stringProperty; }
        set { if (Editable) stringProperty = value; }
    }
}

Solution 1 – Providing a new TypeDescriptor for the class

We can create a new TypeDescriptor for the class. The new type descriptor, is responsible to provide a list of PropertyDescriptor objects when PropertyGrid asks about list of properties. The new type descriptor, should return a suitable PropertyDescriptor for the property which we are going to make it readonly, to contain Read-Only attribute based on some criteria.

To support extensibility, the TypeDescriptor class has a companion class called TypeDescriptionProvider and a companion attribute called TypeDescriptionProviderAttribute. You can use a TypeDescriptionProviderAttribute on a class to introduce a completely different way of exposing metadata that meets your design goals.

Here is the implementation which is required for us to reach to the goal.

MyPropertyDescriptor

It’s responsible to provide metadata for property. When implementing this class, for most properties, we will use the trivial implementation which uses original property’s implementations, but for IsReadOnly we will decide based on the value of Editable property of the owner object:

using System;
using System.ComponentModel;
using System.Linq;
public class MyPropertyDescriptor : PropertyDescriptor
{
    PropertyDescriptor p;
    SampleClass o;
    public MyPropertyDescriptor(PropertyDescriptor originalProperty, SampleClass owenr)
        : base(originalProperty) { p = originalProperty; o = owenr; }
    public override bool CanResetValue(object component)
    { return p.CanResetValue(component); }
    public override object GetValue(object component) { return p.GetValue(component); }
    public override void ResetValue(object component) { p.ResetValue(component); }
    public override void SetValue(object component, object value)
    { p.SetValue(component, value); }
    public override bool ShouldSerializeValue(object component)
    { return p.ShouldSerializeValue(component); }
    public override AttributeCollection Attributes { get { return p.Attributes; } }
    public override Type ComponentType { get { return p.ComponentType; } }
    public override bool IsReadOnly { get { return !o.Editable; } }
    public override Type PropertyType { get { return p.PropertyType; } }
}

MyTypeDescriptor

It’s responsible to provide a list of properties for the object. For StringProperty which we are going to change its behavior at run-time, we will return a MyPropertyDescriptor:

using System;
using System.ComponentModel;
using System.Linq;
public class MyTypeDescriptor : CustomTypeDescriptor
{
    ICustomTypeDescriptor d;
    SampleClass o;
    public MyTypeDescriptor(ICustomTypeDescriptor originalDescriptor, SampleClass owner)
        : base(originalDescriptor) { d = originalDescriptor; o = owner; }
    public override PropertyDescriptorCollection GetProperties()
    { return this.GetProperties(new Attribute[] { }); }
    public override PropertyDescriptorCollection GetProperties(Attribute[] attributes)
    {
        var properties = base.GetProperties(attributes).Cast<PropertyDescriptor>()
            .Select(p => p.Name == "StringProperty" ? new MyPropertyDescriptor(p, o) : p)
            .ToArray();
        return new PropertyDescriptorCollection(properties);
    }
}

MyTypeDescriptionProvider

It’s responsible to return a type descriptor for your object, when someone (like property grid) request type description:

using System;
using System.ComponentModel;
public class MyTypeDescriptionProvider : TypeDescriptionProvider
{
    public MyTypeDescriptionProvider()
        : base(TypeDescriptor.GetProvider(typeof(object))) { }

    public override ICustomTypeDescriptor GetTypeDescriptor(Type type, object o)
    {
        ICustomTypeDescriptor baseDescriptor = base.GetTypeDescriptor(type, o);
        return new MyTypeDescriptor(baseDescriptor, (SampleClass)o);
    }
}

SampleClass

At last, the implementation of the class should be changed:

using System;
using System.ComponentModel;
[TypeDescriptionProvider(typeof(MyTypeDescriptionProvider))]
public class SampleClass
{
    [RefreshProperties(RefreshProperties.All)]
    public bool Editable { get; set; }
    string sp;
    public string StringProperty
    {
        get { return sp; }
        set
        {
            if (Editable)
                sp = value;
        }
    }
}

[RefreshProperties(RefreshProperties.All)] informs the PropertyGrid that should refresh all properties appearance when Editable property changed.

Solution 2 – Handling PropertyGrid events and adding ReadOnlyAttribute

Regarding to the example, we want to have the StringProperty of our class be read-only in PropertyGrid. As an option, we can handle PropertyChanged and SelectedObjectsChanged event of the class and add ReadOnly attribute to AttributeCollection which is returned by Attributes property of PropertyDescriptor of the StringProperty. To do so, we need to use reflection to add an attribute to the AttributeCollection.

PropertyDescriptorExtensions

Here I created an extension method which allows us to add ReadOnly attribute to a PropertyDescriptor:

using System;
using System.ComponentModel;
using System.Linq;
using System.Reflection;

public static class PropertyDescriptorExtensions
{
    public static void SetReadOnlyAttribute(this PropertyDescriptor p, bool value)
    {
        var attributes = p.Attributes.Cast<Attribute>()
            .Where(x => !(x is ReadOnlyAttribute)).ToList();
        attributes.Add(new ReadOnlyAttribute(value));
        typeof(MemberDescriptor).GetProperty("AttributeArray",
            BindingFlags.Instance | BindingFlags.NonPublic)
            .SetValue((MemberDescriptor)p, attributes.ToArray());
    }
}

PropertyChanged and SelectedObjectsChanged

Handling PropertyChanged event is because we want to change the appearance of the property in PropertyGrid based on change in Editable property value. Also handling SelectedObjectsChanged is because we want to make sure that after we assigned an instance of the SampleClass to propertyGrid1.SelectedObject the StringProperty is read-only if required:

private void propertyGrid1_PropertyValueChanged(object s, PropertyValueChangedEventArgs e)
{
    var obj = this.propertyGrid1.SelectedObject as SampleClass;
    if(obj!=null)
    {
        TypeDescriptor.GetProperties(this.propertyGrid1.SelectedObject)
            ["StringProperty"].SetReadOnlyAttribute(!obj.Editable);
        propertyGrid1.Refresh();
    }            
}

private void propertyGrid1_SelectedObjectsChanged(object sender, EventArgs e)
{
    var obj = this.propertyGrid1.SelectedObject as SampleClass;
    if (obj != null)
    {
        TypeDescriptor.GetProperties(this.propertyGrid1.SelectedObject)
            ["StringProperty"].SetReadOnlyAttribute(!obj.Editable);
        propertyGrid1.Refresh();
    }
}

SampleClass

In this solution the SampleClass implementation will remain untouched:

using System;
public class SampleClass
{
    public bool Editable { get; set; }

    string stringProperty;
    public string StringProperty
    {
        get { return stringProperty; }
        set { if (Editable) stringProperty = value; }
    }
}

Solution 3 – Add ReadOnly attribute on the fly in the class implementation based on some criteria

SampleClass

Implementation of the sample class should change:

public class SampleClass:Component
{
    bool editable;
    [RefreshProperties(RefreshProperties.All)]
    public bool Editable
    {
        get { return editable; }
        set
        {
            editable = value;
            TypeDescriptor.GetProperties(this)["StringProperty"].SetReadOnlyAttribute(!editable);
        }
    }

    string stringProperty;

    [ReadOnly(true)]
    public string StringProperty
    {
        get { return stringProperty; }
        set
        {
            if (Editable)
                stringProperty = value;
        }
    }
}

The initial [ReadOnly(true)] for the property is because we want it to be read-only by default and change the it at run-time based on the change in Editable property value.

Conclusion

The main difference between above 3 solutions is in changing or not changing the the existing code and also in designer support:

  1. TypeDescrpitor Solution: The solution is attribute based and without changing the logic of the class, you can provide custom metadata for the class at run-time based on some criteria. The changes in the class code, is just because of adding some attributes and the logic of the class will not change. This solution has designer support.

  2. PropertyGrid Events: The solution can satisfy the requirement without changing the existing code. The changes are just in UI layer. This solution doesn’t have designer support and is based on PropertyGrid control events at run-time.

  3. Adding RaedOnly attribute and changing the value at run-time: The solution works well, but you need to change the logic of the code to change ReadOnly attribute on the fly. This solution has designer support.

Notes

  • The post is written to extend and support the answer which I posted for this question in stackoverflow: .Net how to set IsReadOnly for a property for a UserControl

  • In the first version of the post, for SetReadOnlyAttribute I was changing isReadOnly field of ReadOnly attribute. An important risk catched by Ivan Stoev. Using that solution, there is a risk to change the value of static fields ReadOnlyAttribute.Yes, No or Default, because some property descriptors use those static values. I fixed the problem by removing the existing ReadOnly attribute and adding a new attribute instead.

You May Also Like

About the Author: Reza Aghaei

I’ve been a .NET developer since 2004. During these years, as a developer, technical lead and architect, I’ve helped organizations and development teams in design and development of different kind of applications including LOB applications, Web and Windows application frameworks and RAD tools. As a teacher and mentor, I’ve trained tens of developers in C#, ASP.NET MVC and Windows Forms. As an interviewer I’ve helped organizations to assess and hire tens of qualified developers. I really enjoy learning new things, problem solving, knowledge sharing and helping other developers. I'm usually active in .NET related tags in stackoverflow to answer community questions. I also share technical blog posts in my blog as well as sharing sample codes in GitHub.

2 Comments

  1. Awesome awesome. thank you please please can you make a full tutorials for WinForms Designer and Controls Design.

    Examples:
    1.when to use NotifyPropertyChanged(), RefreshProperties(All,Repaint)
    2.How to make a list of dynamic objects which came from SQL (DataSource, DataMember) properties of controls.
    etc

    Thank you. please keep going… 🙂

  2. While this does work, it appears to be quite brittle; any changes to the Sample class results in an an InvalidCastException in the designer.

Leave a Reply

Your email address will not be published. Required fields are marked *