当<T>的所有属性都是只读时,对于集合<T>,在PropertyGrid中不会显示类别。

11
正如标题所述,我注意到当类“T”的所有属性都是只读时,在**PropertyGrid*(默认集合编辑器)中不会显示集合的类别。
下面的代码表示我拥有的代码结构:
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上。当它是只读的时候,属性网格中不会显示类别(“Category 1”)...

enter image description here

但如果我将该属性设置为可编辑,则类别将会显示...

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

此外,让我们想象一下,在 TestClass3 中声明了10个属性(而不是像这个例子中的1个),其中9个是只读的,而1个是可编辑的,则在这种情况下所有类别都将显示。另一方面,如果所有10个属性都是只读的,则不会显示类别。

对于我来说,PeopertyGrid的这种行为非常令人恼火和意外。无论我的类中是否声明了具有setter或没有setter的属性,我都希望看到我的自定义类别。

我有哪些替代方案可以展示所有属性都是只读的类别?也许编写自定义TypeConverter或集合编辑器可以修复这种令人讨厌的可视化表示行为吗?


1
请参考此答案:如何为用户控件设置属性的IsReadOnly。您可以使用它来更改/ 伪造 具有 set(具有setter但实际上不设置任何内容)的属性的只读方面。也许这不是完美的解决方案,但它可能会有用。 - Jimi
1
“it could be useful” 意味着提供的解决方案具有一些独特的实现方式,可以用于自定义 TypeDescriptionProvider,添加一些缺失的功能。 - Jimi
4个回答

3
这不是PropertyGrid的问题,而是CollectionEditorCollectionForm的特性(问题?)。
如果直接将TestClass3的实例分配给属性网格,则会按预期显示类别下的属性。但是当CollectionForm尝试在其属性网格中显示TestClass3的实例时,由于它没有任何可设置的属性,且其集合转换器不支持创建项目实例,因此它决定将对象包装到另一个继承自自定义类型描述符的对象中,在与类名相同的类别下显示所有属性。
正如其他答案已经建议的那样,您可以通过以下方式修复它:
  • 向类添加虚拟的不可浏览可写属性
  • 或者注册一个新的类型描述符,当被要求返回属性列表时返回虚拟的不可浏览可写属性
但我更喜欢不改变类或其类型描述符,仅因为CollectionForm的问题。
由于问题出在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))]

1
太棒了。我认为没有比这更好的解决方案了。谢谢! - ElektroStudios
1
你应该提到这依赖于集合编辑器的内部/未记录对象,因此它并不是没有风险/未来风险的。 - Simon Mourier
1
此外,您必须更改类(向属性添加属性)才能使其正常工作。 - Simon Mourier
1
让我说,我喜欢这两种解决方案,它们在我的情况下都很有效,但最终我不能选择/接受超过一个答案。也许我错了,但我觉得这个解决方案更接近问题的根源(集合编辑器),通过继承CollectionEditor类,因此生成的代码比需要编写3个类的TypeDescriptionProvider方法要小得多,也更加“清洁”。这并不是说Simon Mourier的答案不好,只是一种不同的方法,同样有效。感谢你们的帮助。 - ElektroStudios
1
大家好,我想与您分享一下最终的可视化表现,这都要归功于您们的帮助:https://github.com/ElektroStudios/S.M.A.R.T.-Tool-for-.NET(当然,在 readme.md 文件中也提到了这两位)。祝您有美好的一天。 - ElektroStudios
显示剩余10条评论

2

让我们来认识一下类中可写但不可浏览的虚拟属性。

当然,这是属性网格的一个问题的解决方法(?),但考虑到创建自定义集合编辑器表单和实现自定义UITypeEditor所需的开销,而这些又需要使用您的自定义表单来克服此行为,因此它至少应被称为半优雅的解决方案。

代码:

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

这是有和没有虚拟属性的结果: enter image description here 如果您仍想为您的集合实现自定义编辑器,请查看此线程中被接受的答案。它并没有涵盖整个过程,但这是一个很好的起点。
希望这可以帮到您。

