Search code examples
c#.netvb.netgenericspropertygrid

Categories are not shown in PropertyGrid for a collection<T>, when all the properties of <T> are read-only


As the title says, I noticed that the categories are not shown in a **PropertyGrid* (in its default collection editor) for a collection(Of T), when all the properties of class "T" are read-only.

The code below represents the code structure I have:

C#:

[TypeConverter(typeof(ExpandableObjectConverter))]
public class TestClass1 {

    public TestClass2 TestProperty1 {get;} = new TestClass2();
}

[TypeConverter(typeof(ExpandableObjectConverter))]
public sealed class TestClass2 {

    [TypeConverter(typeof(CollectionConverter))]
    public ReadOnlyCollection<TestClass3> TestProperty2 {
        get {
            List<TestClass3> collection = new List<TestClass3>();
            for (int i = 0; i <= 10; i++) {
                collection.Add(new TestClass3());
            }
            return collection.AsReadOnly();
        }
    }
}

[TypeConverter(typeof(ExpandableObjectConverter))]
public sealed class TestClass3 {

    [Category("Category 1")]
    public string TestProperty3 {get;} = "Test";
}

VB.NET:

<TypeConverter(GetType(ExpandableObjectConverter))>
Public Class TestClass1

    Public ReadOnly Property TestProperty1 As TestClass2 = New TestClass2()

End Class

<TypeConverter(GetType(ExpandableObjectConverter))>
Public NotInheritable Class TestClass2

    <TypeConverter(GetType(CollectionConverter))>
    Public ReadOnly Property TestProperty2 As ReadOnlyCollection(Of TestClass3)
        Get
            Dim collection As New List(Of TestClass3)
            For i As Integer = 0 To 10
                collection.Add(New TestClass3())
            Next
            Return collection.AsReadOnly()
        End Get
    End Property

End Class

<TypeConverter(GetType(ExpandableObjectConverter))>
Public NotInheritable Class TestClass3

    <Category("Category 1")>
    Public ReadOnly Property TestProperty3 As String = "Test"

End Class

The problem is with TestProperty3. When it is read-only, the category ("Category 1") is not shown in the property grid...

enter image description here

But if I do the property editable, then the category is shown...

C:#

[Category("Category 1")]
public string TestProperty3 {get; set;} = "Test";

VB.NET:

<Category("Category 1")>
Public Property TestProperty3 As String = "Test"

enter image description here

More than that, let's imagine that in TestClass3 are declared 10 properties (instead of 1 like in this example), and 9 of them are read-only, and 1 is editable, then, in this circumstances all the categories will be shown. On the other side, if all the 10 properties are read-only, then categories will not be shown.

This behavior of the PeopertyGrid is very annoying and unexpected for me. I would like to see my custom categories regardless of whether in my class are declared properties with a setter or without it.

What alternatives I have to show categories having all the properties of my class read-only?. Maybe writing a custom TypeConverter or collection editor could fix this annoying visual representation behavior?.


Solution

  • It's not fault of PropertyGrid, it's feature (fault?) of CollectionForm of the CollectionEditor.

    If you assign an instance of TestClass3 directly to a property grid, you will see the property grid is showing properties under categories as expected. But when CollectionForm is trying to show an instance of TestClass3 in its property grid, since it doesn't have any settable property and its collection converter doesn't support creating item instance, then it decides to wrap the object into another object deriving custom type descriptor, showing all properties under a category with the same name as the class name.

    As already suggested by other answers, you can fix it by

    • Adding a dummy non-browsable writable property to your class
    • Or by registering a new type descriptor which returns a dummy non-browsable writable property when it's asked to return list of properties

    But I'd prefer to not change the class or its type descriptor just because of CollectionForm fault.

    Since the problem is with CollectionForm or the CollectiorEditor, as another option you can solve the problem by creating a collection editor deriving from CollectionEditor and override its CreateCollectorForm method and change its behavior when it tries to set selected object of the property grid in the collection editor form:

    public class MyCollectionEditor<T> : CollectionEditor
    {
        public MyCollectionEditor() : base(typeof(T)) { }
        public override object EditValue(ITypeDescriptorContext context, 
            IServiceProvider provider, object value)
        {
            return base.EditValue(context, provider, value);
        }
        protected override CollectionForm CreateCollectionForm()
        {
            var f = base.CreateCollectionForm();
            var propertyBrowser = f.Controls.Find("propertyBrowser", true)
                .OfType<PropertyGrid>().FirstOrDefault();
            var listbox = f.Controls.Find("listbox", true)
               .OfType<ListBox>().FirstOrDefault();
            if (propertyBrowser != null && listbox !=null)
                propertyBrowser.SelectedObjectsChanged += (sender, e) =>
                {
                    var o = listbox.SelectedItem;
                    if (o != null)
                        propertyBrowser.SelectedObject =
                            o.GetType().GetProperty("Value").GetValue(o);
                };
            return f;
        }
    }
    

    Then it's enough to decorate TesProperty2 with this attribute:

    [Editor(typeof(MyCollectionEditor<TestClass3>), typeof(UITypeEditor))]