【问题标题】:Xceed WPF propertyGrid show item for expanded collectionXceed WPF propertyGrid 显示扩展集合的项目
【发布时间】:2016-07-17 03:21:51
【问题描述】:

如何在 Xceed WPF PropertyGrid 中显示自定义对象的ObservableCollection<>,其中每个列表项都可以展开以显示自定义对象属性。 (即:

----PropertyGrid-----

核心类

  • (+/-) ObservableCollection

    • (+/-) CustomClass.Object1

      • 属性 1:值

      • 属性 2:值

      • 属性N:值

    • (+/-) CustomClass.Object2

      • 属性 1:值

      • 属性 2:值

      • 属性N:值

如果我在 ObservableCollection<> 上使用 [ExpandableObject],它只会显示 Counts 属性。

编辑:(添加代码)

MainWindow.xaml:

<Window x:Class="PropGridExample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:xctk="http://schemas.xceed.com/wpf/xaml/toolkit"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:PropGridExample"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <xctk:PropertyGrid x:Name="PropertyGrid" SelectedObject="{Binding BindingItem}"></xctk:PropertyGrid>
    </Grid>
</Window>

MainWindow.xaml.cs

public partial class MainWindow : Window
{
    public MainWindow()
    {
        MainWindowViewModel mwvm = new MainWindowViewModel();
        this.DataContext = mwvm;
        InitializeComponent();
    }
}

MainWindowViewModel.cs

public class MainWindowViewModel
{
    public Item BindingItem { get; set; }

    public MainWindowViewModel()
    {
        BindingItem = new Item();
    }

    public class Item
    {
        public int ID { get; set; }
        [ExpandableObject()]
        public ObservableCollection<CustomClass> Classes { get; set; }

        public Item()
        {
            ID = 1;
            Classes = new ObservableCollection<CustomClass>();
            Classes.Add(new CustomClass() { Name = "CustomFoo" });
        }
    }

    public class CustomClass
    {
        public string Name { get; set; }
        [ExpandableObject()]
        public ObservableCollection<type> Types { get; set; }

        public CustomClass()
        {
            Types = new ObservableCollection<type>();
            Types.Add(new type() { name = "foo", value = "bar" });
            Types.Add(new type() { name = "bar", value = "foo" });
        }
    }

    public class type
    {
        public string name { get; set; }
        public string value { get; set; }
    }
}

【问题讨论】:

  • @jstreet 这是不可能的,因为我还想显示自定义类的其他属性。我还希望能够更改 collectionitems 属性的值。
  • 我不能只使用ToArray() - 方法,因为我使用带有数据绑定的MVVM。 SelectedObject 也会在运行时更改。但是如果你希望我可以发布一个示例应用程序,真正的代码是巨大的。
  • 是的,我想“内联”扩展它,所以如果我扩展集合,我会看到集合中的所有项目。我不想使用集合编辑器
  • @VRage 我相信这可能无法使用 PropertyGrid。它必须是专门的 PropertyGrid 还是 类似于 属性网格的东西?
  • @MackieChan 可以使用 winform 属性网格:codeproject.com/Articles/4448/…。所以我很难相信这在 wpf 版本中是不可能的。但不,它不一定是 propertyGrid,我还想使用树视图来编辑每个项目的值。

标签: c# wpf propertygrid


【解决方案1】:

请注意,这个想法大部分来自CodeProject project you linked to。本文为您提供了大部分内容,但正如您所注意到的,它不会扩展 WPF PropertyGrid 集合中的每个项目。为此,每个“项目”都需要有一个ExpandableObjectAttribute

为了让以后的 StackOverflow 读者理解,我从头说起。

从头开始

所以,从这个例子开始:

public class MainWindowViewModel
{
  /// <summary> This the object we want to be able to edit in the data grid. </summary>
  public ComplexObject BindingComplexObject { get; set; }

  public MainWindowViewModel()
  {
    BindingComplexObject = new ComplexObject();
  }
}

public class ComplexObject
{
  public int ID { get; set; }

  public ObservableCollection<ComplexSubObject> Classes { get; set; }

  public ComplexObject()
  {
    ID = 1;
    Classes = new ObservableCollection<ComplexSubObject>();
    Classes.Add(new ComplexSubObject() { Name = "CustomFoo" });
    Classes.Add(new ComplexSubObject() { Name = "My Other Foo" });
  }
}

public class ComplexSubObject
{
  public string Name { get; set; }

  public ObservableCollection<SimpleValues> Types { get; set; }

  public ComplexSubObject()
  {
    Types = new ObservableCollection<SimpleValues>();
    Types.Add(new SimpleValues() { name = "foo", value = "bar" });
    Types.Add(new SimpleValues() { name = "bar", value = "foo" });
  }
}

public class SimpleValues
{
  public string name { get; set; }
  public string value { get; set; }
}

为了使 WPF PropertyGrid 能够编辑 ObservableCollection 中的每个项目,我们需要为集合提供类型描述符,该描述符将项目作为该集合的“属性”返回,以便可以对其进行编辑。因为我们不能静态确定集合中的项目(因为每个集合有不同数量的元素),这意味着集合本身必须是TypeDescriptor,这意味着实现ICustomTypeDescriptor

(请注意,只有GetProperties 对我们的目的很重要,其余的只是委托给TypeDescriptor):

public class ExpandableObservableCollection<T> : ObservableCollection<T>,
                                                 ICustomTypeDescriptor
{
  PropertyDescriptorCollection ICustomTypeDescriptor.GetProperties()
  {
    // Create a collection object to hold property descriptors
    PropertyDescriptorCollection pds = new PropertyDescriptorCollection(null);

    for (int i = 0; i < Count; i++)
    {
      pds.Add(new ItemPropertyDescriptor<T>(this, i));
    }

    return pds;
  }

  #region Use default TypeDescriptor stuff

  AttributeCollection ICustomTypeDescriptor.GetAttributes()
  {
    return TypeDescriptor.GetAttributes(this, noCustomTypeDesc: true);
  }

  string ICustomTypeDescriptor.GetClassName()
  {
    return TypeDescriptor.GetClassName(this, noCustomTypeDesc: true);
  }

  string ICustomTypeDescriptor.GetComponentName()
  {
    return TypeDescriptor.GetComponentName(this, noCustomTypeDesc: true);
  }

  TypeConverter ICustomTypeDescriptor.GetConverter()
  {
    return TypeDescriptor.GetConverter(this, noCustomTypeDesc: true);
  }

  EventDescriptor ICustomTypeDescriptor.GetDefaultEvent()
  {
    return TypeDescriptor.GetDefaultEvent(this, noCustomTypeDesc: true);
  }

  PropertyDescriptor ICustomTypeDescriptor.GetDefaultProperty()
  {
    return TypeDescriptor.GetDefaultProperty(this, noCustomTypeDesc: true);
  }

  object ICustomTypeDescriptor.GetEditor(Type editorBaseType)
  {
    return TypeDescriptor.GetEditor(this, editorBaseType, noCustomTypeDesc: true);
  }

  EventDescriptorCollection ICustomTypeDescriptor.GetEvents()
  {
    return TypeDescriptor.GetEvents(this, noCustomTypeDesc: true);
  }

  EventDescriptorCollection ICustomTypeDescriptor.GetEvents(Attribute[] attributes)
  {
    return TypeDescriptor.GetEvents(this, attributes, noCustomTypeDesc: true);
  }

  PropertyDescriptorCollection ICustomTypeDescriptor.GetProperties(Attribute[] attributes)
  {
    return TypeDescriptor.GetProperties(this, attributes, noCustomTypeDesc: true);
  }

  object ICustomTypeDescriptor.GetPropertyOwner(PropertyDescriptor pd)
  {
    return this;
  }

  #endregion
}

此外,我们需要ItemPropertyDescriptor 的实现,我在这里提供:

public class ItemPropertyDescriptor<T> : PropertyDescriptor
{
  private readonly ObservableCollection<T> _owner;
  private readonly int _index;

  public ItemPropertyDescriptor(ObservableCollection<T> owner, int index)
    : base("#" + index, null)
  {
    _owner = owner;
    _index = index;
  }

  public override AttributeCollection Attributes
  {
    get
    {
      var attributes = TypeDescriptor.GetAttributes(GetValue(null), false);
      if (!attributes.OfType<ExpandableObjectAttribute>().Any())
      {
        // copy all the attributes plus an extra one (the
        // ExpandableObjectAttribute)
        // this ensures that even if the type of the object itself doesn't have the
        // ExpandableObjectAttribute, it will still be expandable. 
        var newAttributes = new Attribute[attributes.Count + 1];
        attributes.CopyTo(newAttributes, newAttributes.Length - 1);
        newAttributes[newAttributes.Length - 1] = new ExpandableObjectAttribute();

        // overwrite the array
        attributes = new AttributeCollection(newAttributes);
      }

      return attributes;
    }
  }

  public override bool CanResetValue(object component)
  {
    return false;
  }

  public override object GetValue(object component)
  {
    return Value;
  }

  private T Value
    => _owner[_index];

  public override void ResetValue(object component)
  {
    throw new NotImplementedException();
  }

  public override void SetValue(object component, object value)
  {
    _owner[_index] = (T)value;
  }

  public override bool ShouldSerializeValue(object component)
  {
    return false;
  }

  public override Type ComponentType
    => _owner.GetType();

  public override bool IsReadOnly
    => false;

  public override Type PropertyType
    => Value?.GetType();
}

其中大部分只是设置合理的默认值,您可以对其进行调整以满足您的需求。

需要注意的一点是,您可能会以不同的方式实现Attributes 属性,具体取决于您的用例。如果您不执行“如果不存在则将其添加到属性集合中”,那么您需要将属性添加到要扩展的类/类型中;如果您确实保留了该代码,那么无论类/类型是否具有该属性,您都可以扩展集合中的每个项目。

然后就变成了使用ExpandableObservableCollection 代替ObservableCollection 的问题。这种情况很糟糕,因为这意味着您的 ViewModel 里面有 view-stuff-ish 的东西,但是 ¯\_(ツ)_/¯

此外,您需要将ExpandableObjectAttribute 添加到ExpandableObservableCollection 的每个属性中。

代码转储

如果您是在家里跟随,您可以使用以下对话框代码来运行示例:

<Window x:Class="WpfDemo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfDemo"
        xmlns:xctk="http://schemas.xceed.com/wpf/xaml/toolkit"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
      <xctk:PropertyGrid x:Name="It" />
    </Grid>
</Window>

-

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;

namespace WpfDemo
{
  /// <summary>
  /// Interaction logic for MainWindow.xaml
  /// </summary>
  public partial class MainWindow : Window
  {
    public MainWindow()
    {
      InitializeComponent();

      It.SelectedObject = new MainWindowViewModel().BindingComplexObject;
    }
  }
}

这是完整的 ViewModel 实现:

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;
using Xceed.Wpf.Toolkit.PropertyGrid.Attributes;

namespace WpfDemo
{
  public class MainWindowViewModel
  {
    /// <summary> This the object we want to be able to edit in the data grid. </summary>
    public ComplexObject BindingComplexObject { get; set; }

    public MainWindowViewModel()
    {
      BindingComplexObject = new ComplexObject();
    }
  }

  [ExpandableObject]
  public class ComplexObject
  {
    public int ID { get; set; }

    [ExpandableObject]
    public ExpandableObservableCollection<ComplexSubObject> Classes { get; set; }

    public ComplexObject()
    {
      ID = 1;
      Classes = new ExpandableObservableCollection<ComplexSubObject>();
      Classes.Add(new ComplexSubObject() { Name = "CustomFoo" });
      Classes.Add(new ComplexSubObject() { Name = "My Other Foo" });
    }
  }

  [ExpandableObject]
  public class ComplexSubObject
  {
    public string Name { get; set; }

    [ExpandableObject]
    public ExpandableObservableCollection<SimpleValues> Types { get; set; }

    public ComplexSubObject()
    {
      Types = new ExpandableObservableCollection<SimpleValues>();
      Types.Add(new SimpleValues() { name = "foo", value = "bar" });
      Types.Add(new SimpleValues() { name = "bar", value = "foo" });
    }
  }

  public class SimpleValues
  {
    public string name { get; set; }
    public string value { get; set; }
  }

  public class ExpandableObservableCollection<T> : ObservableCollection<T>,
                                                   ICustomTypeDescriptor
  {
    PropertyDescriptorCollection ICustomTypeDescriptor.GetProperties()
    {
      // Create a collection object to hold property descriptors
      PropertyDescriptorCollection pds = new PropertyDescriptorCollection(null);

      for (int i = 0; i < Count; i++)
      {
        pds.Add(new ItemPropertyDescriptor<T>(this, i));
      }

      return pds;
    }

    #region Use default TypeDescriptor stuff

    AttributeCollection ICustomTypeDescriptor.GetAttributes()
    {
      return TypeDescriptor.GetAttributes(this, noCustomTypeDesc: true);
    }

    string ICustomTypeDescriptor.GetClassName()
    {
      return TypeDescriptor.GetClassName(this, noCustomTypeDesc: true);
    }

    string ICustomTypeDescriptor.GetComponentName()
    {
      return TypeDescriptor.GetComponentName(this, noCustomTypeDesc: true);
    }

    TypeConverter ICustomTypeDescriptor.GetConverter()
    {
      return TypeDescriptor.GetConverter(this, noCustomTypeDesc: true);
    }

    EventDescriptor ICustomTypeDescriptor.GetDefaultEvent()
    {
      return TypeDescriptor.GetDefaultEvent(this, noCustomTypeDesc: true);
    }

    PropertyDescriptor ICustomTypeDescriptor.GetDefaultProperty()
    {
      return TypeDescriptor.GetDefaultProperty(this, noCustomTypeDesc: true);
    }

    object ICustomTypeDescriptor.GetEditor(Type editorBaseType)
    {
      return TypeDescriptor.GetEditor(this, editorBaseType, noCustomTypeDesc: true);
    }

    EventDescriptorCollection ICustomTypeDescriptor.GetEvents()
    {
      return TypeDescriptor.GetEvents(this, noCustomTypeDesc: true);
    }

    EventDescriptorCollection ICustomTypeDescriptor.GetEvents(Attribute[] attributes)
    {
      return TypeDescriptor.GetEvents(this, attributes, noCustomTypeDesc: true);
    }

    PropertyDescriptorCollection ICustomTypeDescriptor.GetProperties(Attribute[] attributes)
    {
      return TypeDescriptor.GetProperties(this, attributes, noCustomTypeDesc: true);
    }

    object ICustomTypeDescriptor.GetPropertyOwner(PropertyDescriptor pd)
    {
      return this;
    }

    #endregion
  }

  public class ItemPropertyDescriptor<T> : PropertyDescriptor
  {
    private readonly ObservableCollection<T> _owner;
    private readonly int _index;

    public ItemPropertyDescriptor(ObservableCollection<T> owner, int index)
      : base("#" + index, null)
    {
      _owner = owner;
      _index = index;
    }

    public override AttributeCollection Attributes
    {
      get
      {
        var attributes = TypeDescriptor.GetAttributes(GetValue(null), false);


        if (!attributes.OfType<ExpandableObjectAttribute>().Any())
        {
          // copy all the attributes plus an extra one (the
          // ExpandableObjectAttribute)
          // this ensures that even if the type of the object itself doesn't have the
          // ExpandableObjectAttribute, it will still be expandable. 
          var newAttributes = new Attribute[attributes.Count + 1];
          attributes.CopyTo(newAttributes, newAttributes.Length - 1);
          newAttributes[newAttributes.Length - 1] = new ExpandableObjectAttribute();

          // overwrite the original
          attributes = new AttributeCollection(newAttributes);
        }

        return attributes;
      }
    }

    public override bool CanResetValue(object component)
    {
      return false;
    }

    public override object GetValue(object component)
    {
      return Value;
    }

    private T Value
      => _owner[_index];

    public override void ResetValue(object component)
    {
      throw new NotImplementedException();
    }

    public override void SetValue(object component, object value)
    {
      _owner[_index] = (T)value;
    }

    public override bool ShouldSerializeValue(object component)
    {
      return false;
    }

    public override Type ComponentType
      => _owner.GetType();

    public override bool IsReadOnly
      => false;

    public override Type PropertyType
      => Value?.GetType();
  }
}

【讨论】:

  • 昨天我读到了一位开发者写的“这是不可能的”的帖子:wpftoolkit.codeplex.com/discussions/653034。但是,您却在这里,把它搞砸了:D。非常感谢您的出色工作。
  • 另请注意:确保您有一个可用于您正在扩展的类型的默认(空)构造函数,否则如果您从集合控件中取消,您会发现它们都被删除了!如果取消,似乎下面的项目会被删除和替换,但是,这里必须使用反射,因此需要默认构造函数。
  • 干得好@FriendlyGuy。我冒昧地将您的代码变成了一个 nuget 包 (nuget.org/packages/Extended.Wpf.Toolkit.PropertyGrid.Collection),并希望最终将它包含在 wpftoolkit 本身中。 (github.com/xceedsoftware/wpftoolkit/issues/1479)
【解决方案2】:

MackieChan 提供了主要线索...

无需从 ICustomTypeDescriptor 继承,因为可以使用类型转换器实现类似的结果。

首先创建一个可扩展的对象类型转换器并覆盖 GetProperties 方法。例如,如果您希望维护通用 IList 类型的索引顺序:

using Xceed.Wpf.Toolkit.PropertyGrid.Attributes;
using System.ComponentModel;

public class MyExpandableIListConverter<T> : ExpandableObjectConverter
{
    public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext context, object value, Attribute[] attributes)
    {
        if (value is IList<T> list)
        {
            PropertyDescriptorCollection propDescriptions = new PropertyDescriptorCollection(null);
            IEnumerator enumerator = list.GetEnumerator();
            int counter = -1;
            while (enumerator.MoveNext())
            {
                counter++;
                propDescriptions.Add(new ListItemPropertyDescriptor<T>(list, counter));

            }
            return propDescriptions;
        }
        else
        {
            return base.GetProperties(context, value, attributes);
        }
    }        
}