您扩展了有关@Marc Gravell答案中含糊提到的解决方法的信息。我知道BrowsableAttribute类及其在此场景中的影响,但无论如何,我感谢收到答案,但是,正如我在Marc答案的评论框中提到的:“强制声明可写属性不是我认为的解决方案,因为它不是解决问题,而是使问题更加复杂”。 “TestClass3”中声明的成员结构不应更改。我希望您能理解我的观点,这不是我期望的答案/解决方案。无论如何,还是谢谢。 - ElektroStudios
1
@ElektroStudios 当然我理解你的观点,不用担心。我在这里发布我的答案是为了作为这个错误的解决方法存在。我也有许多只读属性的类,我使用这个解决方法,因为除了实现自己的集合编辑器表单并实现自定义UITypeEditor来使用该表单之外,没有其他方法可以通过使用属性网格允许的内容(属性装饰等)来修复此错误。但是,由于默认的集合编辑器表单使用属性网格,因此您可以看到实现正确解决方案所需的开销。 - ChD Computers

2

这确实是一种非常烦人的行为。然而,我不认为你可以绕过它:它并不是属性描述符出了问题 - 它正在报告正确的类别 - 你可以通过以下方式进行验证:

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

这段代码输出的是Category 1: TestProperty3

这只是集合编辑器UI控件的一个怪癖。奇怪的是,如果您添加第二个可写属性,则会同时显示两个属性的类别。但是,如果您添加第二个只读属性,则不会显示类别。这适用于仅get属性和标记为[ReadOnly(true)]的属性。

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


作为一个副作用/无关的注释:当使用{get;set;} = "initial value";样式初始化(或构造函数初始化)时,最好还要添加[DefaultValue("initial value")]到该属性中,以便正确获取ShouldSerialize*()行为(或在PropertyGrid术语中:使其适当地加粗/不加粗),但是这并不能解决您看到的问题,很抱歉。


我非常感谢您提供详细的答案,但正如您所理解的那样,强制声明可写属性并不是我认为的解决方案,因为它不是解决问题,而是让问题更加复杂。 - ElektroStudios
关于你的话:“这只是集合编辑器UI控件的一个怪癖” - 看,有一件事是肯定的:有一个“标志”(无论是算法、函数的返回值还是布尔变量)告诉PropertyGrid和/或CollectionEditor何时显示类别,何时不应该。在PropertyGrid或CollectionEditor类中的某个地方(或者可能是其中一个继承类中),它会检查对象的所有属性是否都是只读的,如果全部都是只读的,那么告诉显示类别的“标志”就会被禁用... - ElektroStudios
那么,您认为在深入分析公共.NET参考源代码中的PropertyGrid和CollectionEditor类之后,通过反射编写解决方案以获取我的PropertyGrid中的CollectionEditor实例并激活强制显示类别的“标志”可能是可行的思路吗?您认为这是可能的吗? - ElektroStudios
事实上,我正在分析PropertyGridd、PropertyGridView、UITypeEditor、CollectionEditor和其他相关类的源代码,但是这是一个噩梦,有大量声明的成员,即使搜索特定关键字以丢弃源代码中不相关的部分,仍然很难(至少对我来说)确定集合编辑器决定在何处显示类别。但是,至少我尝试着自己做到了。 - ElektroStudios

2
这不是一个错误,属性网格就是设计成这样的。如果一个组件的所有属性都是只读的,那么它被认为是“不可变”的。在这种情况下,它被包装到那个奇怪的“Value”包装属性中。
其中一个解决方案是在造成问题的类(或实例)上声明自定义 TypeDescriptionProvider。该提供程序将返回一个自定义类型描述符实例,该实例将添加一个虚拟的不可浏览(对属性网格不可见)且非只读属性,以便该类不再被视为“不可变”。
以下是例如如何使用它:
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;
}

这就是预期的样子:

enter image description here

这是代码。
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;
    }
}

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

这是漂亮且可重用的代码,谢谢。顺便说一下,关于你的话:“但它可能会对你的代码产生其他影响” - 也许你能提到任何需要注意的重要负面影响吗? - ElektroStudios
1
你改变对象的“外观”可能会对代码的其他部分产生影响,因为许多程序使用类型描述符。但是,如果你将其限制在属性网格周围(按照我说的使用添加和删除),那么它就不应该成为问题。 - Simon Mourier

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接