Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

PropertyGrid expandable collection

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 ILists 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:

  1. If you expand the list while it is empty, and then add items, the grid is refreshed with the items expanded
  2. 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
  3. 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

like image 308
RE6 Avatar asked Sep 15 '15 09:09

RE6


1 Answers

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:

enter image description here

like image 151
György Kőszeg Avatar answered Oct 07 '22 20:10

György Kőszeg