ListItemPropertyDescriptor 定义如下:

using Xceed.Wpf.Toolkit.PropertyGrid.Attributes;
using System.ComponentModel;

public class ListItemPropertyDescriptor<T> : PropertyDescriptor
{
    private readonly IList<T> owner;
    private readonly int index;

    public ListItemPropertyDescriptor(IList<T> owner, int index) : base($"[{index}]", null)
    {
        this.owner = owner;
        this.index = index;

    }

    public override AttributeCollection Attributes
    {
        get
        {
            var attributes = TypeDescriptor.GetAttributes(GetValue(null), false);
            //If the Xceed expandable object attribute is not applied then apply it
            if (!attributes.OfType<ExpandableObjectAttribute>().Any())
            {
                attributes = AddAttribute(new ExpandableObjectAttribute(), attributes);
            }

            //set the xceed order attribute
            attributes = AddAttribute(new PropertyOrderAttribute(index), attributes);

            return attributes;
        }
    }
    private AttributeCollection AddAttribute(Attribute newAttribute, AttributeCollection oldAttributes)
    {
        Attribute[] newAttributes = new Attribute[oldAttributes.Count + 1];
        oldAttributes.CopyTo(newAttributes, 1);
        newAttributes[0] = newAttribute;

        return new AttributeCollection(newAttributes);
    }

