Некоторые вопросы создания компонентов в C#.NET

Безусловно, в .NET Framework много стандартных компонентов и разнообразных вариантов их настройки. Тем не менее, часто возникает необходимость добавить к ним некоторую функциональность или написать новый компонент. Не буду утомлять Вас излишними подробностями, а сосредоточусь на некоторых моментах, представляющих интерес при первом знакомстве с данной темой.

Своя пиктограмма

Это, конечно, дело вкуса, но приятней смотреть на собственную пиктограмму на панели инструментов (Toolbox), чем на шестеренку (пусть даже красиво нарисованную). Как это сделать? Очень просто - следуя документации ([1]), задать классу атрибут ToolboxBitmap. У этого атрибута есть три варианта инициализации, причем первый, использующий путь к файлу, использовать не следует (причина банальная, но весомая - файл с пиктограммой, в этом случае, не будет являться частью сборки).

Все было бы действительно просто, если бы не один нюанс. Рассказываю историю нескольких невеселых часов моей жизни. Создал я проект (Class Library), поменял чуть-чуть название файла, пространства имен и самого класса. После чего унаследовал класс от ListView (System.Windows.Forms), нарисовал пиктограмму и добавил атрибут ToolboxBitmap. Получилось приблизительно следующее:

namespace Windows.Controls
{
[ToolboxBitmap(typeof(CListView), "ListView.ico")]
public class CListView : ListView

Скомпилировал я сборку, добавил компонент в панель инструментов и весьма удивился, увидев до боли знакомую шестеренку. Потратив не один час на попытки добиться нужного результата, я начал склоняться к мысли, что ошибка на самом деле в ДНК. Выпив чаю и немного успокоившись, я решил посмотреть внимательней на ресурсы в моей сборке (это легко сделать с помощью метода Assembly.GetManifestResourceNames или утилиты ILDasm). Результат оправдал ожидания - оказалось, что соответствующий ресурс называется "Controls.ListView.ico", а студия, конечно же, искала " Windows.Controls.ListView.ico". После этого проблема решилась за пару минут, установкой свойства проекта "Default Namespace".

Последовательность действий при добавлении своей пиктограммы к компоненту выглядит так:

