PropertyGrid可扩展集合

时间:2015-09-15 09:30:07

标签: c# .net propertygrid typeconverter propertydescriptor

我希望在我的IList中自动将每个PropertyGrid显示为可扩展的(通过"可扩展",我显然意味着将显示这些项目)。 我不想在每个列表中使用属性(再次,我希望它适用于每个IList

我尝试使用自定义PropertyDescriptorExpandableObjectConverter来实现它。它有效,但在我从列表中删除项目后,PropertyGrid没有刷新,仍然显示已删除的项目。

我尝试使用ObservableCollection同时提升OnComponentChangedRefreshProperties属性,但没有任何效果。

这是我的代码:

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;
    }
}

我使用以下行代表所有IList

TypeDescriptor.AddAttributes(typeof (IList), new TypeConverterAttribute(typeof(ExpandableCollectionConverter)));

一些澄清

我希望在更改列表时网格自动更新。当另一个属性发生变化时刷新也无济于事。

有效的解决方案是:

  1. 如果您在列表为空时展开列表,然后添加项目,则会使用展开的项目刷新网格
  2. 如果您向列表中添加项目,展开它,然后删除项目(不折叠),网格将刷新并展开项目,而不是抛出ArgumentOutOfRangeException,因为它正在尝试显示已删除的项目已经
  3. 我想要一个配置实用程序。只有PropertyGrid才能更改集合
  4. 重要编辑:

    我确实设法使用Reflection更新扩展集合,并在调用context GetValue方法时调用PropertyDescriptor对象上的NotifyValueGivenParent方法(当{{ 1}}事件被提出):

    RefreshRequired

    它完美地工作,除非它导致事件被无限次提升,因为调用var notifyValueGivenParentMethod = context.GetType().GetMethod("NotifyValueGivenParent", BindingFlags.NonPublic | BindingFlags.Instance); notifyValueGivenParentMethod.Invoke(context, new object[] {context.Instance, 1}); 会导致重新加载NotifyValueGivenParent,因此,引发事件,等等。

    我尝试通过添加一个简单的标志来解决它,如果它已经重新加载将阻止重新加载,但由于某种原因,PropertyDescriptor表现异步,因此重新加载在标志关闭后发生。 也许这是另一个探索的方向。唯一的问题是递归

3 个答案:

答案 0 :(得分:3)

无需使用ObservableCollection。您可以按如下方式修改描述符类:

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;
    }
}

而不是ExpandableCollectionConverter我会派生CollectionConverter类,所以您仍然可以使用省略号按钮以旧方式编辑集合(因此,如果集合是,则可以添加/删除项目不是只读的):

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;
    }
}

我会在我希望看到可扩展列表的属性上使用此ListConverter。当然,您可以像在示例中一样注册类型转换器,但是这会覆盖所有内容,这可能不是总体目标。

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(); }
    }
}

重要:应为更改其他属性的属性定义RefreshProperties属性。在此示例中,更改Count将替换整个列表。

将其用作propertyGrid1.SelectedObject = new MyClass();会产生以下结果:

enter image description here

答案 1 :(得分:3)

  

当其他属性刷新时,我不希望它刷新。我希望它在列表更改时刷新。我将项目添加到列表,展开,添加更多项目,但项目未更新

这是PropertyGrid的典型误用。它用于配置组件,而不是用于反映外部源即时更改的并发更改。即使将IList包装到ObservableCollection也无济于事,因为它仅由您的描述符使用,而外部源直接操作基础IList实例。

你仍然可以做的是特别难看的黑客

public class ExpandableCollectionPropertyDescriptor : PropertyDescriptor
{
    // Subscribe to this event from the form with the property grid
    public static event EventHandler CollectionChanged;

    // Tuple elements: The owner of the list, the list, the serialized content of the list
    // The reference to the owner is a WeakReference because you cannot tell the
    // PropertyDescriptor that you finished the editing and the collection
    // should be removed from the list.
    // Remark: The references here may survive the property grid's life
    private static List<Tuple<WeakReference, IList, byte[]>> collections;
    private static Timer timer;

    public ExpandableCollectionPropertyDescriptor(ITypeDescriptorContext context, IList collection, ...)
    {
        AddReference(context.Instance, collection);
        // ...
    }

    private static void AddReference(object owner, IList collection)
    {
        // TODO:
        // - serialize the collection into a byte array (BinaryFormatter) and add it to the collections list
        // - if this is the first element, initialize the timer
    }

    private static void Timer_Elapsed(object sender, ElapsedEventArgs e)
    {
        // TODO: Cycle through the collections elements
        // - If WeakReference is not alive, remove the item from the list
        // - Serialize the list again and compare the result to the last serialized content
        // - If there a is difference:
        //   - Update the serialized content
        //   - Invoke the CollectionChanged event. The sender is the owner (WeakReference.Target).
    }
}

