C# 强制 PropertyGrid 不展开子 class 属性 并在根级别显示它

C# force PropertyGrid to not expand a sub-class property and display it at root level

我遇到了 PropertyGrid 的显示问题。 我有一个名为 Product 的对象,它有一个 属性 Fields 作为 List< Field >嵌套对象。

我使用自定义 TypeConverters 和 PropertyDescriptors 就像在许多在线文章中一样,我实现了这个行为:

正如预期的那样,Fields 扩展得很好,但我试图不将它们扩展到一个单独的子类别中,我只需要与根成员处于同一级别的 Fields 成员。

现在,由于 Product 是一个可绑定对象,我正在尝试使用转换器实现此功能(即,不要循环,只填充 PG 或创建一个新对象)。

我尝试了很多东西,是否可以欺骗 TypeConverter 来做到这一点?这是功能代码:

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }

    private void Form1_Load(object sender, EventArgs e)
    {

        Product product = new Product
        {
            Symbol = "test",
            Details = new PartDetails
            {
                FileLineNo = 123,
                Orientation = "up",
                X = 555,
                Y = 888
            },

            Fields = new FieldList { 
                new Field { Name = "One", Value = "Value 1" },
                new Field { Name = "Two", Value = "Value 2" },
                new Field { Name = "Three", Value = 1234 }
            }

        };

        propertyGrid1.SelectedObject = product;
        propertyGrid1.ExpandAllGridItems();
    }
}


public class Product
{

    public string Symbol { get; set; }

    [TypeConverter(typeof(FieldListTypeConverter))]
    public FieldList Fields { get; set; }

    [TypeConverter(typeof(ExpandableObjectConverter))]
    public PartDetails Details { get; set; }

}

public class PartDetails
{
    public int FileLineNo { get; set; }
    public int X { get; set; }
    public int Y { get; set; }
    public string Orientation { get; set; }
}

public class Field
{
    public string Name { get; set; } 
    public object Value { get; set; } 

}



public class FieldList :
     List<Field>
//, ICustomTypeDescriptor
{

}

public class FieldListTypeConverter : TypeConverter
{
    public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
    {
        if (destinationType != typeof(string))
            return base.ConvertTo(context, culture, value, destinationType);

        return "";
    }

    public override bool GetPropertiesSupported(ITypeDescriptorContext context)
    {
        return true;
    }

    public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext context, object obj, Attribute[] attributes)
    {
        List<PropertyDescriptor> pdList = new List<PropertyDescriptor>();
        List<Field> fields = obj as List<Field>;
        if (fields != null)
        {
            foreach (Field field in fields)
            {
                FieldDescriptor fd = new FieldDescriptor(field);

                pdList.Add(fd);
            }

        }
        return new PropertyDescriptorCollection(pdList.ToArray());
    }

    private class FieldDescriptor : SimplePropertyDescriptor
    {
        public Field field { get; private set; } // instance

        public FieldDescriptor(Field field)
            // component type, property name, property type
            : base(field.GetType(), field.Name, field.Value.GetType())
        {
            this.field = field;
        }

        public override object GetValue(object obj)
        {
            return field.Value;
        }

        public override void SetValue(object obj, object value)
        {
            field.Value = value;
        }

        public override bool IsReadOnly
        {
            get { return false; }
        }
    }
}

总的来说你的想法是正确的,但是你的实现是错误的。

如果您希望字段显示为产品的属性,产品必须为字段中的每个项目提供 PropertyDescriptor 本身。您可以使用应用于 Product class.

的 TypeConverter 来实现此目的
[TypeConverter(typeof(ProductTypeConverter))]
public class Product
{
    public string Symbol { get; set; }
    //[TypeConverter(typeof(FieldListTypeConverter))]
    public FieldList Fields { get; set; }
    [TypeConverter(typeof(ExpandableObjectConverter))]
    public PartDetails Details { get; set; }
}

与:

public class ProductTypeConverter : TypeConverter
{
    public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
    {
        if (destinationType != typeof(string))
        {
            return base.ConvertTo(context, culture, value, destinationType);
        }

        return "";
    }

    public override bool GetPropertiesSupported(ITypeDescriptorContext context)
    {
        return true;
    }

    public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext context, object instance, Attribute[] attributes)
    {
        PropertyDescriptorCollection pdc = TypeDescriptor.GetProperties(instance, attributes, true);
        PropertyDescriptor fieldsDescriptor = pdc.Find("Fields", false);
        List<PropertyDescriptor> pdList = new List<PropertyDescriptor>();
        foreach (PropertyDescriptor pd in pdc)
        {
            if (pd == fieldsDescriptor)
            {

                List<Field> fields = ((Product)instance).Fields;
                if (fields != null)
                {
                    foreach (Field field in fields)
                    {
                        FieldDescriptor fd = new FieldDescriptor(field);
                        pdList.Add(fd);
                    }

                }
            }
            else
            {
                pdList.Add(pd);
            }
        }
        return new PropertyDescriptorCollection(pdList.ToArray());
    }

    private class FieldDescriptor : SimplePropertyDescriptor
    {
        private Field privatefield;
        public Field field
        {
            get
            {
                return privatefield;
            }
            private set
            {
                privatefield = value;
            }
        }

        public FieldDescriptor(Field field) : base(field.GetType(), field.Name, field.Value.GetType())
        {
            // component type, property name, property type
            this.field = field;
        }

        public override object GetValue(object obj)
        {
            return field.Value;
        }

        public override void SetValue(object obj, object value)
        {
            field.Value = value;
        }

        public override bool IsReadOnly
        {
            get
            {
                return false;
            }
        }
    }

}

