logo
03

Расширение компонентов

Несколько в стороне от общей системы режима дизайна находится возможность расширения компонентов дополнительными свойствами. Иными словами, компонент, находящийся в некотором контейнере, например, форме, имеет возможность добавить ряд «виртуальных» свойств к другим компонентам в этом же контейнере. Эти псевдо-свойства отображаются как свойства расширенного компонента, однако на самом деле задание и считывание их значений осуществляются через компонент-расширитель. Это делается посредством интерфейса IExtenderProvider и атрибута ProvideProperty.

Чтобы создать компонент-расширитель, создайте класс компонента и реализуйте интерфейс IExtenderProvider. Система дизайна, обнаружив среди компонентов такой поставщик свойств, вызовет его метод CanExtend для каждого компонента на форме. Если возвращаемое значение будет true для некоторого компонента, дизайнер добавит к нему псевдо-свойства, описанные в атрибутах ProvideProperty.

Псевдо-свойства расширения описываются как [ProvideProperty("PropertyName", ExtendeeType)]. При этом у расширенного компонента имя свойства в дизайнере будет выглядеть как «PropertyName on ExtenderName», где PropertyName – имя свойства из атрибута ProvideProperty, а ExtenderName – имя компонента расширения в контейнере. К сожалению, при выборе объектов для расширения система дизайна не фильтрует объекты по типу ExtendeeType, и в метод CanExtend попадут все возможные компоненты, включая и расширитель, и контейнер. Поэтому метод CanExtend должен делать соответствующие проверки:

public bool CanExtend(object extendee)

{

ExtendeeType ext = extendee as ExtendeeType;

if (ext == null) // не наш тип

return false;

if (ext == this) // не расширяем себя

return false;

// дальше, собственно, логика расширения

if (...)

return true;

return false;

}

Учтите, что система дизайна вызывает CanExtend довольно часто, особенно при открытии формы, по несколько раз на каждый компонент и контрол. Поэтому метод CanExtend должен отрабатывать максимально быстро. Исследования показывают, что эта проверка выполняется после установки каждого свойства, описанного в InitializeComponent.

Код getter и setter для таких псевдо-свойств описывается в классе-расширителе следующим образом:

PropertyType GetPropertyName(ExtendeeType extendee)

{

...

return value;

}

void SetPropertyName(ExtendeeType extendee, PropertyType value)

{

...

}

Здесь PropertyName – это имя свойства, описанного в атрибуте. Тип свойства определяется типом возвращаемого значения метода GetPropertyName. Вообще, GetPropertyName является «представителем» свойства. Если создаваемому виртуальному свойству необходимо назначить некоторые атрибуты (такие, как DefaultValue, Browsable, DesignerSerializationVisibility и аналогичные), их нужно применять к этому методу.

Для компонентов, реализующих IExtenderProvider, поддерживаются методы ShouldSerializeNNN и ResetNNN, принимающие один параметр – расширяемый объект указанного в атрибуте типа:

// вовзращает true, если нужно сериализовать

// псевдо-свойство PropertyName для объекта extendee

bool ShouldSerializePropertyName(ExtendeeType extendee);

// устанавливает псевдо-свойство PropertyName

// для объекта extendee в значение по умолчанию

void ResetPropertyName(ExtendeeType extendee);

Разработчикам приходится часто сталкиваться с несколькими проблемами в существующей системе компонентов-расширителей. Во-первых, при добавлении компонента на форму опрос CanExtend происходит до того, как заданы значения его свойств. Это особенно неудобно, если для принятия решения о расширении нужно иметь ссылку на контейнер (свойство Parent). Во-вторых, дизайнер никак не сообщает компоненту-расширителю, что некоторый компонент был удалён из контейнера.

У контейнеров, хранящих контролы нужно подписаться на события ControlAdded и ControlRemoved или перегрузить соответствующие защищенные методы. При добавлении или удалении контрола необходимо обновить внутренние структуры данных, хранящие соответствия между расширенными компонентами и значениями псевдо-свойств. Если по каким-то причинам CanExtend вернул False, псевдо-свойство не будет добавлено. Заставить дизайнер сделать еще одну попытку добавить псевдо-свойство можно, только перезагрузив состояние дизайнера (то есть все компоненты). Чтобы это сделать, нужно получить сервис IDesignerLoaderService и вызвать у него метод Reload. Сделать это можно так:

