在.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中处理它们。