    public override bool CanResetValue(object component)
    {
        return false;
    }

    public override object GetValue(object component)
    {
        return Value;
    }

    private T Value
      => owner[index];

    public override void ResetValue(object component)
    {
        throw new NotImplementedException();
    }

    public override void SetValue(object component, object value)
    {
        owner[index] = (T)value;
    }

    public override bool ShouldSerializeValue(object component)
    {
        return false;
    }

    public override Type ComponentType
      => owner.GetType();

    public override bool IsReadOnly
      => false;

    public override Type PropertyType
      => Value?.GetType();

}

然后您需要使用 ExpandableObjectAttribute 和 TypeConverterAttribute 动态装饰您希望在属性网格中显示的类型。我创建了一个“装饰管理器”来实现这一点,如下所示。

using System.ComponentModel;
using Xceed.Wpf.Toolkit.PropertyGrid.Attributes;

public static class TypeDecorationManager
{
    public static void AddExpandableObjectConverter(Type T)
    {
        TypeDescriptor.AddAttributes(T, new TypeConverterAttribute(typeof(ExpandableObjectConverter)));
        TypeDescriptor.AddAttributes(T, new ExpandableObjectAttribute());
    }
    public static void AddExpandableIListConverter<I>(Type T)
    {
        TypeDescriptor.AddAttributes(T, new TypeConverterAttribute(typeof(MyExpandableIListConverter<I>)));
        TypeDescriptor.AddAttributes(T, new ExpandableObjectAttribute());
    }
}

