Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?.

like image 337
ElektroStudios Avatar asked Dec 18 '18 10:12

ElektroStudios


3 Answers

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))]
like image 85
Reza Aghaei Avatar answered Sep 19 '22 13:09

Reza Aghaei


Say hello to the dummy writable but not browse-able property in your class.

Of course this is a workaround of the property grid's bug(?) but given the overhead needed to create a custom collection editor form and implement a custom UITypeEditor which in turn will use your custom form just to overcome this behavior, it should be named at least a semi-elegant solution.

Code:

Imports System.Collections.ObjectModel
Imports System.ComponentModel

Public Class Form1

    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles Me.Load
        Dim tc1 As New TestClass1
        PropertyGrid1.SelectedObject = tc1
    End Sub

    <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 TestProperty1 As String = "Test 1"
        <Category("Category 1")>
        Public ReadOnly Property TestProperty2 As String = "Test 2"
        <Category("Category 1")>
        Public ReadOnly Property TestProperty3 As String = "Test 3"
        <Category("Category 2")>
        Public ReadOnly Property TestProperty21 As String = "Test 21"
        <Category("Category 2")>
        Public ReadOnly Property TestProperty22 As String = "Test 22"
        <Category("Category 2")>
        Public ReadOnly Property TestProperty23 As String = "Test 23"
        'We use the following dummy property to overcome the problem with the propertygrid
        'that it doesn't display the categories once all the properties in the category
        'are readonly...
        <Browsable(False)>
        Public Property DummyWriteableProperty As String
            Get
                Return String.Empty
            End Get
            Set(value As String)

            End Set
        End Property
    End Class

End Class

These are the results with and without the dummy property:

enter image description here

If you still want to implement a custom editor for your collections checkout the accepted answer in this thread. It doesn't go through the whole process but it is a good place to start.

Hope this helps.

like image 38
ChD Computers Avatar answered Sep 18 '22 13:09

ChD Computers


This is not a bug, the property grid is designed that way. A component is considered as "immutable" if all its properties are read-only. In this case, it's wrapped into that funky "Value" wrapper property.

One solution is to declare a custom TypeDescriptionProvider on the class (or instance) that poses a problem. This provider will return a custom type descriptor instance which will add a dummy non-browsable (invisible to the property grid) non-readonly property, so the class is not considered "immutable" any more.

This is how you can use it, for example:

public Form1()
{
    InitializeComponent();

    // add the custom type description provider
    var prov = new NeverImmutableProvider(typeof(TestClass3));
    TypeDescriptor.AddProvider(prov, typeof(TestClass3));

    // run the property grid
    var c2 = new TestClass2();

    propertyGrid1.SelectedObject = c2;
}

This is how it will look like, as expected:

enter image description here

And here is the code.

public class NeverImmutableProvider : TypeDescriptionProvider
{
    public NeverImmutableProvider(Type type)
        : base(TypeDescriptor.GetProvider(type))
    {
    }

    public override ICustomTypeDescriptor GetTypeDescriptor(Type objectType, object instance) => new MyTypeProvider(base.GetTypeDescriptor(objectType, instance));

    private class MyTypeProvider : CustomTypeDescriptor
    {
        public MyTypeProvider(ICustomTypeDescriptor parent)
            : base(parent)
        {
        }

        public override PropertyDescriptorCollection GetProperties() => GetProperties(null);
        public override PropertyDescriptorCollection GetProperties(Attribute[] attributes)
        {
            var props = new List<PropertyDescriptor>(base.GetProperties(attributes).Cast<PropertyDescriptor>());
            props.Add(new MyProp());
            return new PropertyDescriptorCollection(props.ToArray());
        }
    }

    private class MyProp : PropertyDescriptor
    {
        public MyProp()
            : base("dummy", new Attribute[] { new BrowsableAttribute(false) })
        {
        }

        // this is the important thing, it must not be readonly
        public override bool IsReadOnly => false;

        public override Type ComponentType => typeof(object);
        public override Type PropertyType => typeof(object);
        public override bool CanResetValue(object component) => true;
        public override object GetValue(object component) => null;
        public override void ResetValue(object component) { }
        public override void SetValue(object component, object value) { }
        public override bool ShouldSerializeValue(object component) => false;
    }
}

This solution has the advantage of not requiring any change to the original class. But it can have other implications in your code, so you really want to test it in your context. Also, note you can/should remove the provider once the grid has been closed.

like image 24
Simon Mourier Avatar answered Sep 21 '22 13:09

Simon Mourier