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:
- If you expand the list while it is empty, and then add items, the grid is refreshed with the items expanded
- If you add items to the list, expand it, and then remove items (without collapsing), the grid is refreshed with the items expanded, and not throwing
ArgumentOutOfRangeException
because it is trying to show items that were deleted already - I want this whole thing for a configuration utility. Only the
PropertyGrid
should change the collections
IMPORTANT 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
TypeDescriptor.AddAttributes(typeof(IList), new TypeConverterAttribute(typeof(ExpandableObjectConverter)));
instead of your custom class? – HillCapacity
andCount
properties – Perfume