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

创建自定义 TypeConverter:第 2 部分 - 实例描述符、可扩展属性和标准值

starIconstarIconstarIconstarIconstarIcon

5.00/5 (5投票s)

2013年7月30日

CPOL

6分钟阅读

viewsIcon

14458

downloadIcon

91

这篇后续文章扩展了第一部分中的示例,包括更简洁的设计时代码生成、可扩展属性支持以及自定义值列表。

在本文系列的第一部分中,我描述了如何创建一个简单的类型转换器,用于将对象转换为字符串以及从字符串转换为对象。这篇后续文章扩展了该示例,包括更简洁的设计时代码生成、可扩展属性支持以及自定义值列表。

本文中的示例假定您正在使用来自第一部分的原始示例项目。

设计器代码

当您将 `Control` 或 `Component` 放置到设计器表面(例如 `Form`)上时,IDE 会自动生成初始化对象所需的任何代码。

修改 `SampleClass` 类以继承自 `Component`,然后将一个实例拖到窗体上并设置第一个属性。保存窗体,然后打开设计器文件。您应该会看到类似这样的代码:

private void InitializeComponent()
{
  CustomTypeConverter2.Length length1 = new CustomTypeConverter2.Length();

  // ... SNIP ...

  // 
  // sample
  // 
  length1.Unit = CustomTypeConverter2.Unit.px;
  length1.Value = 32F;
  this.sample.Length1 = length1;
  this.sample.Length2 = null;
  this.sample.Length3 = null;

  // ... SNIP ...

}

设计器已生成填充对象所需的源代码,通过单独指定每个属性。但是,如果您想一次性设置两个属性,或者执行其他初始化代码,该怎么办?我们可以使用类型转换器来解决这个问题。

虽然这稍微超出了本文的范围,但Nonetheless 值得一提。在上面的代码片段中,您可以看到 `Length2` 和 `Length3` 属性被显式赋值为 `null`,尽管它们已经是这些属性的默认值。如果您正在创建面向公众的库组件,最好为属性应用 `DefaultValue` 属性。这使得代码更整洁(如果值是默认值,则不会生成代码),并且允许其他组件执行自定义处理(如果需要)。例如,`PropertyGrid` 会以普通样式显示默认属性,以粗体显示非默认属性。

更新 Length 类

在调整类型转换器以支持代码生成之前,我们需要扩展 `Length` 类,添加一个新的构造函数。

public Length()
{ }

public Length(float value, Unit unit)
  : this()
{
  this.Value = value;
  this.Unit = unit;
}

我添加了一个构造函数,它将设置类的 `Value` 和 `Unit` 属性。由于添加了带参数的构造函数,我现在需要显式定义一个无参数构造函数,因为不再生成隐式构造函数,而我仍然希望能够执行 `new Length()`。

完成这些修改后,我们就可以深入研究类型转换器的修改了。

CanConvertTo

我们首先需要更新类型转换器,以表明它支持 `InstanceDescriptor` 类,这是 IDE 用于自定义代码生成的机制。我们可以通过重写一个新的方法 `CanConvertTo` 来实现这一点。

从上一篇文章中更新 `LengthConverter` 类,包含以下内容:

public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
{
  return destinationType == typeof(InstanceDescriptor) || base.CanConvertTo(context, destinationType);
}

这些新的重写将告知调用者,除了 `TypeConverter` 基类可以处理的任何内容之外,我们现在还支持 `InstanceDescriptor` 类型。

扩展 ConvertTo

我们在上一篇文章中简要介绍了 `ConvertTo` 重写,以便将我们的 `Length` 对象显示为字符串。既然我们已经重写了 `CanConvertTo` 来表明我们可以处理其他类型,我们还需要更新此方法。

`InstanceDescriptor` 类包含重新生成对象所需的信息,由两部分主要信息组成。

  • 一个 `MemberInfo` 对象,描述类中的一个方法。这可以是构造函数(我们将在示例中使用),或者是一个返回新对象的静态方法 - 例如 `Color.FromArgb`。
  • 一个 `ICollection`,包含传递给源成员所需的任何参数。

让我们更新 `ConvertTo` 以包含提取支持。

public override object ConvertTo(ITypeDescriptorContext context, 
                                 CultureInfo culture, object value, Type destinationType)
{
  Length length;
  object result;

  result = null;
  length = value as Length;

  if (length != null)
  {
    if (destinationType == typeof(string))
      result = length.ToString();
    else if (destinationType == typeof(InstanceDescriptor))
    {
      ConstructorInfo constructorInfo;

      constructorInfo = typeof(Length).GetConstructor(new[] { typeof(float), typeof(Unit) });
      result = new InstanceDescriptor(constructorInfo, new object[] { length.Value, length.Unit });
    }
  }

  return result ?? base.ConvertTo(context, culture, value, destinationType);
}

