65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (164投票s)

2003 年 7 月 1 日

10分钟阅读

viewsIcon

1129922

downloadIcon

13594

PropertyGrid 被广泛用于显示对象的属性和值

图 1:使用数组

图 2:使用自定义集合

引言

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 的一个实例代表公司。它知道它的员工,这些员工是 PersonOrganization 对象将被分配给 PropertyGrid 以显示其数据。应用程序的主窗体包含 PropertyGrid。图 2 显示了此类图中的此初始结构。

图 3:示例类图

设计

域由 OrganizationEmployeePerson 类表示。Organization 类的实例代表公司。它包含 Employee 对象的集合。Employee 对象代表公司的一名员工,包含其姓名、年龄、部门等。员工是人,因此它派生自 Person

Organization 对象需要包含一个 Employee 对象列表。使用数组不符合我们的要求,因为它不可编辑,并且使用序号而不是所需的员工全名。

正如在基础部分提到的,我们必须实现 ICustomTypeDescriptor 来返回我们的自定义属性描述符。因此,我们选择定义一个自定义的类型安全集合类,称为 EmployeeCollection。该集合类将实现 ICustomTypeDescriptor

为了返回有关属性的自定义信息,EmployeeCollection 对象将为其每个项目创建一个类型为 PropertyDescriptor 的自定义属性描述符。因此,我们示例的最终类图将如下所示

图 4:最终设计类图

我们开始实现。

实现 - 第一部分

Person 和 Employee 的实现

PersonEmployee 类的实现非常简单。它们仅作为持有领域数据的实体,以在 PropertyGrid 中显示。我在这里不提供它们。请参阅示例代码(Employee.csPerson.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 集合,检索 CountPropertyDescriptor,并将其添加到 PropertyDescriptor 集合中。

EmployeeCollectionPropertyDescriptor 是我们从抽象基类 PropertyDescriptor 派生的自定义属性描述符类。该类用于按照我们想要的方式格式化显示名称和描述。我的实现将 PropertyDescriptorEmployeeCollection 和适当项的索引相关联。两者将在构造期间提供(另一种实现方式是让 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;
    }
}

看看 DisplayNameDescription 的实现: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:编辑集合内容

图 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)。

图 8:可展开的 Employee

很好,但 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 stringCompany's employee data”)。

图 9:最终外观

我们现在有了

  • 一个员工列表,在左列显示员工的全名,而不是简单的序号。
  • 员工列表中的员工项目可展开以进行内联编辑。
  • 员工项目的数值字段(右列)中显示有意义的数据,而不是显示的类名。
  • 选择 Employees 集合,可以调用标准编辑器来通过添加或删除员工对象来修改集合。

摘要

哇,看起来东西很多。但如果你理解了这个概念,其实并没有那么多。类型描述符和成员描述符是提供动态信息的关键。您可以通过提供自己的实现来定制动态信息。

以下是自定义 PropertyGrid 中集合内容的显示和描述的步骤:

  • 通过派生自 abstract 基类 PropertyDescriptor 的类,提供自定义属性描述符。
  • 重写 abstract 方法和属性。为 DisplayNamedescription 属性提供适当的实现。
  • 让您的集合类实现 ICustomTypeDescriptor 接口。
  • 通过 GetProperties() 方法返回自定义属性描述符的集合。
  • 可选择使用 .NET 提供的 TypeConverter 派生对象或实现自己的类来定制领域类的文本表示。通过使用 TypeConverterAttribute 类将它们分配给适当的类或属性。

要全局化 PropertyGrid 数据,可以将属性描述符链接在一起(另请参阅 全球化属性网格)。

参考文献

历史

  • 2003 年 7 月 1 日:初始版本

许可证

本文未附加明确的许可证,但可能在文章文本或下载文件本身中包含使用条款。如有疑问,请通过下面的讨论区联系作者。

作者可能使用的许可证列表可以在此处找到。

© . All rights reserved.