如何在Xceed WPF PropertyGrid中显示ObservableCollection<>
个自定义对象,其中每个列表项都可以展开以显示自定义对象属性。 (即:
---- ----- PropertyGrid的
CoreClass
(+/-)ObservableCollection&lt; CustomClass&gt;
(+/-)CustomClass.Object1
Property1:Value
Property2:Value
...
PropertyN:Value
(+/-)CustomClass.Object2
Property1:Value
Property2:Value
...
PropertyN:Value
如果我在[ExpandableObject]
上使用ObservableCollection<>
,则只会显示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; }
}
}
答案 0 :(得分:10)
请注意,这个想法的大部分来自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
内容包含查看内容,但¯\_(ツ)_/¯
。
此外,您需要将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();
}
}
答案 1 :(得分:4)
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>)
{
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动态修饰要在属性网格中显示的类型。我创建了一位装修经理&#39;实现这一点如下。
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类型调用AddExpandableIListConverter。
例如,如果您有一个包含IList的某些属性的曲线对象,则可以使所有属性和列表项可扩展,如下所示:
ObjectDecorationManager.AddExpandableObjectConverter(typeof(Curve));
ObjectDecorationManager.AddExpandableObjectConverter(typeof(CurvePoint));
AddCoreExpandableListConverter<CurvePoint>(typeof(IList<CurvePoint>));