Reputation: 1669
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; }
}
}
}
Upvotes: 3
Views: 2059
Reputation: 125197
To customize list of properties for an object you can use custom type descriptor for the object. To do so, you can use either of the following options:
ICustomTypeDescriptor
CustomTypeDescriptor
TypeDescriptor
and register it for your class or the object instanceExample
Here in this example, I've created a class called MyClass
which has a list of custom properties. By implementing ICustomTypeDescriptor
for the class, I'll show List<CustomProperty>
like normal properties in the property grid.
When you use this mechanism, custom properties can also be used for data-binding.
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
This class simulates a custom property:
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
This class is a custom property descriptor which describes a 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;
}
Usage
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;
}
Upvotes: 3
Reputation: 11791
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
).
Upvotes: 1