PropertyGrid 中集合数据的自定义显示






4.92/5 (164投票s)
2003 年 7 月 1 日
10分钟阅读

1129922

13594
PropertyGrid 被广泛用于显示对象的属性和值
引言
PropertyGrid
被广泛用于显示对象的属性和值。在之前的文章中,我已经展示了如何自定义和本地化显示的属性名称和属性描述(全球化 Property Grid 和 全球化 Property Grid - 再探)。
这一次,我将重点关注 Collections
及其数据。我想展示如何在 PropertyGrid
中自定义集合显示其包含对象的方式。
如果您将数组分配给 PropertyGrid
,您会发现 PropertyGrid
会显示数组中包含的所有对象(参见上文,图 1)。
这里第一个问题是,数组如何像属性一样为其包含的对象提供给 PropertyGrid
?此外,数组中包含的对象名称会显示为其序号([0],[1],...)。这就引出了第二个问题。我们如何自定义,以便显示更有意义的数据而不是序号?
在本文中,我将探讨这些问题的答案。我们将开发一个解决方案,其外观将如图 2 所示(参见上文)。
基础知识
对象可以通过实现 ICustomTypeDescriptor
接口来提供关于自身的自定义信息。此接口可用于提供动态类型信息。对于 Collection
对象来说,它会返回其内容,使其在 PropertyGrid
中显示为属性,正是这种情况。
类型描述符返回的信息包含有关类型的信息,例如其属性、事件、类型转换、设计时编辑器等。
如果未使用 ICustomTypeDescriptor
,则运行时将使用静态 TypeDescriptor
,它根据通过反射获得的元数据提供类型信息。
所以,我们已经走了一半路程:我们需要实现 ICustomTypeDescriptor
来访问自定义属性信息。
属性信息将由接口方法 GetProperties()
返回。此方法返回一个 PropertyDescriptorCollection
类型的对象。该对象是(您可能猜到了)PropertyDescriptor
对象的类型安全集合。PropertyDescriptor
是一个 abstract
基类。通过提供自己的派生自 PropertyDescriptor
的类,您可以根据需要提供自定义属性信息,以返回集合的内容并为每个项目显示有意义的内容(显示名称和描述),如上所示(再次参见图 2)。
这比听起来要容易。我将在提供的示例项目中进行演示。
一个示例项目
出于演示目的,我将使用一个示例项目。该项目很简单,但它展示了整体概念。它是一个基于 Windows Forms 的应用程序。以下是示例场景。
场景
在 PropertyGrid
中,我想显示一家公司员工列表的数据。员工应显示其全名。我想添加或删除员工,因此列表应该是可编辑的。列表中的员工项目应该是可展开的,以显示其完整数据。图 2(参见上文)显示了所需的结果。
概念
Organization
的一个实例代表公司。它知道它的员工,这些员工是 Person
。Organization
对象将被分配给 PropertyGrid
以显示其数据。应用程序的主窗体包含 PropertyGrid
。图 2 显示了此类图中的此初始结构。
设计
域由 Organization
、Employee
、Person
类表示。Organization
类的实例代表公司。它包含 Employee
对象的集合。Employee
对象代表公司的一名员工,包含其姓名、年龄、部门等。员工是人,因此它派生自 Person
。
Organization
对象需要包含一个 Employee
对象列表。使用数组不符合我们的要求,因为它不可编辑,并且使用序号而不是所需的员工全名。
正如在基础部分提到的,我们必须实现 ICustomTypeDescriptor
来返回我们的自定义属性描述符。因此,我们选择定义一个自定义的类型安全集合类,称为 EmployeeCollection
。该集合类将实现 ICustomTypeDescriptor
。
为了返回有关属性的自定义信息,EmployeeCollection
对象将为其每个项目创建一个类型为 PropertyDescriptor
的自定义属性描述符。因此,我们示例的最终类图将如下所示
我们开始实现。
实现 - 第一部分
Person 和 Employee 的实现
Person
和 Employee
类的实现非常简单。它们仅作为持有领域数据的实体,以在 PropertyGrid
中显示。我在这里不提供它们。请参阅示例代码(Employee.cs 和 Person.cs)。
EmployeeCollection 的实现
EmployeeCollection
的实现更有趣。最相关部分为粗体。
public class EmployeeCollection : CollectionBase, ICustomTypeDescriptor
{
继承自 CollectionBase
提供了基本的集合行为。只添加添加、删除和查询项目的方法。EmployeeCollection
类实现了 ICustomTypeDescriptor
接口以提供自定义类型信息。
首先,我们添加集合方法。
// Collection methods
implementation public void Add( Employee emp )
{
this.List.Add( emp );
}
public void Remove( Employee emp )
{
this.List.Remove( emp);
}
public Employee this[ int index ]
{
get
{
return (Employee)this.List[index];
}
}
然后,我们实现 ICustomTypeDescriptor
接口。尽管该接口有很多方法,但大多数方法都易于实现,因为我们可以将调用委托给 static
TypeDescriptor
对象的相应方法来提供标准行为。
// Implementation of ICustomTypeDescriptor:
public String GetClassName()
{
return TypeDescriptor.GetClassName(this,true);
}
public AttributeCollection GetAttributes()
{
return TypeDescriptor.GetAttributes(this,true);
}
public String GetComponentName()
{
return TypeDescriptor.GetComponentName(this, true);
}
public TypeConverter GetConverter()
{
return TypeDescriptor.GetConverter(this, true);
}
public EventDescriptor GetDefaultEvent()
{
return TypeDescriptor.GetDefaultEvent(this, true);
}
public PropertyDescriptor GetDefaultProperty()
{
return TypeDescriptor.GetDefaultProperty(this, true);
}
public object GetEditor(Type editorBaseType)
{
return TypeDescriptor.GetEditor(this, editorBaseType, true);
}
public EventDescriptorCollection GetEvents(Attribute[] attributes)
{
return TypeDescriptor.GetEvents(this, attributes, true);
}
public EventDescriptorCollection GetEvents()
{
return TypeDescriptor.GetEvents(this, true);
}
public object GetPropertyOwner(PropertyDescriptor pd)
{
return this;
}
我们以自定义方式实现的唯一方法是重载的 GetProperties()
方法。这些方法用于返回 PropertyDescriptor
对象集合,用于以自定义方式描述属性。这些属性描述符对象稍后将由 PropertyGrid
使用。
public PropertyDescriptorCollection GetProperties(Attribute[] attributes)
{
return GetProperties();
}
public PropertyDescriptorCollection GetProperties()
{
// Create a new collection object PropertyDescriptorCollection
pds = new PropertyDescriptorCollection(null);
// Iterate the list of employees
for( int i=0; i<this.List.Count; i++ )
{
// For each employee create a property descriptor
// and add it to the
// PropertyDescriptorCollection instance
CollectionPropertyDescriptor pd = new
CollectionPropertyDescriptor(this,i);
pds.Add(pd);
}
return pds;
}
}
GetProperties()
的实现非常直接。首先,我们创建一个 PropertyDescriptor
集合对象。该集合将保存返回给客户端的所有 PropertyDescriptor
。在这里,我们可以决定要返回哪种类型的信息。在我们的例子中,我们为员工列表中的每个项目创建一个 EmployeeCollectionPropertyDescriptor
类型的 PropertyDescriptor
对象,并将其添加到 PropertyDescriptor
集合中。
注意:此时只有员工列表成员对 PropertyGrid
可见。如果您想提供关于其他属性的描述,例如 Count
属性,那么请从 TypeDescriptor
对象获取标准的 PropertyDescriptor
集合,检索 Count
的 PropertyDescriptor
,并将其添加到 PropertyDescriptor
集合中。
EmployeeCollectionPropertyDescriptor
是我们从抽象基类 PropertyDescriptor
派生的自定义属性描述符类。该类用于按照我们想要的方式格式化显示名称和描述。我的实现将 PropertyDescriptor
与 EmployeeCollection
和适当项的索引相关联。两者将在构造期间提供(另一种实现方式是让 PropertyDescriptor
直接引用 Employee
对象)。
public class EmployeeCollectionPropertyDescriptor : PropertyDescriptor
{
private EmployeeCollection collection = null;
private int index = -1;
public CollectionPropertyDescriptor(EmployeeCollection coll,
int idx) : base( "#"+idx.ToString(), null )
{
this.collection = coll;
this.index = idx;
}
public override AttributeCollection Attributes
{
get
{
return new AttributeCollection(null);
}
}
public override bool CanResetValue(object component)
{
return true;
}
public override Type ComponentType
{
get
{
return this.collection.GetType();
}
}
public override string DisplayName
{
get
{
Employee emp = this.collection[index];
return emp.FirstName + " " + emp.LastName;
}
}
public override string Description
{
get
{
Employee emp = this.collection[index];
StringBuilder sb = new StringBuilder();
sb.Append(emp.LastName);
sb.Append(",");
sb.Append(emp.FirstName);
sb.Append(",");
sb.Append(emp.Age);
sb.Append(" years old, working for ");
sb.Append(emp.Department);
sb.Append(" as ");
sb.Append(emp.Role);
return sb.ToString();
}
}
public override object GetValue(object component)
{
return this.collection[index];
}
public override bool IsReadOnly
{
get { return true; }
}
public override string Name
{
get { return "#"+index.ToString(); }
}
public override Type PropertyType
{
get { return this.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)
{
// this.collection[index] = value;
}
}
看看 DisplayName
和 Description
的实现:DisplayName
将被格式化以返回员工的全名,而 Description
返回更具描述性的员工字符串。
Organization 的实现
Organization
的实现很简单。它只是创建一些 Employee
类型的示例对象并将它们添加到 EmployeeCollection
中。
public class Organization
{
EmployeeCollection employees = new EmployeeCollection();
public Organization()
{
// Instantiate test data objects and fill employee collection
Employee emp1 = new Employee();
emp1.FirstName = "Max";
emp1.LastName = "Headroom";
emp1.Age = 42;
emp1.Department = "Sales";
emp1.Role = "Manager";
this.employees.Add(emp1);
Employee emp2 = new Employee();
emp2.FirstName = "Lara";
emp2.LastName = "Croft";
emp2.Age = 24;
emp2.Department = "Accounting";
emp2.Role = "Manager";
this.employees.Add(emp2);
emps[0] = emp1;
emps[1] = emp2;
}
[TypeConverter(typeof(ExpandableObjectConverter ))]
public EmployeeCollection Employees
{
get { return employees; }
}
}
Organization
只有一个员工集合作为成员,它将在 Employees
属性中返回。注意 Employees
属性上使用了 TypeConverterAttribute
。这将是使 Employees
属性可展开所必需的,以便我们能够看到集合内容。
Organization
的一个实例将在 Form1
类的构造函数中创建,并分配给 PropertyGrid
。
public Form1()
{
InitializeComponent();
organization = new Organization();
PropertyGrid1.SelectedObject = organization;
}
到目前为止的实现结果如图 5 所示。项目不再按序号显示。列表中的员工按其全名显示。
该集合是可编辑的。如果您在 PropertyGrid
中选择员工节点的数值侧,您会看到一个按钮,用于调用标准编辑器(自定义编辑器也适合此上下文,但将是下一篇文章的主题)来修改员工集合(参见图 6)。
您可以添加、删除或修改集合项目。
实现 - 第二部分
在图 5 中,您在 PropertyGrid
的数值字段中看到类名(PropertyGridSample.Employee
)。这看起来不好。此外,我们无法内联查看或编辑员工数据。这是我们现在要改进的。
类型转换器
PropertyGrid
使用附加到对象或属性的类型转换器来定制 PropertyGrid
中项目的视图。类型转换器对象类型为 TypeConverter
,其最常见的用途是与文本表示之间进行转换。自定义类型转换器派生自 TypeConverter
。
可以通过使用 TypeConverterAttribute
将类型转换器附加到属性或类。
我们已经在 Organization
类中使用了类型转换器,使 Employees
属性可展开。
[TypeConverter(typeof(ExpandableObjectConverter ))]
public EmployeeCollection Employees
{
get { return employees; }
}
ExpandableObjectConverter
是 .NET Framework 定义的标准类型转换器之一。.NET 为基本和最常见的类型提供了 标准类型转换器。
ExpandableObjectConverter
也可用于展开我们的 employee
对象以查看其属性。这一次,我们将该类型转换器附加到一个类(Employee
)。
[TypeConverter(typeof(ExpandableObjectConverter))]
public class Employee : Person
{
现在 Employee
对象是可展开的(参见图 8)。
很好,但 ExpandableObjectConverter
在员工项目的数值字段中显示类名。我们应该改变这一点。我们提供自己的类型转换器。将我们的自定义类型转换器派生自 ExpandableObjectConverter
是个好主意,因为我们仍然希望它是可展开的。
internal class EmployeeConverter : ExpandableObjectConverter
{
EmployeeConverter
类被定义为内部的,以便客户端不可见。为了我们的目的,我们只需要重写 ConvertTo()
。此方法将 Employee
对象转换为任何其他对象。我们将其定义如下。
public override object ConvertTo(ITypeDescriptorContext context,
System.Globalization.CultureInfo culture,
object value, Type destType )
{
if( destType == typeof(string) && value is Employee )
{
Employee emp = (Employee)value;
return emp.Department + "," + emp.Role;
}
return base.ConvertTo(context,culture,value,destType);
}
}
我们确保要转换的值是 Employee
类型,并且转换的目标类型是 string
。我们可以返回任何我们想要的文本。我选择显示部门后跟员工部门的角色。
为了应用我们的类型转换器,我们稍微修改了之前对 TypeConverterAttribute
的使用。
[TypeConverter(typeof(EmployeeConverter))]
public class Employee : Person
{
现在将使用我们的自定义类型转换器 EmployeeConverter
。就是这样!
我们的最终结果如图 9 所示(我还向 EmployeeConverter
添加了一个自定义类型转换器,它简单地显示了一个 static string
“Company's employee data
”)。
我们现在有了
- 一个员工列表,在左列显示员工的全名,而不是简单的序号。
- 员工列表中的员工项目可展开以进行内联编辑。
- 员工项目的数值字段(右列)中显示有意义的数据,而不是显示的类名。
- 选择
Employees
集合,可以调用标准编辑器来通过添加或删除员工对象来修改集合。
摘要
哇,看起来东西很多。但如果你理解了这个概念,其实并没有那么多。类型描述符和成员描述符是提供动态信息的关键。您可以通过提供自己的实现来定制动态信息。
以下是自定义 PropertyGrid
中集合内容的显示和描述的步骤:
- 通过派生自
abstract
基类PropertyDescriptor
的类,提供自定义属性描述符。 - 重写
abstract
方法和属性。为DisplayName
和description
属性提供适当的实现。 - 让您的集合类实现
ICustomTypeDescriptor
接口。 - 通过
GetProperties()
方法返回自定义属性描述符的集合。 - 可选择使用 .NET 提供的
TypeConverter
派生对象或实现自己的类来定制领域类的文本表示。通过使用TypeConverterAttribute
类将它们分配给适当的类或属性。
要全局化 PropertyGrid
数据,可以将属性描述符链接在一起(另请参阅 全球化属性网格)。
参考文献
历史
- 2003 年 7 月 1 日:初始版本
许可证
本文未附加明确的许可证,但可能在文章文本或下载文件本身中包含使用条款。如有疑问,请通过下面的讨论区联系作者。
作者可能使用的许可证列表可以在此处找到。