如果<t>的所有属性均为只读

时间:2018-12-18 10:12:08

标签: c# .net vb.net generics propertygrid

正如标题所述,当类别“ T”的所有属性均为只读时,我注意到类别未显示在集合(Of 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 。如果为只读,则类别(“类别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声明了属性。

我必须显示哪些替代品来将类的所有属性设为只读?也许编写自定义的 TypeConverter 或集合编辑器可以解决这种烦人的视觉表示行为?。

4 个答案:

答案 0 :(得分:2)

这确实是一个非常令人讨厌的行为。但是,我不相信您能解决这个问题:不是属性描述符有问题-它报告的类别正确-您可以通过以下方式进行验证:

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

输出Category 1: TestProperty3

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

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


作为附带说明/不相关的说明:使用{get;set;} = "initial value";样式初始化(​​或构造函数初始化)时,最好还向该属性添加[DefaultValue("initial value")],以使其获得{{ 1}}的行为正确(或用ShouldSerialize*()表示:因此它是粗体/非粗体),但是...对不起,这无法解决您遇到的问题。

答案 1 :(得分:2)

向类中的可写但不可浏览的虚拟属性问好。

这当然是属性网格的bug(?)的一种解决方法,但是鉴于创建自定义集合编辑器表单和实现自定义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

这些是带有和不带有dummy属性的结果:

enter image description here

如果您仍想为您的馆藏实现自定义编辑器,请在this thread中签出接受的答案。它并没有贯穿整个过程,但它是一个很好的起点。

希望这会有所帮助。

答案 2 :(得分:2)

这不是错误,属性网格是按这种方式设计的。如果组件的所有属性均为只读,则将其视为“不可变的”。在这种情况下,它将包装到该时髦的“值”包装器属性中。

一种解决方案是在存在问题的类(或实例)上声明自定义TypeDescriptionProvider。 此提供程序将返回一个custom type descriptor实例,该实例将添加一个虚拟的不可浏览(对属性网格不可见)的非只读属性,因此该类不再被视为“不可变的”。

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

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

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

答案 3 :(得分:2)

这不是PropertyGrid的错,是CollectionForm的{​​{1}}的功能(故障?)。

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

如其他答案所建议,您可以通过

进行修复
  • 向您的班级添加一个虚拟的不可浏览的可写属性
  • 或者通过注册一个新的类型描述符,当它被要求返回属性列表时,该描述符返回一个虚拟的不可浏览的可写属性

但是我宁愿不仅仅因为TestClass3错误而更改类或其类型描述符。

由于问题是CollectionFormCollectionForm,作为另一种选择,您可以通过创建派生自CollectiorEditor的集合编辑器并覆盖其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