【问题标题】:PropertyGrid expandable collectionPropertyGrid 可扩展集合
【发布时间】:2015-12-11 11:59:38
【问题描述】:

我想在我的PropertyGrid 中自动将每个IList 显示为可扩展(通过“可扩展”,我显然是指将显示这些项目)。 我不想在每个列表上都使用属性(再一次,我希望它适用于每个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;
    }
}

我将它用于所有ILists,并带有以下行:

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

一些说明

我希望网格在我更改列表时自动更新。当另一个属性发生变化时刷新,没有帮助。

有效的解决方案是:

  1. 如果您在列表为空时展开列表,然后添加项目,则网格会随着展开的项目刷新
  2. 如果您将项目添加到列表中,展开它,然后删除项目(不折叠),网格会随着项目展开刷新,而不是抛出 ArgumentOutOfRangeException,因为它试图显示已删除的项目
  3. 我想要一个配置实用程序的全部内容。只有 PropertyGrid 应该更改集合

重要编辑:

我确实设法使用Reflection 更新扩展集合,并在调用PropertyDescriptor GetValue 方法时(引发RefreshRequired 事件时)在context 对象上调用NotifyValueGivenParent 方法:

var notifyValueGivenParentMethod = context.GetType().GetMethod("NotifyValueGivenParent", BindingFlags.NonPublic | BindingFlags.Instance);
notifyValueGivenParentMethod.Invoke(context, new object[] {context.Instance, 1});

它工作得很好,除了它会导致事件无限次引发,因为调用NotifyValueGivenParent 会导致重新加载PropertyDescriptor,然后引发事件等等。

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

【问题讨论】:

  • 为什么不直接调用TypeDescriptor.AddAttributes(typeof(IList), new TypeConverterAttribute(typeof(ExpandableObjectConverter))); 而不是自定义类?
  • @SimonMourier 因为那时我看不到集合中的项目,但 CapacityCount 属性
  • 此要求未出现在您的问题中。顺便说一句,它适用于我的 ArrayList 类型的属性。我想这取决于 SelectedObject 中的类。你应该用所有相关的代码和完整的问题来填写你的问题。
  • @SimonMourier 我虽然很明显我的意图是展示这些项目。 (我编辑了问题以说明这一点)
  • 嗨,你可以给 ownerGrid 属性提供反射,而不是 notifyParent 方法,史蒂夫梅德利在这里建议link。在这里工作得很好。

标签: c# .net propertygrid typeconverter propertydescriptor


【解决方案1】:

没有必要使用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;
    }
}

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

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(); 会产生以下结果:

【讨论】:

  • 这不是我想要的。我不希望它在其他属性刷新时刷新。我希望它在列表更改时刷新。我将项目添加到列表中,展开它,添加更多项目,但项目没有更新
  • 谢谢!很好的例子——正是我需要的。
  • @AndroidJoker 有趣的是,当您在此示例中从 List 属性中删除 set 访问器时,项目列表实际上会在您编辑集合时更新。我想,由于列表只能静态创建一次,您可以考虑删除 set 访问器。
【解决方案2】:

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

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

你仍然可以做的是一个特别丑陋的hack

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 的实现者必须是可序列化的
  • 可笑的性能开销
  • 虽然使用弱引用可能会减少内存泄漏,但如果要编辑的对象的生命周期比编辑器表单长,则无济于事,因为它们将保留在静态集合中

【讨论】:

  • 我不希望它同时更新。我想要它完全用于配置。问题是,当您展开列表然后更改它时,从PropertyGrid 展开的列表不会更新。我添加了这个和原始问题的可能解决方案
【解决方案3】:

把它们放在一起,这是可行的:

这是一个包含列表的类,我们将在属性网格中放置一个实例。另外为了演示复杂对象列表的用法,我有 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);
    }
}

现在用法产生:

执行各种操作会产生:

您可以调整它以按照您的喜好操作。

【讨论】:

    猜你喜欢
    • 2016-07-17
    • 1970-01-01
    • 2014-10-01
    • 2015-06-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多