我们仍然执行 `null` 检查以确保有有效值可以转换,但现在我们检查类型是 `string` 还是 `InstanceDescriptor`,并相应地进行处理。

对于实例描述符,我们使用反射来获取接受两个参数的构造函数,然后从中创建一个 `InstanceDescriptor` 对象。很简单!

现在,当我们修改设计器中的 `SampleClass` 组件时,会生成类似下面的源代码。(有下一节警告的提示)

请注意,我还修改了 `SampleClass` 上的属性以包含 `[DefaultValue(typeof(Length), "")]` 以支持默认值。

private void InitializeComponent()
{
  // ... SNIP ...

  // 
  // sample
  // 
  this.sample.Length1 = new CustomTypeConverter2.Length(16F, CustomTypeConverter2.Unit.px);

  // ... SNIP ...  
}

更加整洁!

关于 Visual Studio 的警告

在撰写本文时,Visual Studio 经常会出问题,拒绝生成设计时代码。我猜测这是由于 Visual Studio 缓存了包含 `TypeConverter` 的程序集,或者这是无法在不销毁应用程序域的情况下卸载托管程序集的又一种表现。无论原因是什么,我都发现它很快就成为一个令人沮丧的源头,需要频繁重启 IDE 才能获取更改后的代码。

作为实验,我做了一个测试,其中 `Length` 和 `LengthConverter` 类位于另一个以二进制形式引用的程序集中。在这种模式下,我没有遇到任何问题。

最后,虽然基本转换易于调试,但 `InstanceDescriptor` 转换则困难得多。

需要牢记的一点。

可扩展属性

回到 `ExpandableObjectConverter` 和属性扩展,这可以通过重写 `GetPropertiesSupported` 和 `GetProperties` 方法轻松添加到自定义转换器中。

public override bool GetPropertiesSupported(ITypeDescriptorContext context)
{
  return true;
}

public override PropertyDescriptorCollection GetProperties
(ITypeDescriptorContext context, object value, Attribute[] attributes)
{
  return TypeDescriptor.GetProperties(value, attributes);
}

首先,通过重写 `GetPropertiesSupported`,我们告诉调用者我们支持单个属性编辑。然后我们可以重写 `GetProperties` 来返回要显示的实际属性。

在上面的示例中,我们返回所有可用属性,这可能是正常的行为。假设 `Length` 类上有一个我们不想显示的属性。我们可以返回一个不同的集合,并过滤掉该属性。

public override PropertyDescriptorCollection GetProperties
(ITypeDescriptorContext context, object value, Attribute[] attributes)
{
  //return TypeDescriptor.GetProperties(value, attributes);
  return new PropertyDescriptorCollection
  (TypeDescriptor.GetProperties(value, attributes).Cast<PropertyDescriptor>().Where
  (p => p.Name != "BadProperty").ToArray());
}

一个尴尬的例子,但它确实演示了该功能。

属性网格会尊重 `Browsable` 属性 - 这是控制属性可见性的比上述方法好得多的方式!

自定义值

我最后想演示的例子是自定义值。虽然您可能认为需要创建一个自定义 `UITypeEditor`,但如果您只想要一个基本的下拉列表,可以直接从类型转换器中通过重写 `GetStandardValuesSupported` 和 `GetStandardValues` 来实现。

public override bool GetStandardValuesSupported(ITypeDescriptorContext context)
{
  return true;
}

public override TypeConverter.StandardValuesCollection GetStandardValues(ITypeDescriptorContext context)
{
  List<Length> values;

  values = new List<Length>();
  values.Add(new Length(16, Unit.px));
  values.Add(new Length(32, Unit.px));
  values.Add(new Length(64, Unit.px));
  values.Add(new Length(128, Unit.px));

  return new StandardValuesCollection(values);
}

首先,您需要重写 `GetStandardValuesSupported` 以指定我们确实支持此类值。然后,在 `GetStandardValues` 中,我们只需返回我们想要显示的对象。在此示例中,我生成了 4 个长度并返回它们。运行程序时,您可以查看和选择这些值。当然,您需要确保返回的值可以被 `ConvertFrom` 方法处理!

总结

添加一个高级类型转换器仍然是一项简单的任务,它可以帮助丰富编辑功能。

您可以从本文开头链接下载完整的示例。

© . All rights reserved.