I want to automatically show every IList
as expandable in my PropertyGrid
(By "expandable", I obviously mean that the items will be shown).
I don't want to use attributes on each list (Once again, I want it to work for EVERY IList
)
I tried to achive it by using a custom PropertyDescriptor
and an ExpandableObjectConverter
. It works, but after I delete items from the list, the PropertyGrid
is not being refreshed, still displaying the deleted items.
I tried to use ObservableCollection
along with raising OnComponentChanged
, and also RefreshProperties
attribute, but nothing worked.
This is my code:
public class ExpandableCollectionPropertyDescriptor : PropertyDescriptor
{
private IList _collection;
private readonly int _index = -1;
internal event EventHandler RefreshRequired;
public ExpandableCollectionPropertyDescriptor(IList coll, int idx) : base(GetDisplayName(coll, idx), null)
{
_collection = coll
_index = idx;
}
public override bool SupportsChangeEvents
{
get { return true; }
}
private static string GetDisplayName(IList list, int index)
{
return "[" + index + "] " + CSharpName(list[index].GetType());
}
private static string CSharpName(Type type)
{
var sb = new StringBuilder();
var name = type.Name;
if (!type.IsGenericType)
return name;
sb.Append(name.Substring(0, name.IndexOf('`')));
sb.Append("<");
sb.Append(string.Join(", ", type.GetGenericArguments()
.Select(CSharpName)));
sb.Append(">");
return sb.ToString();
}
public override AttributeCollection Attributes
{
get
{
return new AttributeCollection(null);
}
}
public override bool CanResetValue(object component)
{
return true;
}
public override Type ComponentType
{
get
{
return _collection.GetType();
}
}
public override object GetValue(object component)
{
OnRefreshRequired();
return _collection[_index];
}
public override bool IsReadOnly
{
get { return false; }
}
public override string Name
{
get { return _index.ToString(); }
}
public override Type PropertyType
{
get { return _collection[_index].GetType(); }
}
public override void ResetValue(object component)
{
}
public override bool ShouldSerializeValue(object component)
{
return true;
}
public override void SetValue(object component, object value)
{
_collection[_index] = value;
}
protected virtual void OnRefreshRequired()
{
var handler = RefreshRequired;
if (handler != null) handler(this, EventArgs.Empty);
}
}
.
internal class ExpandableCollectionConverter : ExpandableObjectConverter
{
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destType)
{
if (destType == typeof(string))
{
return "(Collection)";
}
return base.ConvertTo(context, culture, value, destType);
}
public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext context, object value, Attribute[] attributes)
{
IList collection = value as IList;
PropertyDescriptorCollection pds = new PropertyDescriptorCollection(null);
for (int i = 0; i < collection.Count; i++)
{
ExpandableCollectionPropertyDescriptor pd = new ExpandableCollectionPropertyDescriptor(collection, i);
pd.RefreshRequired += (sender, args) =>
{
var notifyValueGivenParentMethod = context.GetType().GetMethod("NotifyValueGivenParent", BindingFlags.NonPublic | BindingFlags.Instance);
notifyValueGivenParentMethod.Invoke(context, new object[] {context.Instance, 1});
};
pds.Add(pd);
}
// return the property descriptor Collection
return pds;
}
}
And I use it for all IList
s with the following line:
TypeDescriptor.AddAttributes(typeof (IList), new TypeConverterAttribute(typeof(ExpandableCollectionConverter)));
Some Clarifications
I want the grid to automatically update when I change the list. Refreshing when another property changes, does not help.
A solution that works, is a solution where:
ArgumentOutOfRangeException
because it is trying to show items that were deleted alreadyPropertyGrid
should change the collectionsIMPORTANT EDIT:
I did manage to make the expanded collections update with Reflection
, and calling NotifyValueGivenParent
method on the context
object when the PropertyDescriptor
GetValue method is called (when RefreshRequired
event is raised):
var notifyValueGivenParentMethod = context.GetType().GetMethod("NotifyValueGivenParent", BindingFlags.NonPublic | BindingFlags.Instance);
notifyValueGivenParentMethod.Invoke(context, new object[] {context.Instance, 1});
It works perfectly, except it causes the event to be raised infinite times, because calling NotifyValueGivenParent
causes a reload of the PropertyDescriptor
, and therfore, raising the event, and so on.
I tried to solve it by adding a simple flag that will prevent the reloading if it is already reloading, but for some reason NotifyValueGivenParent
behaves asynchronously, and therefore the reloading happens after the flag is turned off.
Maybe it is another direction to explore. The only problem is the recursion
There is no need for using ObservableCollection
. You can modify your descriptor class as follows:
public class ExpandableCollectionPropertyDescriptor : PropertyDescriptor
{
private IList collection;
private readonly int _index;
public ExpandableCollectionPropertyDescriptor(IList coll, int idx)
: base(GetDisplayName(coll, idx), null)
{
collection = coll;
_index = idx;
}
private static string GetDisplayName(IList list, int index)
{
return "[" + index + "] " + CSharpName(list[index].GetType());
}
private static string CSharpName(Type type)
{
var sb = new StringBuilder();
var name = type.Name;
if (!type.IsGenericType)
return name;
sb.Append(name.Substring(0, name.IndexOf('`')));
sb.Append("<");
sb.Append(string.Join(", ", type.GetGenericArguments()
.Select(CSharpName)));
sb.Append(">");
return sb.ToString();
}
public override bool CanResetValue(object component)
{
return true;
}
public override Type ComponentType
{
get { return this.collection.GetType(); }
}
public override object GetValue(object component)
{
return collection[_index];
}
public override bool IsReadOnly
{
get { return false; }
}
public override string Name
{
get { return _index.ToString(CultureInfo.InvariantCulture); }
}
public override Type PropertyType
{
get { return collection[_index].GetType(); }
}
public override void ResetValue(object component)
{
}
public override bool ShouldSerializeValue(object component)
{
return true;
}
public override void SetValue(object component, object value)
{
collection[_index] = value;
}
}
Instead of the ExpandableCollectionConverter
I would derive the CollectionConverter
class, so you can still use the ellipsis button to edit the collection in the old way (so you can add/remove items if the collection is not read-only):
public class ListConverter : CollectionConverter
{
public override bool GetPropertiesSupported(ITypeDescriptorContext context)
{
return true;
}
public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext context, object value, Attribute[] attributes)
{
IList list = value as IList;
if (list == null || list.Count == 0)
return base.GetProperties(context, value, attributes);
var items = new PropertyDescriptorCollection(null);
for (int i = 0; i < list.Count; i++)
{
object item = list[i];
items.Add(new ExpandableCollectionPropertyDescriptor(list, i));
}
return items;
}
}
And I would use this ListConverter
on the properties where I want to see expandable list. Of course, you can register the type converter generally as you do in your example, but that overrides everything, which might not be overall intended.
public class MyClass
{
[TypeConverter(typeof(ListConverter))]
public List<int> List { get; set; }
public MyClass()
{
List = new List<int>();
}
[RefreshProperties(RefreshProperties.All)]
[Description("Change this property to regenerate the List")]
public int Count
{
get { return List.Count; }
set { List = Enumerable.Range(1, value).ToList(); }
}
}
Important: The RefreshProperties
attribute should be defined for the properties that change other properties. In this example, changing the Count
replaces the whole list.
Using it as propertyGrid1.SelectedObject = new MyClass();
produces the following result:
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With