为您希望在属性网格中展开的任何类型调用 AddExpandableObjectConverter,为您希望在网格中展开的任何 IList 类型调用 AddExpandableILListConverter。

例如,如果您有一个带有 IList 等属性的曲线对象,那么所有属性和列表项都可以按如下方式展开:

TypeDecorationManager.AddExpandableObjectConverter(typeof(Curve));
TypeDecorationManager.AddExpandableObjectConverter(typeof(CurvePoint));

AddCoreExpandableListConverter<CurvePoint>(typeof(IList<CurvePoint>));

【讨论】:

  • 请注意,'"["+ index+"]"' 部分很重要,因为扩展工具包 PropertyGrid 将使用它来尝试绑定到列表的模拟属性。其他任何事情都会导致 System.Windows.Data 绑定错误。
  • 另请注意,如果目标集合具有this[] 的重载(例如:this[string]),则必须在重载之前声明this[int]。如果this[int] 是基类所固有的并且重载(this[string])不是,那么this[int] 将不会被视为在重载之前声明,因此您必须创建一个新的this[int] 来转发base[int]
  • 如果你无法控制你的模型类,这就是你要走的路。
  • 我看到另一个答案使用"#" + index(井号而不是括号)而不是使用"[" + index + "]" 作为属性名称(为了清楚起见,我改为$"[{index}]")。解决上述@Jens 评论中提到的索引器问题?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-12-11
  • 2023-04-04
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多