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...
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"
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?.
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
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))]
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:
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.
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:
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With