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:
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; }
}
}
}
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
).