protected override void OnControlAdded(ControlEventArgs e)

{

base.OnControlAdded (e);

// проверяем, что мы в режиме дизайна

if (Site != null && Site.DesignMode)

{

// получаем сервис нужного типа

IDesignerLoaderService service =

(IDesignerLoaderService)Site.GetService(

typeof(IDesignerLoaderService));

// если сервис предоставлен, перезагружаем дизайнер

if (service != null)

service.Reload();

}

}

При этом дизайнер перезагрузит все компоненты, опросит их на предмет свойств и учтёт компоненты-расширители. В данном случае контрол уже будет иметь, например, выставленное свойство Parent, и его расширение пройдёт успешно.

Существует еще один способ внедрения собственного кода в процесс редактирования компонентов. В окне свойств компонента можно добавить маленькую картинку около имени свойства и обработать двойной щелчок по этой картинке. Для этого нужно воспользоваться сервисом IPropertyValueUIService, который доступен описанным ранее способом через Site. Этот сервис позволяет добавить в систему дизайна делегат типа PropertyValueUIHandler, который вызывается для каждого свойства в данном контейнере. В обработчике вы можете выбрать свойства, которые нужно расширить, и добавить объекты PropertyValueUIItem в полученный массив. Эти объекты как раз и представляют картинку около свойства (размером 8х8), обработчик её активации и строку подсказки (ToolTip). К сожалению, строка подсказки в настоящее время не используется, но, возможно, в будущих версиях окна свойств значение этого свойства будет учтено. Приведем пример добавления обработчика:

// перегружаем свойство Site для добавления своего обработчика

public override ISite Site

{

get { return base.Site; }

set

{

base.Site = value;

// получаем сервис

IPropertyValueUIService uiService =

(IPropertyValueUIService)this.GetService(

typeof(IPropertyValueUIService));

// если сервис присутствует, добавляем свой обработчик

if( uiService != null )

uiService.AddPropertyValueUIHandler(

new PropertyValueUIHandler(MyPropertyValueUIHandler));

}

}

// описываем обработчик, который выбирает редактируемые свойства,

// помеченные атрибутом [Bindable(true)]

private void MyPropertyValueUIHandler(ITypeDescriptorContext

context, PropertyDescriptor propDesc, ArrayList itemList)

{

if (!propDesc.IsReadOnly

&& propDesc.Attributes.Contains(BindableAttribute.Yes))

{

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

Image img = Image.FromStream(

Assembly.GetExecutingAssembly().GetManifestResourceStream(

"BindableProperty.bmp"));

// добавляем новый элемент расширения для данного свойства

itemList.Add(new PropertyValueUIItem(img,

new PropertyValueUIItemInvokeHandler(

this.MyUIHandlerInvoke), "Bindable Property") );

}

}

// обработчик нашего встроенного элемента

private void MyUIHandlerInvoke(ITypeDescriptorContext context,

PropertyDescriptor propDesc, PropertyValueUIItem item)

{

MessageBox.Show("Clicked: " + propDesc.DisplayName

+ " on " + context.Instance.ToString());

}

Если вам нужно передавать дополнительные данные в обработчик щелчка по картинке, можно создать класс, производный от PropertyValueUIItem, и передавать дополнительные данные через него. Также не забывайте вовремя удалять обработчик, используя IPropertyValueUIService.RemovePropertyValueUIHandler.

Улучшение компонента

Большинство компонентов и контролов создаются для решения некоторой конкретной задачи в некотором конкретном приложении. Если же вы создаете компоненты на продажу или как open-source и рассчитываете на признание его в мире .NET, то необходимо учесть еще некоторые аспекты. В этой части кратко рассказано о трёх из них – локализации, источниках данных и поддержке людей с ограниченными возможностями.