使用 PropertyGrid 显示和编辑动态对象






4.84/5 (22投票s)
如何使用 PropertyGrid 和动态对象来创建灵活便捷的业务对象编辑器
引言
在开发业务应用程序时,你经常会遇到一个小但令人恼火的问题:需要创建业务对象的构造函数,以便应用程序的管理员可以直接在用户界面中(无需编码)为业务对象添加新属性(或修改旧属性)。
例如,考虑一个管理组织员工信息的应用程序。`Employee` 类已经包含 `FirstName`、`LastName`、`BirthDate` 等属性,但现在管理员需要添加一个新的 `BirthPlace` 属性。
public class Employee
{
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime BirthDate { get; set; }
}
当你需要实现此功能时,首先想到的是不要使用类型化的业务对象,而是使用动态配置的对象。也就是说,类将包含一个伪属性集合,而不是明确定义的属性(如 `FirstName`、`LastName` 等),然后我们可以随时添加和删除新属性,以及按名称引用所需的属性。要引用业务对象的属性,你需要访问该集合,按名称查找正确的属性,并写回新值。这似乎是一个不错的解决方案,尽管你必须确保此类对象的持久性,这意味着你可能无法使用像 *Entity Framework* 或 *nHibernate* 这样的直接 ORM 工具。
dynamic employee = new BusinessObject();
employee.FirstName = "John";
employee.LastName = "Doe";
第二个任务是创建一个用户界面来查看和编辑此对象(编辑器)。如果对象的结构、其契约和 `public` 属性是提前已知的(正如普通类通常那样),这实际上不是一个大问题,因为你可以轻松地创建任何用于手动编辑的表单。但对于动态(可配置)对象来说,这可能是一个问题:你在开发过程中不知道它的结构,因此你不知道如何创建编辑器。接下来想到的解决方案是使用动态生成的 UI。每个人都知道 *Visual Studio* 中的 *PropertyGrid* 控件——它使用反射来检查未知对象,然后显示对象的结构并为其属性提供编辑器。基本上,这个控件是一个方便的工具,它是可配置的,并且非常适合管理任务。然而,由于该控件使用反射,它无法在可配置对象中找到我们动态定义的属性,因为对象的结构不同。
很明显,我们可以用自己的数据替换反射从对象中提取的数据。为此,我们可以使用 `PropertyDescriptor` 类(或者更确切地说,它的继承者),它是属性类的抽象,并且通常在 Visual Studio 的设计器中使用。这并非一个坏主意,但会带来一个小的不便:你将无法像使用普通强类型对象那样与业务对象进行交互,也无法以常规方式直接引用其属性,而必须以某种方式按名称查找所需的属性并间接引用它们。
var employee = new BusinessObject();
employee["FirstName"] = "John";
employee["LastName"] = "Doe";
这段代码(来源 3)看起来不寻常且相当笨拙。它甚至可能导致随机错误并增加开发时间,因为它违反了使用对象可以真正分散注意力并干扰开发人员工作的刻板印象。
如我们所见,上述解决方案可以正常工作,但存在一些缺点。因此,我建议使用一个更有趣、更方便的解决方案。
在 NET 4.0 中,出现了一个新功能——动态类型。你可以从 MSDN 获取有关动态类型的更多信息,因此我不会在本文中过多讨论理论。我想说的是,`DynamicObject` 类允许开发人员确定可以在动态属性上执行的操作,并设置如何执行这些操作。例如,你可以确定当你尝试获取或设置对象属性、调用方法或执行标准的数学运算(如加法和乘法)时会发生什么。如果你想为我们的业务对象创建用户友好的应用程序界面,这个类会很有用,因为它允许你访问先前未定义的属性。你不能直接创建 `DynamicObject` 的实例。要实现动态行为,你可以继承 `DynamicObject` 类并重写必要的 `TryGetMember` 和 `TrySetMember` 方法。
public class BusinessObject : DynamicObject
{
private readonly IDictionary dynamicProperties =
new Dictionary();
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
var memberName = binder.Name;
return dynamicProperties.TryGetValue(memberName, out result);
}
public override bool TrySetMember(SetMemberBinder binder, object value)
{
var memberName = binder.Name;
dynamicProperties[memberName] = value;
return true;
}
}
这给了我们一个巨大的优势,因为我们可以任意引用动态类型实例,从而模拟缺少的属性。
例如,在我们的 `BusinessObject` 类中没有 `FirstName` 属性,但是我们可以引用它,并且通过对此动态类型的正确实现,该属性将被自动添加。我们的动态类型看起来与普通类完全相同,并且我们通过“点”表示法来引用其属性。
为了确保 `PropertyGrid` 的正常工作,我们必须实现 `DynamicObject` 的继承者,以便它能够为控件提供其属性的描述:名称、数据类型、特定于 `PropertyGrid` 的属性(如 `Category` 或 `Description`)。下面给出的此类实现的示例,为简化起见,不支持属性。
public class BusinessObject : DynamicObject, ICustomTypeDescriptor, INotifyPropertyChanged
{
private readonly IDictionary<string, object> dynamicProperties =
new Dictionary<string, object>();
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
var memberName = binder.Name;
return dynamicProperties.TryGetValue(memberName, out result);
}
public override bool TrySetMember(SetMemberBinder binder, object value)
{
var memberName = binder.Name;
dynamicProperties[memberName] = value;
NotifyToRefreshAllProperties();
return true;
}
#region Implementation of ICustomTypeDescriptor
public PropertyDescriptorCollection GetProperties()
{
// of course, here must be the attributes associated
// with each of the dynamic properties
var attributes = new Attribute[0];
var properties = dynamicProperties
.Select(pair => new DynamicPropertyDescriptor(this,
pair.Key, pair.Value.GetType(), attributes));
return new PropertyDescriptorCollection(properties.ToArray());
}
public string GetClassName()
{
return GetType().Name;
}
#region Hide not implemented members
. . . . .
#endregion
#region Implementation of INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
if (PropertyChanged == null)
{
return;
}
var eventArgs = new PropertyChangedEventArgs(propertyName);
PropertyChanged(this, eventArgs);
}
private void NotifyToRefreshAllProperties()
{
OnPropertyChanged(String.Empty);
}
#endregion
private class DynamicPropertyDescriptor : PropertyDescriptor
{
. . . . .
}
从来源 5 可以看出,我们的类实现了 `ICustomTypeDescriptor` 接口,以确保 `PropertyGrid` 的正常工作。
`ICustomTypeDescriptor` 接口允许对象提供有关其内部结构的信息,即数据类型(与 `TypeDescriptor` 不同,它可以从元数据中检索信息)。例如,.NET 使用 `ICustomTypeDescriptor` 来提供有关 COM 对象类型的信息,这些对象不支持属性或属性。为了提供对类型动态数据的访问,动态类可以实现 `ICustomTypeDescriptor` 或继承 `CustomTypeDescriptor` 类,该类提供了该接口的简单实现。
此外,我们的类包含一个嵌套的 `private` 类 `DynamicPropertyDescriptor`,它描述了动态属性。
最常见的解决方案呈现在以下 UML 图中
private class DynamicPropertyDescriptor : PropertyDescriptor
{
private readonly BusinessObject businessObject;
private readonly Type propertyType;
public DynamicPropertyDescriptor(BusinessObject businessObject,
string propertyName, Type propertyType, Attribute[] propertyAttributes)
: base(propertyName, propertyAttributes)
{
this.businessObject = businessObject;
this.propertyType = propertyType;
}
public override bool CanResetValue(object component)
{
return true;
}
public override object GetValue(object component)
{
return businessObject.dynamicProperties[Name];
}
public override void ResetValue(object component)
{
}
public override void SetValue(object component, object value)
{
businessObject.dynamicProperties[Name] = value;
}
public override bool ShouldSerializeValue(object component)
{
return false;
}
public override Type ComponentType
{
get { return typeof(BusinessObject); }
}
public override bool IsReadOnly
{
get { return false; }
}
public override Type PropertyType
{
set { return propertyType; }
}
}
现在我们使用任何 `PropertyGrid` 的实现。在此示例中,我们考虑来自 www.codeplex.com 的开源 WPF 控件库,名为 *ExtendedWPFToolkit*。
如果我们替换以下行
var properties = TypeDescriptor.GetProperties(instance.GetType(),
new Attribute[] { new PropertyFilterAttribute(PropertyFilterOptions.All)});
用另一个
var properties = TypeDescriptor.GetProperties(instance)
那么 `PropertyGrid` 将显示我们的业务对象及其在运行时添加的所有属性(当然,正如我们记得的,我们还没有实现属性支持)。
同样,我们可以使用 `PropertyGrid` 来查看和编辑对象,并在运行时管理对象属性。此外,如果我们稍微扩展 `PropertyGrid`,添加添加和删除属性的命令,我们将能够以最小的努力创建一个通用的业务对象编辑器。我们所要做的就是提供持久性机制来保存所有添加的动态属性。
正如你所见,这个解决方案对于每个人来说都是灵活而方便的——对于应用程序用户来说很容易操作,对于程序员来说,操作这样的类会很快捷方便。
<extToolkit:PropertyGrid Name="propertyGrid1" Height="249" Width="336" />
首先,我们应该在 C# 代码中添加控件与我们的业务对象的绑定
propertyGrid1.SelectedObject = employee;
然后我们必须在 *PropertyGrid.cs 文件*(在 *ExtendedWPFToolkit* 源码中)找到以下方法
private List<PropertyItem> GetObjectProperties(object instance)
该方法接收目标对象属性的数据。
历史
- 2011年5月9日:首次发布