Xceed WPF propertyGrid показать элемент для расширенной коллекции

как, я показываю ObservableCollection<> пользовательских объектов в xCeeD WPF PropertyGrid, в котором каждый элемент списка может быть развернут для отображения свойств пользовательских объектов. (то есть:

---- - PropertyGrid - - - - -

CoreClass

  • (+/ -) ObservableCollection

    • (+/-) CustomClass.Объект1

      • Property1: Значение

      • Property2: Значение

      • ...

      • PropertyN: Значение

    • (+/-) CustomClass.Объект2

      • Property1: Значение

      • Property2: Значение

      • ...

      • PropertyN: Значение

если я использую [ExpandableObject] на ObservableCollection<> он показывает только количество свойство.

Edit:(добавлены код)

файл 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; }
    }
}

2 ответов


отметим, что эта идея происходит от проект CodeProject, который вы связали с. Статья получает вас большую часть пути, но, как вы заметили, она не расширяет каждый элемент в коллекции для 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();
  }
}

Маккичен предоставил основные ключи к этому...

нет необходимости наследовать от 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>)
        {
            IList<T> list = value as IList<T>; 
            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 для любого типа, который вы хотите расширить в сетке свойств, и AddExpandableIListConverter для любого типа IList, который вы хотите расширить в сетке.

например, если у вас есть объект кривой с некоторыми свойства, включая IList, тогда все свойства и элементы списка могут быть расширены следующим образом:

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

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