Custom TypeConverter for Editing Complex Objects

在.NET开发中,经常需要在用户界面上编辑对象的属性。这些对象可能是简单的数据类型,也可能是复杂的自定义类型。在这篇文章中,将探讨如何使用TypeConverter类来增强对象的编辑功能,使得复杂的对象可以像简单的字符串一样被编辑。

首先,来看一个实际的例子。假设需要存储测量单位,为此定义了一个简单的类,该类包含两个属性来描述一个给定的测量值。

public class Length { public override string ToString() { string value; string unit; value = this.Value.ToString(CultureInfo.InvariantCulture); unit = this.Unit.ToString(); return string.Concat(value, unit); } public Unit Unit { get; set; } public float Value { get; set; } }

这是一个非常标准的类,它只有两个属性以及一个默认的隐式构造函数。还重写了ToString方法,这对于调试目的非常有用,并且在PropertyGrid中显示的不仅仅是CustomTypeConverter1.Length。

为了演示,创建了一个示例类,其中包含三个Length属性。

internal class SampleClass { public Length Length1 { get; set; } public Length Length2 { get; set; } public Length Length3 { get; set; } }

为了完整性,这里是Unit枚举的定义。

public enum Unit { None, cm, mm, pt, px }

接下来,设置了一个示例项目,将SampleClass的实例绑定到PropertyGrid上,并将Length1属性预设为32px。当运行这个项目时,会发现编辑体验非常不满意,因为无法编辑任何内容。

那么,能做些什么呢?TypeConverterAttribute类允许将类与一个可以处理类型实例转换的类型关联起来。每个类型只能有一个这种属性的出现。可以使用两种方式之一提供转换类型:

[TypeConverter(typeof(LengthConverter))]

在这里,传入一个类型对象,这意味着类型必须直接被项目引用并作为依赖项分发。

[TypeConverter("CustomTypeConverter1.LengthConverter, CustomTypeConverter1")]

另一种选择是使用直接字符串,如上所示。这个字符串是完全限定的类型名称,意味着它可以位于不同的程序集中,但不是直接引用或标记为依赖项。

使用哪一个取决于需求,但请记住,字符串版本不能进行编译时检查,所以如果弄错了名称,将无法编辑类型直到发现问题!

ExpandableObjectConverter类是.NET框架内置的,它以最低的成本提供最基本的功能。

[TypeConverter(typeof(ExpandableObjectConverter))] public class Length { }

如果将Length类的声明更改为上述,并运行示例,会得到这样的结果:第一个属性现在可以展开,Length类中的每个属性都可以单独设置。然而,这种方法有两个立即的问题:属性只能一次编辑一个,不能通过根属性组合值。具有null值的属性(示例屏幕截图中的第二和第三个属性)无法实例化。

根据需求,这可能是完全可以接受的。在情况下,它不是,所以继续自定义转换器!

为了创建自定义转换器,需要有一个继承自TypeConverter的类。至少,会重写CanConvertFrom和ConvertFrom方法。

public class LengthConverter : TypeConverter { public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) { return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType); } public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { string stringValue; object result; result = null; stringValue = value as string; if (!string.IsNullOrEmpty(stringValue)) { int nonDigitIndex; nonDigitIndex = stringValue.IndexOf(stringValue.FirstOrDefault(char.IsLetter)); if (nonDigitIndex > 0) { result = new Length { Value = Convert.ToSingle(stringValue.Substring(0, nonDigitIndex)), Unit = (Unit)Enum.Parse(typeof(Unit), stringValue.Substring(nonDigitIndex), true) }; } } return result ?? base.ConvertFrom(context, culture, value); } }

这个简短的类做了什么?第一个重写,CanConvertFrom,是在.NET想要知道是否可以从给定类型转换时调用的。在这里,说“如果是字符串,那么是的可以转换”(或者至少尝试!),否则它回退并请求基础转换器是否可以进行转换。在大多数情况下,那可能是肯定的,但无论如何,保留它是明智的;

现在来看看有趣的方法。ConvertFrom执行类型转换。现在将忽略context参数,因为还没有需要它。可以使用culture参数作为指南,如果需要进行任何转换,如数字或日期。关键参数是value,因为它包含要转换的原始数据。

这个方法首先检查value是否是非null非空字符串。(如果使用.NET 4或以上,可能使用IsNullOrWhitespace方法)。接下来尝试找到第一个字母字符的索引 - 方法假设输入是的形式。

如果找到一个字母,那么创建一个新的Length对象,并使用对象初始化来设置Value属性为字符串的第一部分转换为float,使用Enum.Parse来设置Unit属性使用字符串的后半部分。这就是那个可怕命名的枚举。还是会向展示一个更好的方式!

这就是需要的全部。嗯,几乎,需要改变类头部:

[TypeConverter(typeof(LengthConverter))] public class Length { }

现在当运行示例项目时,可以直接在不同的Length基于属性中输入值,并将它们转换为正确的值,包括创建新值。

请注意,这个例子没有涵盖清除值 - 例如,如果输入一个空字符串。在这种情况下,可以在这种情况下返回一个新的Length对象,然后更改ToString方法以返回一个空字符串。简单地返回null从ConvertFrom实际上并不起作用,所以目前,不知道实现值重置的最佳方法。

错误处理没有演示错误处理,首先,这是一个裸骨的例子,也因为.NET为提供它,至少在属性网格的情况下。它会自动处理无法转换值的失败。缺点是相当不实用的错误消息。如果自己抛出异常,提供的异常文本会显示在对话框的Details部分,允许指定一个更简洁的消息。

转换为不同的数据类型除了将类型转换为类,还可以使用方法转换器将类转换为另一种类型,通过重写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 && destinationType == typeof(string)) result = length.ToString(); return result ?? base.ConvertTo(context, culture, value, destinationType); }

正如所覆盖的所有方法一样,如果不能明确处理传递的值,那么请基础类尝试处理它。上述方法展示了如何检查value是否是Length对象,然后如果destinationType是string,简单地返回value.ToString()。通过这种方法返回的任何内容都会出现PropertyGrid中,所以如果决定返回格式化的字符串 - 将需要在ConvertFrom中处理它们。

沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485