logo
03

Конвертор типа

Рассмотрим построение конвертора типа на примере класса SpaceSize, хранящего информацию об отступах слева, справа, сверху и снизу:

[Serializable]

[TypeConverter(typeof(SpaceSizeConverter))]

public struct SpaceSize

{

private int _left;

private int _top;

private int _right;

private int _bottom;

public SpaceSize(int left, int top, int right, int bottom)

{

_left = left;

_top = top;

_right = right;

_bottom = bottom;

}

public int Left { get { return _left; } }

public int Top { get { return _top; } }

public int Right { get { return _right; } }

public int Bottom { get { return _bottom; } }

}

Как можно видеть, SpaceSize является неизменяемой (immutable) структурой. Поэтому сериализация свойства этого типа в код должна выглядеть как-то так:

this.MyControl2.Margins = new SpaceSize(5, 5, 5, 20);

Для этого мы определяем следующий конвертер типа:

public class SpaceSizeConverter : TypeConverter

Реализуем необходимые методы:

public override bool

CanConvertFrom(ITypeDescriptorContext context, Type sourceType)

{

if (sourceType == typeof(String))

return true;

return base.CanConvertFrom(context, sourceType);

}

public override

bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)

{

if (destinationType == typeof(InstanceDescriptor))

return true;

if (destinationType == typeof(string))

return true;

return base.CanConvertTo(context, destinationType);

}

В данном случае мы сообщаем, что можем создать свой тип из строки, а также вернуть строку и InstanceDescriptor, имея экземпляр своего типа. Не забывайте вызывать базовый класс, если не знаете как поступить с некоторым типом.

Далее рассмотрим конвертацию из строки в объект типа SpaceSize. Обратите внимание на комментарии в коде:

public override object ConvertFrom(ITypeDescriptorContext context,

CultureInfo culture, object value)

{

if (value is string)

{

try

{

// убираем лишние пробелы слева и справа

string str = ((string)value).Trim();

// мы не должны возвращать null, поскольку SpaceSize – value-type

if (str.Length == 0)

return new SpaceSize(0,0,0,0);

if (culture == null)

// берем текущую культуру, если не задано

culture = CultureInfo.CurrentCulture;

// разделяем строку на части используя разделитель списка из культуры

string[] strs=((string)value).Split(culture.TextInfo.ListSeparator[0]);

if (strs.Length != 4) // проверяем, что получилось 4 элемента

throw new ArgumentException("Can not convert '"

+ (string)value + "' to type SpaceSize");

int[] nums = new int[strs.Length];

// используем конвертер для целых чисел и преобразуем каждый элемент

TypeConverter typeConverter = TypeDescriptor.GetConverter(typeof(int));

for (int i = 0; i < nums.Length; i++)

nums[i] = (int)typeConverter.ConvertFromString(

context, culture, strs[i]);

// создаем новый объект

return new SpaceSize(nums[0], nums[1], nums[2], nums[3]);

}

catch

{

// грубая обработка ошибок

throw new ArgumentException("Can not convert '"

+ (string)value + "' to type SpaceSize");

}

}

return base.ConvertFrom(context, culture, value);

}

Таким образом, мы объяснили дизайнеру, как введенное пользователем в окне свойств значение, например, “32;32;10;10” преобразовать к объекту нашего типа. Заметьте, что мы использовали CultureInfo для получения разделителя списка, тем самым упростив жизнь людям в других странах, где список может разделяться другим символом. Аналогично мы не стали использовать int.Parse для строки, а использовали TypeConverter для типа int. В общем-то, разницы в данном конкретном случае нет, поскольку Int64Converter использует Convert.ToInt64 и Int64.Parse для выполнения этого преобразования, однако в более сложных случаях лучше использовать именно такой подход.

Теперь рассмотрим обратное преобразование, которое выполняется очень похоже:

public override object ConvertTo(ITypeDescriptorContext context,CultureInfo culture, object value, System.Type destinationType)

{

if (destinationType == typeof(string) && value is SpaceSize)

{

// преобразуем к нашему типу

SpaceSize spaceSize = (SpaceSize)value;

if (culture == null)

// используем культуру по умолчанию,

// если она не была передана в параметре

culture = CultureInfo.CurrentCulture;

// получаем разделитель списка

string str = culture.TextInfo.ListSeparator + " ";

// получаем конвертор типа для целых чисел

// и преобразуем каждое свойство к строке

TypeConverter typeConverter = TypeDescriptor.GetConverter(typeof(int));

string[] strs = new string[4];

int i = 0;

strs[i++] = typeConverter.ConvertToString(

context, culture, spaceSize.Left);

strs[i++] = typeConverter.ConvertToString(

context, culture, spaceSize.Top);

strs[i++] = typeConverter.ConvertToString(

context, culture, spaceSize.Right);

strs[i++] = typeConverter.ConvertToString(

context, culture, spaceSize.Bottom);

// объединяем строки в одну и возвращаем результат

return String.Join(str, strs);

}

// здесь будет добавлена конвертация в InstanceDescriptor ...

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

}

В приведенном выше коде значения свойств преобразуются в строки с использованием информацию из культуры и конвертор типа int. Затем они объединяются, а для читаемости добавляется пробел между значениями. Теперь рассмотрим конвертацию в InstanceDescriptor:

if (destinationType == typeof(InstanceDescriptor) && value is SpaceSize)

{

SpaceSize spaceSize = (SpaceSize)value;

// получить информацию о конструкторе с данными типами параметров

ConstructorInfo constructorInfo = typeof(SpaceSize).GetConstructor(

new Type[]{ typeof(int), typeof(int), typeof(int), typeof(int) }

);

if (constructorInfo != null)

// создать описатель экземпляра из информации о конструкторе

// и конкретных параметров конструктора

return new InstanceDescriptor(constructorInfo,

new object[]{spaceSize.Left, spaceSize.Top, spaceSize.Right, spaceSize.Bottom});

}

InstanceDescriptor будет использоваться для генерирования кода инициализации сериализуемого свойства. Он создает в методе InitializeComponent формы, на которую будет помещен наш контрол, код следующего вида:

this.MyControl2.Margins = new SpaceSize(5, 5, 5, 20);

Следующий код, будучи добавленным в конвертор типа, позволит создавать объекты, имея на руках имена свойств и их значения. Эти методы используются дизайнером в случае неизменяемого (immutable) объект (как SpaceSize). Однако как быть, если хочется редактировать отдельные свойства объекта, открытого в окне свойств? В этом случае при изменении свойств объект создается именно этим методом.

public override object CreateInstance(ITypeDescriptorContext context,

IDictionary propertyValues)

{

return new SpaceSize((int)propertyValues["Left"],

(int)propertyValues["Top"],

(int)propertyValues["Right"],

(int)propertyValues["Bottom"]);

}

public override bool GetCreateInstanceSupported(ITypeDescriptorContext context)

{

return true;

}

Следующие два метода позволяют редактору свойств развернуть объект и показать свойства в отдельных ячейках в нужном порядке. Если это единственное, чего вы хотите от конвертера (разворачивание свойств), то достаточно использовать ExpandableObjectConverter.

public override PropertyDescriptorCollection

GetProperties(ITypeDescriptorContext context,

object value, Attribute[] attributes)

{

return TypeDescriptor.GetProperties(typeof(SpaceSize), attributes).Sort(

new string[]{"Left", "Top", "Right", "Bottom"});

}

public override bool GetPropertiesSupported(ITypeDescriptorContext context)

{

return true;

}

На этом написание конвертора закончено. Вы можете добавить свойство типа SpaceSize к своему контролу и посмотреть на него в дизайнере.