← Ноябрь 2004 → | ||||||
1
|
2
|
3
|
4
|
5
|
6
|
7
|
---|---|---|---|---|---|---|
8
|
9
|
10
|
11
|
12
|
13
|
14
|
15
|
16
|
17
|
18
|
19
|
20
|
21
|
22
|
23
|
24
|
25
|
27
|
28
|
|
29
|
30
|
За последние 60 дней 2 выпусков (1-2 раза в 2 месяца)
Сайт рассылки:
http://rsdn.ru
Открыта:
14-06-2000
Статистика
-5 за неделю
Программирование на Visual С++ No.105 .Net - классы, компоненты и контролы
Информационный Канал Subscribe.Ru |
РАССЫЛКА САЙТА
RSDN.RU |
No. 105 /
2004-11-26 Подписчиков: 27061 |
РАССЫЛКА ЯВЛЯЕТСЯ ЧАСТЬЮ ПРОЕКТА RSDN, НА САЙТЕ КОТОРОГО ВСЕГДА МОЖНО НАЙТИ ВСЮ НЕОБХОДИМУЮ РАЗРАБОТЧИКУ ИНФОРМАЦИЮ, СТАТЬИ, ФОРУМЫ, РЕСУРСЫ, АРХИВ ВЫПУСКОВ РАССЫЛКИ И МНОГОЕ ДРУГОЕ. |
.Net - классы, компоненты и контролы Введение Такой метод создания ПО, как компонентное программирование, появился относительно недавно. Его можно охарактеризовать как технологию создания ПО из готовых блоков. Т.е. программисты пытаются украсть идею у строителей, занимающихся крупнопанельным домостроением. Создание ПО из компонентов подразумевает, что компоненты будут добавляться к проекту во время разработки. При этом будет производиться их начальная настройка. Компоненты как таковые не подразумевают (вернее сказать, не обязаны иметь) пользовательского интерфейса (ни для программиста, ни для конечного пользователя). В этом качестве выступают части IDE и дополнительные программные дизайнеры. Первой компонентной средой был продукт, купленный Microsoft на заре своего существования. Впоследствии на его базе родился VB. Далее была Delphi... в общем, к концу двадцатого века компоненты стали поддерживаться почти везде (даже в Visual C++, хотя он и по сей день не очень-то визуальный). Отдельным классом компонентов стали контролы. По сути контрол - это тот же компонент, но имеющий свой GUI, и предназначенный для сборки пользовательского интерфейса. Продукты Microsoft уже давно поддерживают компонентные технологии, но .NET Framework и VS.NET стали действительно революционными в этом отношении. Для того чтобы ответить на вопрос "в чем собственно революция?" нужно сначала разобраться, что Microsoft вкладывает в слова компонент и класс. Компоненты Для начала дадим определение класса. Класс - это тип данных, определяемый программистом, и содержащий данные (в виде полей и констант) и поведение (методы, свойства, события и другие элементы, которые могут содержать код). Классы поддерживают наследование, инкапсуляцию и полиморфизм, т.е. позволяют писать универсальный легко расширяемый код. Теперь дадим определение компонента. Компонент - это объединенные в отчуждаемую форму исполняемый бинарный код и данные, которые могут использоваться для построения программных систем. Отчуждаемость подразумевает возможность использования компонента без дополнительных знаний о нем. На практике это означает, что компонент сам должен содержать сведения о себе. Компонент должен также иметь внешний (публичный) интерфейс. Интерфейс является как бы механизмом, через который можно запустить находящийся внутри компонента код. Отчуждаемость также означает, что экземпляр компонента может быть создан динамически, и что для этого не обязательно использовать всякого рода компиляторы и интерпретаторы. По сути, компонент - это класс, предоставляющий информацию о себе (метаинформацию), экземпляры которого можно создавать динамически (не имея никакой статической информации о нем). Интересно, что практически любой класс в .NET отвечает этим требованиям. Метаинформация создаются для любого элемента класса (будь он трижды скрытым), экземпляр любого класса можно динамически создать, и любой класс помещается в сборки (один или более исполнимых модулей), которые можно распространять независимо. В общем, можно было бы спокойно сказать, что любой класс в .NET - это компонент, если бы... Если бы не наличие отдельного класса с именем Component. Класс Component и интерфейс IComponent Класс Component является стандартной реализацией интерфейса IComponent, а интерфейс IComponent указывает на то, что реализующий его класс является компонентом. Зачем, спросите вы? Ведь, по сути, в .NET любой класс является компонентом! Все очень просто. Хотя это и не обязательно, но обычно, когда говорят о компонентах, подразумевают, что экземпляры класса будут использоваться в той или иной визуальной среде разработки (RAD). При этом для тесной интеграции компонента с IDE ему может понадобиться обратная связь с этой самой IDE. Такая обратная связь предоставляется через так называемый сайт. Сайт - это некоторая абстракция, ассоциируемая с каждым компонентом и дополняющая компонент специфической для контейнера информацией, например, такой, как имя компонента (в VS.NET у компонента во время исполнения имени нет, оно есть только во время разработки), а также обеспечивающая обратную связь с контейнером. В .NET сайт реализуется через интерфейс ISite. ISite назначается компоненту через свойство IComponent.Site. IComponent к тому же происходит от интерфейса IDisposable, который отвечает за освобождение unmanaged-ресурсов. Но это скорее уже дополнительная функция, которая, хотя и часто встречается, необходимой не является. Создать компонент очень просто. Нужно создать сборку (в виде DLL, в визарде проектов VS.NET нужно выбрать проект "Class Library"), в которую поместить класс, унаследованный от Component (который также можно создать с помощью визарда - если в контекстном меню проекта выбрать "Add->Add New Item" и затем выбрать "Component Class") или IComponent. Вот простой вариант (на C#):
Данный код создан визардом, хотя ничто не мешает создать его вручную. Код компонента можно писать вручную, а можно пользоваться визуальным дизайнером, который позволит избавиться от части рутинного труда. Дизайнер позволяет помещать на рабочую поверхность другие компоненты и настраивать их свойства с помощью редактора свойств (см. рисунок 1).
Самое интересное, что сериализация компонентов (в том числе и в дизайнере самого компонента) по возможности осуществляется в код. Так, если открыть дизайнер компонента (двойным щелчком на соответствующем файле в "Solution Explorer", см. рисунок 1) и бросить на его поверхность компонент ImageList (из Toolbox, палитры компонентов), то в описание компонента будет добавлена строка, описывающая переменную-член, описывающую добавленный ImageList:
А в метод InitializeComponent - инициализация этого компонента:
"По возможности" - означает, что то или иное свойство не удается превратить в код, содержимое свойства сериализуется в поток (Stream) который записывается в ресурсы. В код при этом вставляется вызов функции, загружающий объект из ресурсов. Для сериализации в Stream объект должен или быть помечен атрибутом Serializable или реализовать интерфейс ISerializable. Так, если выделить брошенный нами ImageList и в его свойствах выбрать свойство Images, нажать на появившуюся кнопку и добавить картинки, то в InitializeComponent будет добавлено еще две строчки:
Первая создает и инициализирует вспомогательный объект - ResourceManager. Вторая загружает (из ресурсов) экземпляр ImageListStreamer и помещает получившееся значение в свойство ImageStream компонента ImageList. Сам поток сохраняется в ресурсы дизайнером (этот код скрыт в недрах VS.NET). Таким образом, появляется код, который после компиляции может создать нужные экземпляры объектов и проинициализировать их состояние. Сам дизайнер (компонентов, форм или любой иной) загружает состояние создаваемого компонента или формы путем компиляции кода инициализации. Для упрощения своей задачи VS помечает генерируемый и компилируемый в дизайн-тайме код специальной директивой препроцессора #region (и именем региона "Component Designer generated code"). VS честно пытается скомпилировать код, находящийся внутри InitializeComponent, но (при генерации кода) безжалостно затирает все содержимое этого метода. Можете произвести следственный эксперимент. Замените строку:
На:
Сохраните файл. Закройте дизайнер. Откройте его еще раз. Сдвиньте ImageList. Это нужно для того, чтобы дизайнер понял, что состояние компонента изменилось, и его нужно снова сериализовать в код. Переключитесь в код... И вы увидите, что строка, задающая имя ресурса, снова изменилась на "imageList1.ImageStream". Для пущей визуальности можно разбить окно VS на два и поместить код и дизайнер компонента в разные области (это делается путем перетаскивания закладки в центр редактируемой области). Если не редактировать ничего в дизайнере компонента, можно скомпилировать проект и убедиться (под отладчиком) что в runtime прекрасно работает код, занимающийся конкатенацией. Как альтернативу этому эксперименту можно предложить описать и инициализировать переменную внутри InitializeComponent. Если переменная будет простого типа, то она просто бесследно исчезнет (после первой записи содержимого дизайнера). Если же объявить компонент, то он появится в дизайнере (при его активизации) и превратится в член класса при первом же изменении кода (вернее, при первой же сериализации дизайнера компонента в код). Все это очень похоже на сериализацию компонентов и контролов в таких RAD-средах, как Delphi и VB. За тем лишь исключением, что VB и Delphi сериализуют компоненты в специальные структурированные хранилища, имеющие синтаксис, очень похожий на код, а VS.NET генерирует стопроцентный код! Попробуйте произвести какие-либо расчеты в *.dfm или *.frm файлах внутри инициализации компонентов. Красноречию дизайнеров не будет предела. В том, что это спокойно пройдет в VS.NET, мы уже убедились. CodeDom Как же устроена генерация и компиляция кода в VS.NET? Сможет ли простой смертный познать тайну мудрецов, писавших эту среду? И можно ли заставить эти механизмы работать без VS.NET? Да и как VS.NET умудряется генерировать и компилировать код для одного и того же компонента под такие разные языки, как VB.NET и C#? Дело в том, что генерация и дизайнтайм-компиляция кода осуществляются VS не напрямую, а с помощью механизма называемого CodeDom. CodeDom - это нечто вроде XmlDom-парсера, но не для XML, а для кода. В принципе, это и есть ответ на вопросы из предыдущего абзаца, но думаю, что вы потребуете более глубокого объяснения. Не так ли? :) Пожалуйста... Спецификация CLI (Common Language Infrastructure) определяет набор требований к языкам программирования. Эти ограничения позволяют создать универсальные генераторы кода под языки, совместимые с CLI. Для того, чтобы VS.NET поняла язык и смогла создавать на нем формы и ASP.NET-страницы, нужно, чтобы для этого языка был реализован CodeDom-провайдер. Microsoft на сегодня поставляет два CodeDom-провайдера:
Имеется также ряд провайдеров от независимых поставщиков. А в ближайшем будущем Microsoft планирует выпустить VS.NET 1.1 (а соответственно, и .NET SDK 1.1) в который также войдет провайдер для Java (вернее J#) и, скорее всего, долгожданный провайдер для C++. Дизайнер получает указатель на интерфейс ICodeGenerator, которому можно передать абстрактную структуру, состоящую из отдельных веток (каждая из этих веток представляет атомарный участок кода), и превращает его в текст на некотором языке (зависящем от выбранного провайдера). Эти сервисы документированы (хотя и не лучшим образом). Ну и главное, что реализации провайдеров для VB.NET и C# поставляются в составе .NET-рантайм (даже не SDK!). Документированность и открытость этих сервисов и интерфейсов означает, что (теоретически) любой программист может создать свой провайдер или использовать чужой. Причем ни в том, ни в другом случае (опять же теоретически) программист не будет зависеть он конкретного языка. Пример генерации и компиляции кода через CodeDom Вот пример генерирующий код простого приложения типа "Hello World!" :):
Этот код генерирует исходный код (уж простите за каламбур) на C# и VB.NET. Он также компилирует исполняемый модуль test.exe. Вот полученные исходные коды: (VB)
(C#)
Нетрудно заметить, что для описания выражений используются независимые от языка конструкции. Эти конструкции формируют дерево выражений, описывающих алгоритмы и типы данных. Из этого дерева можно как генерировать исходные тексты на конкретном языке, так и компилировать исполняемые молули. Логика работы сериализатора VS.NET Как вы, видимо, уже поняли, рантайм-сериализация и CodeDom-сериализация - это разные вещи. Но пока вы вряд ли сможете оценить, насколько это разные вещи! CodeDom-сериализация производится в два этапа. На первом создается "дерево кода", а на втором это дерево превращается в текст на необходимом языке. Во вторую часть процесса вмешаться невозможно (и не нужно), а вот в первую можно. Итак, каков же алгоритм? VS.NET:
При открытии разрабатываемого компонента среда просто находит нужный участок кода и запускает CodeDom-компилятор для компиляции кода инициализации. После этого с помощью скомпилированного кода она создает экземпляр компонента. Далее работа ведется только с этим экземпляром. Компонент (через свойство Site) может оповестить VS.NET, что он был изменен. При этом VS.NET помечает компонент как измененный, и перегенерирует код по описанной выше схеме. Таким образом, главным хранилищем данных является код. Внимательный читатель может заметить, что не все свойства компонента являются компонентами. Да и не совсем понятно, что происходит с компонентами, на которые ссылается основной компонент. Сериализация простых классов Итак, для типов, реализующих IComponent, сериализация производится автоматически. Но компоненты могут иметь свойства типа структур или классов, которые не реализуют IComponent. Как быть в этом случае? В принципе неясно, почему Microsoft ввела такое ограничение, но, к сожалению, это так. Для обхода своего же ограничения и для увеличения гибкости была придумана идеология конвертера типов. Когда VS.NET пытается сериализовать свойство, тип которого она сериализовать не умеет, она пытается получить у этого типа (через рефлексию) значение атрибута TypeConverter. Этот атрибут указывает (или в строковом виде или в виде ссылки на тип) на класс, унаследованный от класса TypeConverter, и умеющий преобразовывать объекты искомого типа в некоторый другой тип. TypeConverter - это универсальная технология, позволяющая преобразовывать типы, но при сериализации VS.NET интересует только преобразование между исходным типом и InstanceDescriptor. InstanceDescriptor позволяет описать, как создать конкретный экземпляр объекта. То есть InstanceDescriptor содержит информацию, описывающую экземпляр. С его помощью VS.NET генерирует CodeDom-код конструктора. В конечном счете, VS.NET получает (в текстовом виде) код конструктора. В итоге в описание формы (или разрабатываемого компонента) попадает примерно такой код:
Конструкторы для классов Font и Point были сгенерированны с помощью InstanceDescriptor, специально для этого написанным наследником от TypeConverter. Наследник TypeConverter должен реализовать два метода:
и
Первый должен ответить на вопрос, может ли данный TypeConverter совершить требуемое преобразование:
Второй собственно и занимается преобразованием:
В задачи TypeConverter при преобразовании объекта в InstanceDescriptor входит выбор подходящего описания конструктора и заполнение списка значений его параметров. Эти значения берутся из сериализуемого объекта. Эта информация передается в InstanceDescriptor через его конструктор. Описание конструктора передается в виде экземпляра класса MemberInfo. Его можно получить с помощью метода GetConstructor объекта Type, описывающего сериализуемый тип. Этому методу нужно передать массив, содержащий перечисление типов параметров конструктора. Вся сложность заключается в том, чтобы проанализировать содержимое экземпляра и подобрать наиболее подходящий конструктор, сформировав для него список параметров. Задача усложняется тем, что количество вариаций параметров конструктора сериализуемого типа может быть довольно велико. Нужно нагородить кучу if-ов. Нагромождение это может быть довольно большим, так как каждый if содержит громоздкий код формирования двух массивов (со списком параметров и с описанием типов параметров). Для упрощения жизни я создал небольшой класс-помощник:
Этот класс создан на C++, так как мне нужно было использовать его в MC++-проекте. Но его несложно переписать на C#. Основная его идея заключатся в том, что он позволяет добавлять описания параметры простым вызовом метода Add вместо манипуляций с двумя массивами. Вот как можно использовать этот класс-помощник:
Этот пример (написанный также на MC++) взят из кода контрола TreeGrid, который можно найти на прилагаемом к журналу CD. В нем производится преобразование объекта Column (колонка) в InstanceDescriptor. С использованием InstanceDescriptorBuilder эта операция становится довольно простой. В приведенном примере производится анализ свойств объекта Column и, если они содержат значения, отличные от значений по умолчанию, в конструктор добавляется соответствующий параметр . В конце этого действа у InstanceDescriptorBuilder-а вызывается метод ToInstanceDescriptor, создающий и возвращающий InstanceDescriptor, описывающий сериализуемый объект. Получив InstanceDescriptor VS.NET, сериализуем все параметры конструктора, которые были описаны в InstanceDescriptor. Процесс этот по своей природе рекурсивен. Так что если в качестве параметра конструктора окажется тип, реализующий TypeConverter, операция будет повторена для него. Таким образом, получается список вложенных конструкторов. Код может быть сложнее, если в процессе сериализации была применена ручная CodDom-сериализация. Атрибут TypeConverter Чтобы VS.NET могла узнать о том, что с типом ассоциирован конвертер, это нужно указать с помощью атрибута TypeConverterAttribute (или в сокращенной форме TypeConverter):
Управление сериализацией отдельных свойств компонента Итак, компоненты сериализуются в код автоматически. Но что делать, если некоторые свойства компонента сериализовать не нужно? Для управления сериализацией отдельных свойств используется атрибут DesignerSerializationVisibilityAttribute (в сокращенной форме DesignerSerializationVisibility). Этот атрибут может принимать значения из перечисления DesignerSerializationVisibility:
Visible - сделан для эстетического разнообразия, так как по умолчанию (если не указывать этот атрибут вообще) все свойства и так видны сериализатору. Hidden - позволяет скрыть свойство от сериализатора. Помеченные этим атрибутом свойства сериализоваться не будет. Content - говорит сериализатору, что свойство должно сериализоваться "по содержимому". Это значение применяется для сериализации коллекций. Чтобы сериализовать свойство по содержимому, нужно, чтобы возвращаемый им объект реализовывал один из следующих методов:
Кроме этого объект должен реализовать интерфейсы IList и ICollection. Методы AddRange и Add определяются по имени и не имеют никакого отношения к этим интерфейсам. Вот код, формируемый VS.NET при сериализации свойства MenuCommands компонента MenuCommand:
Сами MenuCommand создаются и настраиваются как отдельные компоненты. Зачем оно надо? Многие могут задаться вопросом, а зачем вообще нужна сериализация в код? В конце концов, это ведь приводит к существенному замедлению работы дизайнера. Многие дизайнеры прошлого поколения производили сериализацию в поток. Это давало довольно быструю загрузку приложения, что может только приветствоваться во время выполнения программы, но приводило к серьезным проблемам во время разработки. Дело в том, что в процессе разработки зачастую приходится переходить на новые версии компонентов. При этом сериализованное состояние компонентов может различаться, что приведет к тому, что компонент попросту не сможет загрузить старое состояние. Это происходит из-за того, что в потоке обычно не хранится формат записанных данных. Вместо этого компонент, загружая данные, самостоятельно определяет, что и в какой последовательности должно идти. В принципе, если сериализация осуществляется вручную, можно предпринять действия, позволяющие загрузить состояние из старых версий потоков, но при этом появляются дополнительные проверки и, как следствие, непроизводительные затраты. К тому же это довольно сложно и почти всегда приводит к нагромождению специфичного кода. Ну и, естественно, бинарный поток нельзя подправить вручную. Для обхода этих проблем были придуманы более высокоуровневые технологии сериализации, такие, как property bag в COM/VB или DFM в Delphi. Эти технологии предполагали сериализацию отдельных свойств компонента. За счет этого добавление свойств в компонент уже не вызывало особых проблем. Так как при загрузке со старой версии, в которой, например, отсутствует то или иное свойство, компоненту попросту не предлагается загрузить его состояние. При этом компонент попросту использовал для этого свойства значение по умолчанию. Естественно, что при следующей сериализации значение этого свойства сохранялось и никаких проблем не возникало. Вторым достоинством этого способа было то, что обычно сериализация производилась или непосредственно в текст (как в VB и последних версиях Delphi), или в структурированный бинарный вид (как в ранних версиях Delphi), который (с помощью дополнительной утилиты) можно было превратить в текстовую форму (и обратно). Текстовое представление позволяло в крайнем случае подправить что-нибудь руками. Собственно порча данных может произойти и без смены версий компонентов. Ну и, в конце концов, хранение состояния компонентов в текстовом виде позволяло делать генераторы кода и более удобно работать с версиями проекта. Системы контроля кода значительно лучше работают с текстовыми файлами, нежели с бинарными. Например, достаточно просто сравнить разные версии текстового файла. Но у этого способа есть один существенный недостаток. Дело в том, что скорость загрузки состояния объекта была довольно низкой. В дизайнтайме это еще терпимо, но терять время в рантайме - это слишком большое расточительство. Чтобы совместить высокую скорость загрузки компонентов в рантайме с удобством работы и хранения состояния компонентов во время разработки, способы сериализации для рантайма и дизайнтайма различались. В рантайме (т.е. при создании exe- или dll-модуля) состояние компонентов сохранялось в бинарный поток, а в дизайтайме в текстовый формат типа property bag или DFM. Это часто приводило к проблемам. Так, многие COM-дизайнеры, в том числе и VB, при сериализации в property bag поддерживали только значительно урезанную функциональность. Сериализация в код позволяет избавиться от двойственности при сохранении состояния компонентов. Компоненты всегда сериализуются в единый формат. При создании приложения происходит компиляция в код, так что в конечном приложении нет никаких потоков. Загрузка формы происходит путем компиляции загрузочного кода. При этом, если у компонента появилось новое свойство, оно попросту не будет инициализировано (что приведет к использованию настроек по умолчанию). Более того, в конечном приложении отсутствует блок интерпретации сериализованного состояния, что приводит к ускорению загрузки приложения и уменьшению его размеров. Как и в случае с property bag, код загрузки можно эффективно хранить в системах контроля версий и при необходимости изменять вручную. Более того, появляется возможность компилировать проекты, созданные в VS.NET, в отсутствие VS.NET. Ну, и поскольку генерируемый код - это просто код на целевом языке, отпадает необходимость изучать дополнительные форматы. В общем, одни плюсы, за исключением тормозов, возникающих при открытии форм в дизайнере. Но с ускорением процессоров и оттачиванием алгоритмов загрузки в последующих версиях VS.NET эта проблема должна исчезнуть. Ручная сериализация Сериализацией можно непосредственно управлять. Для этого нужно создать наследника CodeDomSerializer и переопределить его методы Serialize и Deserialize. Этого наследника нужно подключить к классу, для которого нужна ручная сериализация. Делается это с помощью атрибута DesignerSerializer (или RootDesignerSerializer). Я не буду в этой статье подробно рассматривать вопрос ручной сериализации, так как он заслуживает отдельной статьи. Скажу только, что с помощью ручной сериализации можно добиться довольно интересных эффектов. Если вас интересует эта тема, могу посоветовать посмотреть статью ".NET Shape Library: A Sample Designer" (http://gotdotnet.com/team/windowsforms/shapedesigner.aspx), в которой приводится пример работы с этой техникой. Свойство Site При загрузке компонента под управлением VS.NET в свойство Site помещается ссылка на интерфейс ISite. Это мало чем примечательный интерфейс. Вот список входящих в него свойств и методов:
Component - возвращает компонент, ассоциированный с сайтом.
Container - возвращает контейнер, в котором находится компонент.
DesignMode - говорит, находится ли компонент в режиме разработки, то есть, загружен компонент в дизайнере типа VS.NET, или что компонент создан кодом приложения. В принципе, совершенно бесполезное свойство, так как когда компонент находится в рантайме, ему попросту не подключают site. А стало быть, проверить данное свойство совершенно невозможно.
Свойство Name позволяет установить или считать имя компонента. Container - возвращает ссылку на интерфейс IContainer, ассоциированный с сайтом. С помощью метода Components этого интерфейса можно добраться до формы и других компонентов и контролов. Например, следующий код выведет имена всех компонентов, лежащих на форме.
В ISite, кроме документированных свойств, входит не упомянутый в документации метод GetService.
Функциональность интерфейса ISite, честно говоря, невелика, но этот метод расширяет ее до воистину потрясающих размеров. Дело в том, что GetService - это своеобразная замена QueryInterface из COM. Вот пример использования этого метода:
С помощью этой функции можно запрашивать дополнительные интерфейсы. К сожалению, господа из Microsoft не удосужились привести полный список доступных через этот метод интерфейсов. Но с помощью Анакрино, отладчика и какой-то матери мне удалось получить этот список:
Свойства Еще со времен VB 1.0 настройка состояния компонентов во время разработки производится через настройку свойств компонента. Собственно, для этого они и были придуманы. Как и в те незапамятные времена, настройка осуществляется через универсальный редактор свойств. В VS.NET он получил имя PropertyGrid. PropertyGrid в VS.NET только внешне похож на свой прототип из VB 1.0. Теперь его возможности могут быть расширены, причем довольно гибко. Но, в конце концов, гибкий редактор свойств уже не в новинку. В VB 6, и тем более в Delphi 7, редакторы свойств тоже предоставляли интерфейс расширения. А вот доступность редактора свойств в виде отчуждаемого элемента управления - это действительно оригинально! Причем PropertyGrid доступен даже на машинах, где не установлена VS.NET. Достаточно .NET Runtime. PropertyGrid можно даже использовать не по назначению. Так в диалогах конфигурации VS.NET используется именно PropertyGrid. Об опыте применения PropertyGrid в качестве средства конфигурации приложения рассказывается в статье Андрея Корявченко "Конфигурирование .NET-приложений" в этом номере журнала. Интересно, что расширение возможностей PropertyGrid сделано не непосредственно, а через расширение настроек компонентов и подключение так называемых дизайнеров (см. ниже). Так, с помощью атрибутов можно задать описание свойства, категорию, к которой принадлежит свойство, и многое другое. Вот список атрибутов, применимых к свойствам и классу:
Иконка компонента Задать иконку для компонента можно с помощью атрибута ToolboxBitmap. В качестве иконки можно задать битмап из ресурса, или находящийся на диске (по относительному пути). Чтобы задать компонент из ресурса, в конструкторе атрибута нужно указать имя битмапа и любой тип, расположенный в том же пространстве имен и в той же сборке. Например:
При этом MyComponent.BMP должен быть подключен к проекту VS.NET и его свойство "Build Action" должно быть установлено в "Embedded Resource". Но это справедливо только для C# и VB.NET. В MC++ придется извращаться более сложным образом. В MC++ нужно открыть свойства проекта, найти раздел "Linker\Command Line" и дописать туда следующий текст:
Если вы хотите использовать иконку от уже имеющегося компонента, можно воспользоваться конструктором, задающим только тип. Например, следующий код задает компоненту иконку от контрола "Button" (кнопка):
Если нужно задать в качестве иконки битмап, находящийся на диске, нужно воспользоваться конструктором с единственным параметром строкового типа:
Дизайнер компонентов Любой компонент (в том числе контролы и формы) может иметь свой дизайнер. Дизайнер - это класс, предназначенный для настройки компонента во время разработки. Чтобы VS.NET могла определить, что для некоторого компонента нужно подключить дополнительный дизайнер, нужно установить этому компоненту атрибут System.ComponentModel.DesignerAttribute:
Дизайнер компонентов должен как минимум реализовать интерфейс System.ComponentModel.Design.IDesigner:
Вызывается VS.NET при подключении дизайнера к компоненту. В параметре component указывается компонент, к которому подключается дизайнер.
Определяет, как компонент будет вести себя при двойном щелчке по нему левой кнопкой мыши. Обычно при этом создается обработчик самого часто используемого события. Но можно, например, открыть диалог настройки компонента.
Через это свойство дизайнер должен возвратить ссылку на компонент, полученный в методе Initialize.
Возвращает список так называемых действий (Verbs), используемых для формирования контекстного меню компонента. Действие представляется классом System.ComponentModel.Design.DesignerVerb, позволяющим задать свойства меню (такие, как Text, Visible, Checked), а также обработчик события выбора пункта описываемого действием. Интерфейс IDesigner можно реализовать самостоятельно, но еще проще воспользоваться готовой реализацией - классом ComponentDesigner. Кроме вышеуказанного интерфейса он реализует еще интерфейс System.ComponentModel.Design.IDesignerFilter. Этот интерфейс позволяет виртуально (на время разработки) изменять состав атрибутов, событий и, что самое важное, свойств объекта. Виртуальные и измененные свойства/события/атрибуты будут видны в редакторе свойств, и их можно будет настраивать так, как будто они настоящие. Вот список методов этого интерфейса:
Pre-методы позволяют добавить свои (виртуальные) свойства/события/атрибуты к уже имеющимся. Post-методы позволяют удалить или изменить описание свойств/событий/атрибутов компонента. Таким образом можно делать совершенно неописуемые надругательства над любимым компонентом. Например, вы можете создать у своего компонента свойство некоего хитрого типа, позволяющего упростить настройку компонента, а более низкоуровневые свойства скрыть. Следующий код демонстрирует реализацию дизайнера компонентов, добавляющий пункт в контекстное меню, и виртуальное свойство, доступное только во время разработки:
В данном примере виртуальное свойство не делает никакой полезной работы, просто изменяя значение реально имеющегося свойства "Test". Подключить дизайнер к компоненту можно следующим образом:
MarshalByValueComponent Обычно или вообще отсутствует необходимость маршалинга компонентов (особенно контролов) в другой AppDomain, или нужно производить маршалинг по ссылке. Но иногда нужно, чтобы маршалинг компонента проходил по значению. Для этого компонент нужно наследовать от типа MarshalByValueComponent. Редактор типов (UITypeEditor) Когда вы выбираете свойство в PropertyGrid, в правой его части появляется текстовое поле, позволяющее ввести значение, или кнопка, позволяющая открыть выпадающее окно или диалог. Это выпадающее окно или диалог позволяют визуально настроить значение свойства. Для простых типов вроде строки или целого используется встроенный редактор (который и выглядит как текстовое поле). Для сложных же можно создать свой редактор. Такой редактор должен быть унаследован от UITypeEditor и ассоциирован с некоторым свойством или типом через атрибут Editor. Естественно, что если редактор ассоциирован с типом, этот редактор будет появляться у всех свойств этого типа. Вместе с VS.NET поставляется множество готовых редакторов, причем некоторые из них расширяемы. Например, FileNameEditor позволяет создать редактор, открывающий диалог выбора файла. Причем у этого класса можно переопределить метод InitializeDialog:
в котором можно произвести тонкую настройку диалога выбора файла. FolderNameEditor позволяет выбирать путь к некоторому каталогу. В VS.NET имеется еще множество редакторов типов. Но в основном они предназначены для редактирования конкретных типов, уже ассоциированы с этими типами и используются автоматически. Собственный дизайнер создать довольно просто. Нужно всего лишь создать наследника класса System.Drawing.Design.UITypeEditor и переопределить методы:
VS.NET поддерживает два вида редакторов: модальный и выпадающий. То, какого типа должен быть ваш дизайнер, задается через возвращаемое значение метода GetEditStyle. UITypeEditorEditStyle.DropDown - означает, что ваш редактор будет выглядеть как выпадающее окно, а UITypeEditorEditStyle.Modal - модальным диалогом. В функции EditValue вы должны создать и открыть диалог или контрол (для DropDown-режима). Параметр context этого метода позволяет получить доступ к компоненту и контейнеру, а параметр provider к runtime-сервисам. Самым важным сервисом, предоставляемым через этот параметр, является IWindowsFormsEditorService. С помощью его метода DropDownControl можно показать контрол в виде выпадающего окна. В конце работы этот метод должен вернуть измененное значение (или то же самое, если пользователь решил отказаться от изменений). Вот простой редактор, позволяющий изменять многострочный текст:
Модальный редактор мало чем отличается. Единственное, что нужно сделать - это заменить вызов метода IWindowsFormsEditorService.DropDownControl на вызов IWindowsFormsEditorService.ShowDialog, ну и, естественно, вместо контрола использовать собственный диалог. Заключение В этой части статьи я рассказал о компонентах и связанных с ними технологиях. Следующая часть будет посвящена элементам управления (Control-ам) и связанным с ними технологиями. Control-ы являются наследниками компонентов, а значит, все, сказанное в этой части статьи, справедливо и для них. |
Ведущий рассылки: Алекс Jenter jenter@rsdn.ru
|
http://subscribe.ru/
http://subscribe.ru/feedback/ |
Подписан адрес: Код этой рассылки: comp.prog.visualc |
Отписаться |
В избранное | ||