11

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

ElektroStudios
  • 19,105
  • 33
  • 200
  • 417
  • 1
    See this answer: [How to set IsReadOnly for a property for a usercontrol](https://stackoverflow.com/questions/48360043/net-how-to-set-isreadonly-for-a-property-for-a-usercontrol?answertab=active#tab-top). You might use it to alter / *fake* the read-only aspect of a property which has an *empty* `set` (has a setter but it actually sets nothing). Maybe it's not the *perfect* solution but it could be useful. – Jimi Dec 18 '18 at 12:07
  • 1
    With *it could be useful* I mean that the provided solutions have some peculiar implementations that can be used to customize the `TypeDescriptionProvider`, adding some missing features. – Jimi Dec 18 '18 at 12:16

4 Answers4

3

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))]
Reza Aghaei
  • 120,393
  • 18
  • 203
  • 398
  • This way you don't need to add a dummy property to the class, or to the list of its properties returned by a type descriptor. – Reza Aghaei Dec 21 '18 at 20:06
  • 1
    Awesome. I don't think it could exists a better solution than this. Thank you!. – ElektroStudios Dec 21 '18 at 22:21
  • 1
    You should mention this relies on internal/undocumented objects of the collection editor, so it's not risk/future-free. – Simon Mourier Dec 22 '18 at 10:48
  • 1
    Plus you do have to change the class (add an attribute to the property) to make it work. – Simon Mourier Dec 22 '18 at 10:59
  • In general I agree with @SimonMourier but in this case I would say, just by adding some null checking (for example checking to see if `Value` property exists), the code is reliable enough. We just subscribed to an event and we didn't prevent other subscribers from receiving it, so even if we suppose there may be some changes in the internal implementations, they will not make any problem for our code. – Reza Aghaei Dec 22 '18 at 13:55
  • 1
    On the other hand, this fault/feature, is not documented itself, and without reading internal implementations we cannot offer any solution, including adding a dummy non-browsable writable property or changing adding such property descriptor. While in a lot of cases I also use type description and I really like it, but I cannot say registering a new collection editor is not safe or is more risky than adding dummy property or property descriptor. Registering an `Editor` attribute is a really natural and common scenario in Windows Forms. – Reza Aghaei Dec 22 '18 at 14:02
  • After all, it's Windows Forms,and it's really less likely to have a new feature and probably impossible to have a breaking change in such things ... – Reza Aghaei Dec 22 '18 at 14:38
  • 1
    Let me say that I love both solutions, and both of them are effective at least in my scenario, but at the end I can't choose/accept more than one answer. Maybe I could be wrong but I feel this solution is one step closer to the root of the problem (the collection editor) by inheriting the **CollectionEditor** class, and maybe for that reason the resulting code is proportionally smaller and "cleaner" than the need to write 3 classes for the TypeDescriptionProvider approach.It doesn't not make Simon Mourier's answer worst,just a different approach,but also effective.Thank you both for the help. – ElektroStudios Dec 22 '18 at 15:26
  • 1
    Hey guys, I just want to share with you the resulting visual representation thanks both for your help: https://github.com/ElektroStudios/S.M.A.R.T.-Tool-for-.NET (of course both are mentioned in the readme.md). Have a nice day. – ElektroStudios Dec 23 '18 at 12:54
  • 1
    I feel that I should mention here that the OP declares (in a comment in my answer) "The declared members structure that I have in "TestClass3" should not be changed" and he finally accepts an answer that requires the declared members to be changed (add an attribute to the property), moreover the provided code is based on undocumented features of the derived editor. For example there is no warranty that the control named "propertyBrowser" or "listbox" name will not be changed in the future. Nevertheless I think that @SimonMourier 's answer is the most elegant solution given the factors... – ChD Computers Dec 25 '18 at 18:58
  • @Christos I guess you didn't read the comments. I also like the other answer and it was one of my options to answer this question as well as the solution which you used. But what I shared will be my preferred solution. Maybe its opinion based ;). But I should mention having one good solution doesn't mean other solutions are bad so we don't need to struggle about the solutions. – Reza Aghaei Dec 25 '18 at 19:06
  • @Christos about *The declared members structure that I have in "TestClass3" should not be changed* while I believe it's OP's preference, I would say, decorating a property with an attribute is not changing members structure. – Reza Aghaei Dec 25 '18 at 19:09
  • @Christos About relying on internal things, read [this comment](https://stackoverflow.com/questions/53830738/categories-are-not-shown-in-propertygrid-for-a-collectiont-when-all-the-prope/53890224?noredirect=1#comment94638153_53890224) and [this one](https://stackoverflow.com/questions/53830738/categories-are-not-shown-in-propertygrid-for-a-collectiont-when-all-the-prope/53890224?noredirect=1#comment94638247_53890224). – Reza Aghaei Dec 25 '18 at 19:12
  • 1
    Dear @RezaAghaei rest assured that I have read (and understand) **everything** in this thread, as I said I felt to comment and I did. Acting on your feeling / guessing that I didn't read or I didn't understand is something that I would not like to discuss or comment further. Thank you anyway and merry Christmas! – ChD Computers Dec 25 '18 at 19:38
  • @Christos I hope my comment has not offended you as I didn't mean anything offending. Thanks for sharing your point of view and merry Christmas to you too! Cheers! – Reza Aghaei Dec 25 '18 at 19:46
2

That is indeed a very annoying behaviour. However, I don't believe you can get around it: it isn't the property-descriptor that is at fault - it is reporting the correct category - as you can validate via:

var props = TypeDescriptor.GetProperties(new TestClass3());
foreach(PropertyDescriptor prop in props)
{
    Console.WriteLine($"{prop.Category}: {prop.Name}");
}

which outputs Category 1: TestProperty3.

So; this is simply a quirk of the collection editor UI control . Oddly, if you add a second writable property, it starts displaying the category for both. But if you add a second read-only property: it doesn't display the category. This applies both for get-only properties, and properties marked [ReadOnly(true)].

So: I don't think there's a good solution here, except perhaps to use a different property-grid implementation, or to add a dummy writable property - sorry!


As a side/unrelated note: when using the {get;set;} = "initial value"; style initialization (or constructor initialization), it is a good idea to also add [DefaultValue("initial value")] to that property, so that it gets the ShouldSerialize*() behaviour correct (or in PropertyGrid terms: so that it is bold / not-bold appropriately), but... this won't fix the problem you're seeing, sorry.

Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • I really appreciate your answer for giving details, but as you will understand, forcing to declare a writable property is not what I consider a solution because instead of solving the problem, it undergoes to it. – ElektroStudios Dec 20 '18 at 03:29
  • About your words: "this is simply a quirk of the collection editor UI control" - see, one thing is for sure: there is a "flag" (whatever, like an algorithm, the return value of a function or a boolean variable) that tells the PropertyGrid and/or the CollectionEditor itself when to show categories and when should not. Somewhere in the PropertyGrid or CollectionEditor class (or maybe in one of the inherited classes) it checks whether all the properties of the object are read-only, and if all are read-only then that "flag" that tells to show categories deactivates... – ElektroStudios Dec 20 '18 at 03:31
  • Then, do you think that maybe after analyzing in deep the PropertyGrid and CollectionEditor classes in the public .NET reference source-code, it could be a viable thinking to start writting a solution via Reflection to get the instance of CollectionEditor from my PropertyGrid to activate that "flag" that forces to show categories?. Do you consider it possible to do?. – ElektroStudios Dec 20 '18 at 03:32
  • In fact I was analyzing the source-code of the PropertyGridd, PropertyGridView, UITypeEditor, CollectionEditor and other related classes but it is a nightmare with loads of declared members that even searching for specific keywords to discard non-related parts of the source-code it still makes very difficult (at least for me) to try determine how and where the collection editor decides to show categories. But hey, at least I tried to do it by myself. – ElektroStudios Dec 20 '18 at 04:00
2

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.

ChD Computers
  • 3,135
  • 3
  • 23
  • 33
  • You extended the info about a workaround that was vaguely mentioned in @Marc Gravell answer.I was aware of the BrowsableAttribute class and its effects in this scenario,but anyway I'm thankful for receiving an answer,however,as I mentioned in the commentary box of Marc answer:"forcing to declare a writable property is not what I consider a solution because instead of solving the problem,it undergoes to it". The declared members structure that I have in "TestClass3" should not be changed.I hope you can understand my point of view,this is not the answer/solution that I expect.Thanks anyway. – ElektroStudios Dec 20 '18 at 21:02
  • 1
    @ElektroStudios Of course I understand your POV don't worry. I posted my answer here to exist as a workaround for this bug. I also have many classes with only readonly properties and I use this workaround because there isn't a way by using what the property grid allows (property decorations, etc) that can fix this bug other than to implement your own collection editor form and then implement a custom UITypeEditor to use that form instead of the default one. However, since the default collection editor form uses the property grid you can see the overhead needed to implement a correct solution. – ChD Computers Dec 20 '18 at 21:42
2

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.

Simon Mourier
  • 132,049
  • 21
  • 248
  • 298
  • This is beautiful and reusable code, thank you. By the way, about your words: "But it can have other implications in your code" - maybe could you mention any important negative implication to be aware of it?. – ElektroStudios Dec 21 '18 at 11:54
  • 1
    The fact that you're changing the "facade" of an object can have implication on other parts of your code as many programs use type descriptors. But if you restrict that around the property grid, (use Add and Remove after as I said), then it shouldn't pose a problem – Simon Mourier Dec 21 '18 at 12:10