Search code examples
c#winformspropertygrid

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


I'm facing a display problem with PropertyGrid. I have object called Product that have a property Fields as a List< Field > nested objects.

I used custom TypeConverters and PropertyDescriptors like in many articles available online and I achieved this behavior: enter image description here

As expected, Fields were expanded nicely but I am trying to NOT EXPAND them into a separate sub-category, I need just Fields members on the same level as root members.

Now since Product is a bind-able object, I am trying to achieve this functionality using converters (ie. don't loop and just populate the PG or create a new object).

I tried a lot of things, Is it possible to trick a TypeConverter to do this? Here is the functional code:

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

Solution

  • In general you have the correct idea, but your implementation is wrong.

    If you want the Fields to show as properties of Product, Product must provide a PropertyDescriptor itself for each item in Fields. You can achieve this using a TypeConverter applied to the Product class.

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

    With:

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

    Do note that the properties added from Fields will not be grouped together and will display in alphabetical order along with the other un-categorized property (Symbol).