现在您可以像这样使用它:

public class Form1 : Form
{
    MyObject myObject = new MyObject();

    public MyForm()
    {
        InitializeComponent();
        ExpandableCollectionPropertyDescriptor.CollectionChanged += CollectionChanged();
        propertyGrid.SelectedObject = myObject;
    }

    private void CollectionChanged(object sender, EventArgs e)
    {
        if (sender == myObject)
            propertyGrid.SelectedObject = myObject;
    }
}

但说实话,我根本不会使用它。它有严重的缺陷:

  • 如果PropertyGrid更改了集合元素,但计时器尚未更新上次外部更改,该怎么办?
  • IList的实施者必须是可序列化的
  • 可笑的性能开销
  • 虽然使用弱引用可能会减少内存泄漏,但如果要编辑的对象的生命周期比编辑器表单更长,则无效,因为它们将保留在静态集合中

答案 2 :(得分:0)

将所有内容放在一起,可以起作用:

这是带有列表的类,我们将在其属性网格中放置一个实例。为了演示复杂对象列表的用法,我还有NameAgePair类。

public class SettingsStructure
{
    public SettingsStructure()
    {
        //To programmatically add this to properties that implement ILIST for the naming of the edited node and child items:
        //[TypeConverter(typeof(ListConverter))]
        TypeDescriptor.AddAttributes(typeof(IList), new TypeConverterAttribute(typeof(ListConverter)));

        //To programmatically add this to properties that implement ILIST for the refresh and expansion of the edited node
        //[Editor(typeof(CollectionEditorBase), typeof(System.Drawing.Design.UITypeEditor))]
        TypeDescriptor.AddAttributes(typeof(IList), new EditorAttribute(typeof(CollectionEditorBase), typeof(UITypeEditor)));
    }

    public List<string> ListOfStrings { get; set; } = new List<string>();
    public List<string> AnotherListOfStrings { get; set; } = new List<string>();
    public List<int> ListOfInts { get; set; } = new List<int>();
    public List<NameAgePair> ListOfNameAgePairs { get; set; } = new List<NameAgePair>();
}

public class NameAgePair
{
    public string Name { get; set; } = "";
    public int Age { get; set; } = 0;

    public override string ToString()
    {
        return $"{Name} ({Age})";
    }
}

这里是ListConverter类,用于处理子节点的创建。

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;
    }

    public override object ConvertTo(ITypeDescriptorContext pContext, CultureInfo pCulture, object value, Type pDestinationType)
    {
        if (pDestinationType == typeof(string))
        {
            IList v = value as IList;
            int iCount = (v == null) ? 0 : v.Count;
            return $"({iCount} Items)";
        }
        return base.ConvertTo(pContext, pCulture, value, pDestinationType);
    }
}

这是各个项目的ExpandableCollectionPropertyDescriptor类。

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;
    }
}

然后是CollectionEditorBase类,用于在关闭集合编辑器后刷新属性网格。

public class CollectionEditorBase : CollectionEditor
{
    protected PropertyGrid _PropertyGrid;
    private bool _ExpandedBefore;
    private int _CountBefore;

    public CollectionEditorBase(Type type) : base(type) { }

    public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value)
    {
        //Record entry state of property grid item
        GridItem giThis = (GridItem)provider;
        _ExpandedBefore = giThis.Expanded;
        _CountBefore = (giThis.Value as IList).Count;

        //Get the grid so later we can refresh it on close of editor
        PropertyInfo piOwnerGrid = provider.GetType().GetProperty("OwnerGrid", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
        _PropertyGrid = (PropertyGrid)piOwnerGrid.GetValue(provider);

        //Edit the collection
        return base.EditValue(context, provider, value);
    }

    protected override CollectionForm CreateCollectionForm()
    {
        CollectionForm cf = base.CreateCollectionForm();
        cf.FormClosing += delegate (object sender, FormClosingEventArgs e)
        {
            _PropertyGrid.Refresh();
            //Because nothing changes which grid item is the selected one, expand as desired
            if (_ExpandedBefore || _CountBefore == 0) _PropertyGrid.SelectedGridItem.Expanded = true; 
        };
        return cf;
    }

    protected override object CreateInstance(Type itemType)
    {
        //Fixes the "Constructor on type 'System.String' not found." when it is an empty list of strings
        if (itemType == typeof(string)) return string.Empty;
        else return Activator.CreateInstance(itemType);
    }
}

现在用法产生:

List nodes expanded

执行各种操作会产生:

enter image description here

您可以调整它的操作使其自如。