  1. Добавить иконку в проект как "embedded resource".
  2. Задать классу атрибут "ToolboxBitmap".
  3. Удостовериться, что свойство проекта "Default Namespace" соответствует пространству имен искомого класса.
  4. Перекомпилировать проект.
Отмечу также, что, поместив пиктограммы в отдельную папку проекта, нужно добавить к названию файла имя папки через точку (например "icons.ListView.ico"). 

Создание редактора типов (TypeEditor)

Несомненно, без визуального редактирования компонентов написание приложений существенно замедлилось бы. В .NET есть набор стандартных редакторов типов, который, однако, в некоторых ситуациях нуждается в расширении. Хорошим примером такой ситуации является реализация в собственном элементе управления свойства DataSource. В документации ([1]) описано создание своего редактора типов, но некоторые моменты освещены недостаточно подробно. Постараюсь восполнить этот пробел.

Прежде всего, следует написать "Конвертор типов" (производный класс от TypeConverter), для того, чтобы значение свойства отображалось надлежащим образом. Если не усложнять себе жизнь, то можно реализовать лишь преобразование нужных типов (DataTable,..) в строку. Обычно для этого используется свойство Component.Site.Name.

Затем, нужно унаследовать свой класс от UITypeEditor и, в зависимости от нужной функциональности при редактировании свойства, в методе GetEditStyle возвратить соответствующий UITypeEditorEditStyle. Основной метод редактора типов - EditValue - вызывается при попытке пользователя (редактора типов) изменить значение свойства. В нем, с помощью сервиса IWindowsFormsEditorService, можно показать выпадающий список возможных значений свойств или форму. Я лично предпочитаю выпадающий список, поскольку работать с ним быстрее. Отмечу также, что IWindowsFormsEditorService позволяет показывать не только стандартный ListBox, но и любой другой (в том числе пользовательский) элемент управления. Для наполнения списка обычно достаточно перечислить компоненты на форме и добавить в список подходящие (например, в зависимости от их типа). Сделать это можно с помощью параметра "ITypeDescriptorContext context":

foreach( IComponent component in context.Container.Components )
{

}

После завершения реализации редактора типов, нужно задать у свойства соответствующие атрибуты (TypeConverter, Editor и, при желании, Category, DefaultValue, Description и т.п.):

[Category("Data"),
TypeConverter(typeof(Windows.Controls.CDataSourceValueConverter)),
Editor(typeof(Windows.Controls.CDataSourceTypeEditor),
typeof(UITypeEditor)),
DefaultValue(null),
Description("Источник данных.")]
public object DataSource

Редактирование коллекций

Для редактирования своих коллекций писать свой редактор типов необязательно. Стандартный редактор (CollectionEditor) предоставляет вполне приемлемую функциональность. Он определяет тип содержащихся в коллекции объектов с помощью свойства Item (к примеру, индексатора в C#). Все что нужно - это написать класс, реализующий интерфейс ICollection (можно использовать класс "System.Collections.CollectionBase"). Затем для свойства задать атрибут ReadOnly(true), поскольку свойство, по идее, не должно меняться и DesignerSerializationVisibility(DesignerSerializationVisibility.Content) для правильной сериализации коллекции.

Регистрация компонента для вкладки ".NET Framework Components"

При добавлении компонента в панель инструментов можно пойти двумя путями: выбрать компонент из списка или выбрать файл компонента. При частом использовании одних и тех же редко редактируемых компонентов проще использовать первый вариант. Для этого нужно путь к папке, содержащую сборку (сборки) с компонентами, поместить в ключ реестра "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NETFramework\AssemblyFolders". После этого - перезапустить студию.

Использование Win32 API

Ни для кого не секрет, что управляющие элементы в .NET предоставляют доступ далеко не ко всем своим свойствам. Координаты заголовка столбца в ListView, к примеру, получить крайне затруднительно. С другой стороны, программируя с использованием Win32 API, сделать это проще простого - получить окно заголовка (событие LVM_GETHEADER) и взять у него координаты заголовка по индексу (событие HDM_GETITEMRECT). Пользоваться Win32 API несложно ([2]), нужно лишь объявить нужные функции с помощью атрибута DllImport:

[DllImport("user32.dll", CharSet=CharSet.Auto)]
public static extern IntPtr SendMessage(IntPtr hWnd,
int msg, int wParam, int lParam);

Некоторую проблему представляет лишь работа со структурами (атрибут StructLayout). Прочитав этот раздел, Вы можете подумать, что я призываю пользоваться Win32 API - поверьте, это не так. Напротив - я сам стараюсь этого избежать. Основной недостаток использования Win32 API - ограничение круга операционных систем, на которых может быть внедрено решение. Более того, встречаются ситуации, когда одни и те же API-функции ведут себя по-разному на разных Windows-платформах. Но если Ваше приложение должно работать только на одной конкретной операционной системе, использование Win32 API вполне допустимо (при отсутствии аналогов в .NET).

Наследование управляющих элементов

Наследование управляющих элементов - тема слишком широкая для одной статьи, поэтому выскажу лишь некоторые соображения по этому поводу. Вопрос о том, когда и как использовать наследование, достаточно хорошо освещен в документации ([3], [4]). От себя добавлю, что наследование элементов управления весьма полезно, когда есть свой каркас приложения, решающий частные задачи и не претендующий на универсальность. В этом случае можно ориентировать элементы управления на этот каркас, что поможет в дальнейшем ускорить разработку приложений.

И еще одно замечание, по поводу визуального редактирования. Иногда необходимо немного изменить функциональность некоторого свойства базового элемента управления, которое не объявлено как virtual. Например, хочется ограничить возможные значения этого свойства. Решить такую задачу можно с помощью модификатора new. Конечно, при этом останется возможность доступа к этому свойству извне (с помощью приведения типа к базовому классу). Но, с другой стороны, при визуальном проектировании этого не случится, а необходимость приведения типа управляющего элемента к базовому элементу чаще всего весьма сомнительна. 

Заключение

Создание компонент, как и программирование в целом - процесс творческий и каждый решает сам как именно это делать. Надеюсь, что эта статья поможет Вам на первых этапах разработки.