当 <T> 的所有属性都是 read-only 时,collection<T> 的类别不会显示在 PropertyGrid 中

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

正如标题所说,我注意到当所有的class "T" 的属性是 read-only.

下面的代码代表了我的代码结构:

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

问题出在 TestProperty3。当它是 read-only 时,类别 ("Category 1") 未显示在 属性 网格中...

但如果我执行 属性 可编辑,则显示类别...

C:#

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

VB.NET:

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

不仅如此,让我们假设在 TestClass3 中声明了 10 个属性(而不是本例中的 1 个),其中 9 个是 read-only,并且1是可编辑的,那么,在这种情况下,所有的类别都会显示出来。另一方面,如果所有 10 个属性都是 read-only,则不会显示类别。

PeopertyGrid 的这种行为对我来说非常烦人和意外。我想查看我的自定义类别,无论在我的 class 中声明的属性是否带有 setter。

我必须用什么替代方法来显示具有我的 class read-only 所有属性的类别?。也许写一个自定义 TypeConverter 或 collection 编辑器可以解决这个烦人的视觉表现行为?

这确实是一个非常烦人的行为。但是,我不相信您可以绕过它:错误的不是 属性-描述符 - 它报告了正确的类别 - 您可以通过以下方式验证:

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

输出 Category 1: TestProperty3.

所以;这只是集合编辑器 UI 控件的一个怪癖。奇怪的是,如果您添加第二个 writable 属性,它会开始显示两者的类别。但是如果你添加第二个read-only 属性:它不显示类别。这适用于仅 get 属性和标记为 [ReadOnly(true)].

的属性

所以:我认为这里没有好的解决方案,除了可能使用不同的 属性-grid 实现,或者添加一个虚拟可写 属性 - 抱歉!


作为 side/unrelated 注意:当使用 {get;set;} = "initial value"; 样式初始化(​​或构造函数初始化)时,最好也将 [DefaultValue("initial value")] 添加到 属性 ,以便它获得正确的 ShouldSerialize*() 行为(或 PropertyGrid 术语:使其适当地加粗/不加粗),但是......这不会解决你看到的问题, 对不起。

向你的 class.

中的虚拟可写但不 browse-able 属性 问好

当然,这是 属性 网格错误(?)的解决方法,但考虑到创建自定义 collection 编辑器表单和实现自定义 UITypeEditor 所需的开销,后者将使用您的自定义表单只是为了克服这种行为,它至少应该命名为 semi-elegant 解决方案。

代码:

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

这些是使用和不使用虚拟机的结果 属性:

如果您仍想为 collection 实施自定义编辑器,请查看 this thread 中已接受的答案。它没有经历整个过程,但它是一个很好的起点。

希望对您有所帮助。

这不是错误,属性 网格就是这样设计的。如果一个组件的所有属性都是只读的,则该组件被视为 "immutable"。在这种情况下,它被包裹在那个时髦的 "Value" 包装器 属性.

一个解决方案是在 class(或实例)上声明一个自定义 TypeDescriptionProvider,这会带来问题。 此提供程序将 return 一个 custom type descriptor 实例,该实例将添加一个虚拟不可浏览(对 属性 网格不可见)非只读 属性,因此 class不再被视为 "immutable"。

这是您可以使用它的方式,例如:

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;
}

这是预期的样子:

这是代码。

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;
    }
}

此解决方案的优点是不需要对原始 class 进行任何更改。但它可能会对您的代码产生其他影响,因此您确实希望在您的上下文中对其进行测试。另外,请注意,您 can/should 在网格关闭后删除提供程序。

这不是 PropertyGrid 的错误,这是 CollectionEditorCollectionForm 的功能(错误?)。

如果您将 TestClass3 的实例直接分配给 属性 网格,您将看到 属性 网格按预期显示类别下的属性。但是当 CollectionForm 试图在其 属性 网格中显示 TestClass3 的实例时,因为它没有任何可设置的 属性 并且其集合转换器不支持创建item 实例,然后它决定将该对象包装到另一个派生自定义类型描述符的对象中,显示与 class 名称同名的类别下的所有属性。

正如其他答案已经建议的那样,您可以通过

修复它
  • 正在向您的 class
  • 添加一个虚拟的不可浏览的可写 属性
  • 或者通过注册一个新的类型描述符,当它被要求 return 属性列表 return 时,它是一个虚拟的不可浏览的可写 属性

但我不想仅仅因为 CollectionForm 错误而更改 class 或其类型描述符。

由于问题出在 CollectionFormCollectiorEditor,作为另一种选择,您可以通过创建从 CollectionEditor 派生的集合编辑器并覆盖其 CreateCollectorForm 方法并在它尝试在集合编辑器表单中设置 属性 网格的选定对象时更改其行为:

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;
    }
}

然后用这个属性装饰TesProperty2就够了:

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