请注意,从字段中添加的属性不会组合在一起,而是会按字母顺序与其他未分类的属性一起显示 属性 (Symbol)。

要自定义对象的属性列表,您可以使用对象的自定义类型描述符。为此,您可以使用以下任一选项:

  • 你的class可以实现ICustomTypeDescriptor
  • 您的 class 可以派生自 CustomTypeDescriptor
  • 您可以创建一个新的 TypeDescriptor 并为您的 class 或对象实例注册它

例子

在此示例中,我创建了一个名为 MyClass 的 class,其中包含一个自定义属性列表。通过为 class 实现 ICustomTypeDescriptor,我将像 属性 网格中的普通属性一样显示 List<CustomProperty>

使用此机制时,自定义属性也可用于数据绑定。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
public class MyClass : ICustomTypeDescriptor
{
    public string OriginalProperty1 { get; set; }
    public string OriginalProperty2 { get; set; }
    public List<CustomProperty> CustomProperties { get; set; }

    #region ICustomTypeDescriptor
    public AttributeCollection GetAttributes() => TypeDescriptor.GetAttributes(this, true);
    public string GetClassName() => TypeDescriptor.GetClassName(this, true);
    public string GetComponentName() => TypeDescriptor.GetComponentName(this, true);
    public TypeConverter GetConverter() => TypeDescriptor.GetConverter(this, true);
    public EventDescriptor GetDefaultEvent() => TypeDescriptor.GetDefaultEvent(this, true);
    public PropertyDescriptor GetDefaultProperty() 
        => TypeDescriptor.GetDefaultProperty(this, true);
    public object GetEditor(Type editorBaseType) 
        => TypeDescriptor.GetEditor(this, editorBaseType, true);
    public EventDescriptorCollection GetEvents() => TypeDescriptor.GetEvents(this, true);
    public EventDescriptorCollection GetEvents(Attribute[] attributes) 
        => TypeDescriptor.GetEvents(this, attributes, true);
    public PropertyDescriptorCollection GetProperties() => GetProperties(new Attribute[] { });
    public PropertyDescriptorCollection GetProperties(Attribute[] attributes)
    {
        var properties = TypeDescriptor.GetProperties(this, attributes, true)
            .Cast<PropertyDescriptor>()
            .Where(p => p.Name != nameof(this.CustomProperties))
            .Select(p => TypeDescriptor.CreateProperty(this.GetType(), p,
                p.Attributes.Cast<Attribute>().ToArray())).ToList();
        properties.AddRange(CustomProperties.Select(x => new CustomPropertyDescriptor(this, x)));
        return new PropertyDescriptorCollection(properties.ToArray());
    }
    public object GetPropertyOwner(PropertyDescriptor pd) => this;
    #endregion
}

CustomProperty

这个class模拟一个自定义属性:

public class CustomProperty
{
    public string Name { get; set; }
    public object Value { get; set; }
    public string DisplayName { get; set; }
    public string Description { get; set; }
    public string Category { get; set; } = "Custom Properties";
}

CustomPropertyDescriptor

这个 class 是一个自定义的 属性 描述符,它描述了 CustomProperty:

public class CustomPropertyDescriptor : PropertyDescriptor
{
    object o;
    CustomProperty p;
    internal CustomPropertyDescriptor(object owner, CustomProperty property)
        : base(property.Name, null) { o = owner; p = property; }
    public override Type PropertyType => p.Value?.GetType() ?? typeof(object);
    public override void SetValue(object c, object v) => p.Value = v; 
    public override object GetValue(object c) => p.Value;
    public override bool IsReadOnly => false;
    public override Type ComponentType => o.GetType();
    public override bool CanResetValue(object c) => false;
    public override void ResetValue(object c) { }
    public override bool ShouldSerializeValue(object c) => false;
    public override string DisplayName => p.DisplayName ?? base.DisplayName;
    public override string Description => p.Description ?? base.Description;
    public override string Category => p.Category ?? base.Category;
}

用法

private void Form1_Load(object sender, EventArgs e)
{
    var o = new MyClass();
    o.CustomProperties = new List<CustomProperty>()
    {
        new CustomProperty
        {
            Name ="Property1",
            DisplayName ="First Property",
            Value ="Something",
            Description = "A custom description.",
        },
        new CustomProperty{ Name="Property2", Value= 100},
        new CustomProperty{ Name="Property3", Value= Color.Red},
    };
    propertyGrid1.SelectedObject = o;
}