четверг, 8 октября 2009 г.

.NET Labas domas

Converting a bitmap to a byte array
For .NET Framework 2.0 the following line of code (C#) seems to do the trick:
byte[] bytes = (byte[])TypeDescriptor.GetConverter(bmp).ConvertTo(bmp, typeof(byte[]));

EMF+ Metafile Record Format Documentation

http://www.aces.uiuc.edu/~jhtodd/Metafile/

Разработка собственного дизайнера форм для .NET-приложений
http://msdn.microsoft.com/ru-ru/library/dd335950.aspx

Размывание изображения при DrawImage
http://www.gotdotnet.ru/Forums/Windows/326006.aspx

Using Interpolation Mode to Control Image Quality During Scaling
http://msdn.microsoft.com/en-us/library/ms533836.aspx


Разработка собственного дизайнера форм для .NET-приложений

Автор:

  • Сейд Хашими

В течение многих лет MFC была популярной инфраструктурой для создания Windows-приложений. В MFC есть дизайнер форм, облегчающий создание форм, обработку событий и решение других задач по разработке форм. Несмотря на широкую популярность MFC часто критиковали за недостатки, большинство из которых устранено в Microsoft .NET Framework. Действительно, расширяемая архитектура Windows Forms в .NET Framework обеспечивает гораздо большую гибкость разработки, чем MFC.

Например, в Windows Forms можно перетащить свой элемент управления из окна инструментария на рабочую область дизайнера Visual Studio. Интересно, что, хотя Windows Forms ничего не известно об этом элементе управления, она может служить хостом для него и позволяет управлять его свойствами. В MFC все это невозможно.

В этой статье я расскажу о том, что происходит на внутреннем уровне, когда вы проектируете свои формы. Затем покажу, как самостоятельно разработать простейший дизайнер форм, который позволит создавать формы так же, как и дизайнер форм Visual Studio. Для этого вы должны отчетливо представлять, какая функциональность доступна в .NET Framework.

Некоторые основы Windows Forms

Прежде чем приступить к рассмотрению проекта, расскажу о некоторых базовых концепциях, которые важно усвоить. Начнем с определения дизайнера. Дизайнер задает UI и поведение компонента на этапе разработки. Например, когда вы перетаскиваете кнопку на форму, дизайнер кнопки определяет внешний вид и поведение кнопки. В среде периода разработки имеются дизайнер форм и редактор свойств, позволяющие манипулировать компонентами и создавать UI. Кроме того, в среде периода разработки есть сервисы, которые используются для настройки и расширения инфраструктуры разработки и с которыми можно взаимодействовать.

Дизайнер форм предоставляет сервисы периода разработки и позволяет программистам разрабатывать формы. Хост дизайнера взаимодействует со средой периода разработки и управляет состоянием дизайнера, его работой (например транзакциями) и компонентами. Кроме того, важно понимать несколько требований, которым должны соответствовать сами компоненты. Так, компонент должен быть уничтожаемым (disposable) (самостоятельно вызывающим метод Dispose), он должен предоставлять свойство Site, им может управлять его контейнер. Чтобы соответствовать этим требованиям, компоненты реализуют интерфейс IComponent:

public interface System.ComponentModel.IComponent : IDisposable
{
ISite Site { get; set; }
public event EventHandler Disposed;
}

Интерфейс IComponent — основополагающий контракт между средой периода разработки и элементом, размещаемым в рабочей области (например в окне дизайнера форм Visual Studio). Кнопка поддерживает IComponent, следовательно, ее можно поместить в дизайнер Windows Forms.

В .NET Framework два типа компонентов: визуальные и невизуальные. Визуальный компонент — это элемент UI, такой как Control, а невизуальный компонент не имеет UI, как, например, компонент, создающий соединение с базой данных SQL Server. Когда вы перетаскиваете компоненты на рабочую область, дизайнер форм в Visual Studio .NET различает визуальные и невизуальные компоненты (рис. 1).

Dd335950.docs_gif(ru-ru,MSDN.10).gif Рис. 1. Визуальные и невизуальные компоненты

Контейнер содержит компоненты и позволяет этим компонентам обращаться друг к другу. Если контейнер управляет своим компонентом, то, когда контейнер уничтожается, он должен уничтожить этот компонент. И это правильно, так как компоненты могут использовать неуправляемые ресурсы, которые не освобождаются автоматически сборщиком мусора. Контейнер реализует интерфейс IContainer, в котором всего несколько методов, позволяющих добавлять и удалять элементы из контейнера:

public interface IContainer : IDisposable
{
ComponentCollection Components { get; }
void Add(IComponent component);
void Add(IComponent component, string name);
void Remove(IComponent component);
}

Пусть простота этого интерфейса не вводит вас в заблуждение. Концепция контейнеров крайне важна на этапе разработки и полезна в других случаях. Например, вам, конечно же, доводилось писать бизнес-логику, которая создает несколько уничтожаемых компонентов. Обычно такой код имеет вид:

using(MyComponent a = new MyComponent())
{
// a.do();
}
using(MyComponent b = new MyComponent())
{
// b.do();
}
using(MyComponent c = new MyComponent())
{
// c.do();
}

При использовании объекта Container можно уменьшить число строк кода:

using(Container cont = new Container())
{
MyComponent a = new MyComponent(cont);
MyComponent b = new MyComponent(cont);
MyComponent c = new MyComponent(cont);
// a.do();
// b.do();
// c.do();
}

Но контейнер не только обеспечивает автоматическое уничтожение компонентов. В .NET Framework используются так называемые объекты связи (sites) — они связывают контейнеры с компонентами. Отношения между ними показаны на рис. 2. Как видите, компонент управляется ровно одним контейнером, и у каждого компонента есть ровно один объект связи. Когда вы разрабатываете формы в дизайнере, один и тот же компонент не может появляться более чем в одной рабочей области. Однако с одним и тем же контейнером можно связать несколько компонентов.

Dd335950.docs_gif(ru-ru,MSDN.10).gif Рис. 2. Взаимосвязи

Контейнер может управлять жизненными циклами содержащихся в нем компонентов. Взамен компоненты получают доступ к сервисам, предоставляемым контейнером. Такие отношения аналогичны случаю, когда COM+-компонент содержится в COM+-контейнере. Позволяя COM+-контейнеру управлять собой, COM+-компонент может участвовать в транзакциях и использовать другие сервисы, предоставляемые COM+-контейнером. В контексте среды периода разработки отношение между компонентом и его контейнером устанавливается через объект связи. Когда вы помещаете компонент на форму, хост дизайнера создает экземпляр объекта связи для компонента и его контейнера. При создании такого отношения компонент подключается к объекту связи и через свое свойство ISite получает доступ к сервисам контейнера.

Сервисы и контейнеры

Компонент, позволяя контейнеру владеть собой, получает доступ к сервисам, предоставляемым этим контейнером. В данном контексте сервис можно рассматривать как функцию, которая имеет общеизвестный интерфейс, доступна через провайдер сервиса, хранится в контейнерах сервиса и которую можно получить, указав ее тип.

Провайдеры сервисов реализуют интерфейс IServiceProvider:

public interface IServiceProvider
{
object GetService(Type serviceType);
}

Клиенты получают сервис, указав тип нужного им сервиса при вызове метода GetService провайдера сервиса. Контейнеры сервисов выступают в роли хранилищ сервисов и реализуют интерфейс IServiceContainer, тем самым предоставляя средства добавления и удаления сервисов. В следующем коде показано определение интерфейса IServiceContainer. Заметьте: определение интерфейса просто содержит методы добавления и удаления сервиса.

public interface IServiceContainer : IServiceProvider
{
void AddService(Type serviceType,
ServiceCreatorCallback callback);
void AddService(Type serviceType,
ServiceCreatorCallback callback, bool promote);
void AddService(Type serviceType, object serviceInstance);
void AddService(Type serviceType, object serviceInstance,
bool promote);
void RemoveService(Type serviceType);
void RemoveService(Type serviceType, bool promote);
}

Контейнеры сервисов могут хранить и возвращать сервисы. То есть они также являются провайдерами сервисов и, следовательно, реализуют интерфейс IServiceProvider. Сочетание сервисов, провайдеров и контейнеров сервисов образует простой проектировочный шаблон, дающий массу преимуществ. Например, этот шаблон:

  • создает слабые связи между клиентскими компонентами и используемыми ими сервисами;
  • создает простое хранилище сервисов и механизм обнаружения, позволяющий без проблем масштабировать приложение (или его части). Можно написать приложение, реализовав только те его части, которые необходимы, затем добавить дополнительные сервисы, не внося кардинальных изменений в приложение или модуль;
  • позволяет реализовать отложенную загрузку сервисов; у метода AddService имеется перегруженная версия, создающая сервисы, когда их запрашивают в первый раз;
  • служит альтернативой статическим классам;
  • поддерживает программирование, основанное на контрактах;
  • применим для реализации сервиса-фабрики;
  • годится для создания архитектуры с поддержкой плагинов. Этот простой шаблон можно применять для загрузки подключаемых модулей (плагинов) и предоставления сервисов этим модулям (например протоколирования или конфигурирования).

Шаблон сервисов широко используется инфраструктурой периода разработки, поэтому очень важно тщательно изучить его.

Создаем дизайнер форм

Итак, вы получили общее представление о среде периода разработки. Рассмотрим архитектуру дизайнера форм, в основе которой лежат описанные выше элементы (рис. 3).

Dd335950.docs_gif(ru-ru,MSDN.10).gif Рис. 3. Архитектура дизайнера форм

Центральное место в архитектуре занимает компонент. Все другие элементы прямо или косвенно взаимодействуют с компонентами. Дизайнер форм — связующее звено между остальными элементами. Дизайнеры форм с помощью хоста дизайнера получают доступ к инфраструктуре периода разработки. Хост дизайнера опирается на сервисы периода разработки и предоставляет собственные сервисы. Сервисы могут использовать и часто используют другие сервисы.

.NET Framework не предоставляет доступ к дизайнеру форм Visual Studio .NET, поскольку его реализация специфична для приложения. Сами интерфейсы недоступны, зато доступна среда периода разработки. Все, что от вас требуется, — создать реализации, специфичные для вашего дизайнера форм, и передать вашу версию среде периода разработки для последующего использования.

Мой пример дизайнера форм показан на рис. 4. Как у любого дизайнера форм, у него есть окно инструментария (toolbox), из которого пользователи выбирают инструменты или элементы управления, рабочая область, позволяющая создавать формы, и сетка свойств, предназначенная для управления свойствами компонентов.

Dd335950.docs_gif(ru-ru,MSDN.10).gif Рис. 4. Пример собственного дизайнера форм

Начнем с разработки окна инструментария. Но предварительно надо решить, как показывать инструменты пользователю. В Visual Studio .NET имеется панель навигации, содержащая несколько групп, в каждую из которых входят соответствующие инструменты. Чтобы разработать окно инструментария, вы должны:

  • создать UI, показывающий инструменты пользователю;
  • реализовать IToolboxService;
  • подключить реализацию IToolboxService к среде периода разработки;
  • реализовать обработку событий, таких как выбор инструмента и его перетаскивание.

В любом реальном приложении разработка UI окна инструментария заняла бы много времени. Первое проектировочное решение, которое надо принять, — определить, как обнаруживать и загружать инструменты. Существует несколько заслуживающих внимания подходов. Первый — «зашить» в код дизайнера показываемые инструменты. Этого лучше не делать, если только вы не пишете крайне простое приложение, которое почти не потребует поддержки в будущем.

Второй подход — считывать данные об инструментах из файла конфигурации. Например, можно описать инструменты следующим образом:

<Toolbox>
<ToolboxItems>
<ToolboxItem DisplayName="Label"
Image="ResourceAssembly,Resources.LabelImage.gif"/>
<ToolboxItem DisplayName="Button"
Image="ResourceAssembly,Resources.ButtonImage.gif"/>
<ToolboxItem DisplayName="Textbox"
Image="ResourceAssembly,Resources.TextboxImage.gif"/>
</ToolboxItems>
</Toolbox>

Преимущество этого подхода в том, что если потребуется изменить набор инструментов, показываемых в окне инструментария, можно добавить или удалить инструменты без перекомпиляции кода. Кроме того, реализация оказывается довольно простой. Вы должны реализовать обработчик раздела, который будет считывать раздел Toolbox и возвращать список, содержащийся в элементе ToolboxItems.

Третий подход — создать класс для каждого инструмента и пометить класс атрибутами, инкапсулирующими такие его параметры, как отображаемое имя (display name), группа и битовая карта (bitmap). При запуске приложение загружает набор сборок (из общеизвестного каталога, заданного в файле конфигурации или каким-то другим способом) и ищет типы с определенным атрибутом (например ToolboxAttribute). Типы с этим атрибутом загружаются в окно инструментария. Этот метод, пожалуй, самый гибкий, а искать инструменты с помощью механизма отражения очень удобно. Но он требует несколько больше усилий. В своем примере я применил второй подход.

Следующая важная операция — получение изображений инструментов. Можно целыми днями создавать свои изображения для окна инструментария, но было бы очень удобно каким-то образом обращаться к изображениям окна инструментария Visual Studio .NET. К счастью, это возможно. На внутреннем уровне Visual Studio .NET использует разновидность третьего подхода: компоненты и элементы управления помечаются атрибутом (ToolboxBitmapAttribute), который задает, откуда берется изображение для компонента или элемента управления.

В примере содержимое окна инструментария (группы и элементы) задано в файле конфигурации приложения. Чтобы загрузить окно инструментария, собственный обработчик раздела читает раздел Toolbox и возвращает связывающий класс (binding class). Затем связывающий класс передается методу LoadToolbox элемента управления TreeView, представляющего окно инструментария:

Листинг 1. Загрузка окна инструментария

{
// Очищаем существующие узлы и изображения дерева
toolboxView.Nodes.Clear();
if (treeViewImgList != null) treeViewImgList.Dispose();
treeViewImgList = new ImageList(this.components);

// Всегда есть два изображения для узла категории и узла
// указателя (pointer node)
treeViewImgList.Images.Add(requiredImgList.Images[0]);
treeViewImgList.Images.Add(requiredImgList.Images[1]);

// Задаем ImageList для TreeView
toolboxView.ImageList = treeViewImgList;

// Если имеются категории...
if(tools!=null && tools.FDToolboxCategories!=null &&
tools.FDToolboxCategories.Length>0)
{
foreach(Category cat in tools.FDToolboxCategories)
{
LoadCategory(cat);
}
}
}

private void LoadCategory(Category cat)
{
// Если категория содержит элементы...
if(cat!=null && cat.FDToolboxItem!=null &&
cat.FDToolboxItem.Length>0)
{
// Создаем узел для категории
TreeNode catNode = new TreeNode(cat.DisplayName);
catNode.ImageIndex = 0;
catNode.SelectedImageIndex = 0;

// Добавляем эту категорию в дерево
toolboxView.Nodes.Add(catNode);

// У каждой категории имеется узел выбора
AddSelectionNode(catNode);
foreach(FDToolboxItem item in cat.FDToolboxItem)
{
LoadItem(item,catNode);
}
}
}

private void LoadItem(FDToolboxItem item,TreeNode cat)
{
if(item!=null && item.Type!=null && cat!=null)
{
// Загружаем тип
Type toolboxItemType = Type.GetType(item.Type);
ToolboxItem toolItem =
new ToolboxItem(toolboxItemType);

// Получаем изображение элемента
// и создаем узел для него
Image img = GetItemImage(toolboxItemType);
TreeNode nd = new TreeNode(toolItem.DisplayName);
nd.Tag = toolItem;

// Добавляем битовую карту элемента
// в список изображений
if(img!=null)
{
treeViewImgList.Images.Add(img);
nd.ImageIndex = treeViewImgList.Images.Count-1;
nd.SelectedImageIndex =
treeViewImgList.Images.Count-1;
}

// Добавляем этот узел в узлы категории
cat.Nodes.Add(nd);
}
}

private Image GetItemImage(Type type)
{
// По заданному типу получаем AttributeCollection
// и ищем атрибут ToolboxBitmap
AttributeCollection attrCol=
TypeDescriptor.GetAttributes(type,true);
if(attrCol!=null)
{
ToolboxBitmapAttribute toolboxBitmapAttr =
(ToolboxBitmapAttribute)attrCol
[typeof(ToolboxBitmapAttribute)];
if(toolboxBitmapAttr!=null)
{
return toolboxBitmapAttr.GetImage(type);
}
}
return null;
}

Метод LoadItem создает экземпляр ToolboxItem для заданного типа и вызывает GetItemImage, чтобы получить изображение, связанное с этим типом. Метод GetItemImage получает набор атрибутов данного типа и ищет атрибут ToolboxBitmapAttribute. Если он находит атрибут, то возвращает изображение, которое связывается с только что созданным ToolboxItem. Заметьте: метод использует TypeDescriptor — вспомогательный класс из пространства имен System.ComponentModel, позволяющий получить информацию об атрибутах и событиях заданного типа.

Теперь вы знаете, как разработать UI окна инструментария. Следующий этап — реализация интерфейса IToolboxService. Поскольку этот интерфейс прямо связан с окном инструментария, удобно реализовать этот интерфейс в классе, унаследованном от TreeView. В основном реализация довольно проста, но следует уделить особое внимание обработке операций drag-and-drop и сериализации элементов окна инструментария (см. метод toolboxView_MouseDown в реализации ToolboxService из исходного кода, который можно скачать к этой статье на сайте MSDN Magazine). Наконец, надо подключить реализацию сервиса к среде периода разработки. Я покажу, как это сделать, но сначала расскажу о том, как реализовать хост дизайнера.

Реализация сервисов

Инфраструктура дизайнера форм основана на сервисах. Существует набор сервисов, которые необходимо реализовать, и необязательные сервисы, просто расширяющие функциональность дизайнера. Это важная особенность рассмотренного выше шаблона сервисов и дизайнера форм. Можно начать с реализации базового набора, а в дальнейшем добавить дополнительные сервисы.

Хост дизайнера подключен к среде периода разработки. Эта среда использует хост-сервис для создания новых компонентов, когда пользователь перетаскивает их из окна инструментария, для управления транзакциями дизайнера, для поиска сервисов при манипуляциях над компонентами и т. д. В интерфейсе хост-сервиса, IDesignerHost, определен ряд методов и событий. При реализации хоста вы реализуете хост-сервис и еще несколько сервисов. К ним относятся IContainer, IComponentChangeService, IExtenderProviderService, ITypeDescriptionFilterService и IDesignerEventService.

Хост дизайнера

Хост дизайнера является ядром дизайнера форм. Когда вызывается конструктор хоста, хост с помощью родительского провайдера сервиса (IServiceProvider) конструирует свой контейнер сервиса. Построение таких цепочек провайдеров позволяет получить эффект «просачивания» (trickle-down effect). После создания контейнера сервиса хост добавляет в провайдер собственные сервисы:

Листинг 2. Добавление сервисов в провайдер

public DesignerHostImpl(IServiceProvider parentProvider)
{
// Добавляем в parentProvider
serviceContainer = new ServiceContainer(parentProvider);
loggerService = parentProvider.GetService(
typeof(IServiceRequestLogger)) as
IServiceRequestLogger;

// Сопоставляем имя объекта связи (site) и ISite
sites = new Hashtable(
CaseInsensitiveHashCodeProvider.Default,
CaseInsensitiveComparer.Default);

// Сопоставляем компонент и дизайнер
designers = new Hashtable();

// Создаем список провайдеров расширения
// (extender providers)
extenderProviders = new ArrayList();

// Создаем стек транзакций
transactions = new Stack();

// Добавляем сервисы
serviceContainer.AddService(typeof(IDesignerHost), this);
serviceContainer.AddService(typeof(IContainer), this);
serviceContainer.AddService(
typeof(IComponentChangeService), this);
serviceContainer.AddService(
typeof(IExtenderProviderService), this);
serviceContainer.AddService(
typeof(IDesignerEventService), this);
serviceContainer.AddService(
typeof(INameCreationService),
new NameCreationServiceImpl(this));
serviceContainer.AddService(
typeof(ISelectionService),
new SelectionServiceImpl(this));
serviceContainer.AddService(
typeof(IMenuCommandService),
new MenuCommandServiceImpl(this));
serviceContainer.AddService(
typeof(ITypeDescriptorFilterService),
new TypeDescriptorFilterServiceImpl(this));
}

Когда компонент перетаскивают на рабочую область, его требуется добавить в контейнер хоста. Добавление нового компонента — довольно сложная операция, поскольку вы должны выполнить несколько проверок и вызвать некоторые события:

Листинг 3. Добавление компонентов

public void Add(IComponent component, string name)
{
if (component == null) throw
new ArgumentException("component");

// Если имя отсутствует, создаем его
if (name == null || name.Trim().Length == 0)
{
// Необходим сервис именования
INameCreationService nameCreationService = GetService(
typeof(INameCreationService)) as
INameCreationService;
if(nameCreationService==null)
throw new Exception
("Failed to get INameCreationService.");
name = nameCreationService.CreateName(
this,component.GetType());
}

// Если мы владеем компонентом и имя изменилось,
// переименовываем компонент
if (component.Site != null &&
component.Site.Container == this &&
name != null &&
string.Compare(name,component.Site.Name,true)!=0)
{
component.Site.Name=name;
return;
}

// Создаем объект связи для компонента
// и связываем его с компонентом
ISite site = new SiteImpl(component, name, this);
component.Site = site;

// Вызываем требуемые события
ComponentEventArgs evtArgs = new ComponentEventArgs(
component);
ComponentEventHandler compAdding = ComponentAdding;
if (compAdding != null)
{
try
{
compAdding(this, evtArgs);
}
catch{}
}

// Если это корневой компонент
IDesigner designer = null;
if(rootComponent == null)
{
// Задаем корневой компонент
rootComponent = component;

// Создаем корневой дизайнер
rootDesigner =
(IRootDesigner)TypeDescriptor.CreateDesigner(
component,typeof(IRootDesigner));
designer = rootDesigner;
}
else
{
designer = TypeDescriptor.CreateDesigner(
component,typeof(IDesigner));
}

// Добавляем дизайнер в список и инициализируем его
designers.Add(component,designer);
designer.Initialize(component);

// Добавляем компонент в список компонентов контейнера
sites.Add(site.Name,site);
compAdding = ComponentAdding;
if (compAdding != null)
{
try
{
compAdding(this, evtArgs);
}
catch{}
}
}

Если не учитывать проверки и события, можно сформулировать алгоритм добавления следующим образом. Прежде всего по типу создается IComponent, а по компоненту — ISite. При этом устанавливается отношение между объектом связи и компонентом. Заметьте: конструктор объекта связи принимает экземпляр хоста дизайнера. Конструктор объекта связи принимает хост дизайнера и компонент, чтобы установить отношение «компонент-контейнер», показанное на рис. 2. Затем создается дизайнер компонента, который инициализируется и добавляется в словарь компонентов и дизайнеров. Наконец, новый компонент добавляется в контейнер хоста дизайнера.

При удалении компонентов нужна кое-какая очистка. Если снова не учитывать простые проверки, при операции удаления требуется удалить дизайнер, уничтожить дизайнер, удалить объект связи компонента, а затем уничтожить компонент.

Транзакции дизайнера

Концепция транзакций дизайнера, в принципе, аналогична концепции транзакций базы данных: и в том, и в другом случае последовательно выполняемые операции объединяются в группу, чтобы их можно было рассматривать как единое целое и поддерживать для них механизм фиксации/отмены. Транзакции дизайнера повсеместно используются инфраструктурой периода разработки для того, чтобы поддерживать отмену операций и чтобы приложения могли задерживать обновление показываемой информации до тех пор, пока не будет выполнена вся транзакция. Хост дизайнера позволяет управлять транзакциями дизайнера через интерфейс IDesignerHost. Управлять транзакциями не так уж сложно (см. файл DesignerTransactionImpl.cs в примере приложения).

DesignerTransactionImpl представляет одну операцию в транзакции. Когда у хоста запрашивают создание транзакции, он создает экземпляр DesignerTransactionImpl, используемый для управления одним изменением. Хост отслеживает транзакции, а экземпляры DesignerTransactionImpl управляют каждым изменением. Если вы не реализуете управление транзакциями, то при работе с дизайнером форм будут генерироваться некоторые интересные исключения.

Интерфейсы

Как я говорил, компоненты помещают в контейнеры, чтобы управлять их жизненным циклом и предоставлять им сервисы. В интерфейсе хоста дизайнера, IDesignerHost, определены методы создания и удаления компонентов, поэтому неудивительно, что хост поддерживает эти операции. В сервисе-контейнере также определены методы добавления и удаления компонентов, которые перекрывают методы CreateComponent и DestroyComponent интерфейса IDesignerHost. Таким образом, большинство сложных операций реализовано в методах создания и удаления, определенных в контейнере, а методы создания и уничтожения просто переадресуют вызовы этим методам.

В интерфейсе IComponentChangeService определены события изменения, добавления, удаления и переименования. Кроме того, определены методы для событий changed («после изменения») и changing («при изменении»), вызываемые средой периода разработки после или при изменении компонента (например при модификации свойства). Этот сервис предоставляется хостом дизайнера, поскольку компоненты создаются и уничтожаются хостом. Помимо создания и удаления компонентов хост выполняет операции переименования, используя метод создания. Логика переименования проста, но любопытна:

// Если мы владеем компонентом и его имя изменилось,
// переименовываем компонент
if (component.Site != null &&
component.Site.Container == this && name != null &&
string.Compare(name,component.Site.Name,true) != 0)
{
// Проверка имени и вызов событий changing/changed
// выполняются в свойстве Site.Name, поэтому
// здесь не нужно делать этого
component.Site.Name=name;
return;
}

Реализация этого интерфейса довольно проста, поэтому остальное можно посмотреть в примере приложения.

ISelectionService используется при выборе компонентов в рабочей области. Когда пользователь выбирает компоненты, среда периода разработки вызывает метод SetSelectedComponents и передает ему выбранные компоненты. Реализация SetSelectedComponents показана в следующем листинге:

Листинг 4. SetSelectedComponents

public void SetSelectedComponents(
ICollection components, SelectionTypes selectionType)
{
// Вызываем событие changing
EventHandler selChange = SelectionChanging;
if (selChange != null)
{
try
{
selChange(this, EventArgs.Empty);
}
catch{}
}

if (components == null) components = new ArrayList();

bool ctrlDown=false,shiftDown=false;

// Определяем, нажаты ли при щелчке клавиши Shift или Ctrl
if ((selectionType & SelectionTypes.Click) ==
SelectionTypes.Click)
{
ctrlDown = ((Control.ModifierKeys & Keys.Control)==
Keys.Control);
shiftDown = ((Control.ModifierKeys & Keys.Shift)==
Keys.Shift);
}

if (selectionType == SelectionTypes.Replace)
{
// Отбрасываем список выбранных компонентов
// и заменяем его на создаваемый следующим оператором
selectedComponents = new ArrayList(components);
}
else
{
if (!shiftDown && !ctrlDown && components.Count == 1 &&
!selectedComponents.Contains(components))
{
selectedComponents.Clear();
}

// Пользователь выбрал какой-то компонент или
// отменил выбор какого-то компонента
IEnumerator ie = components.GetEnumerator();
while(ie.MoveNext())
{
IComponent comp = ie.Current as IComponent;
if(comp!=null)
{
if (ctrlDown || shiftDown)
{
if (selectedComponents.Contains(comp))
{
selectedComponents.Remove(comp);
}
else
{
// Помещаем этот компонент в начало,
// так как это последний
// выбранный компонент
selectedComponents.Insert(0,comp);
}
}
else
{
if (!selectedComponents.Contains(comp))
{
selectedComponents.Add(comp);
}
else
{
selectedComponents.Remove(comp);
selectedComponents.Insert(0,comp);
}
}
}
}
}

// Вызываем событие changed
selChange = SelectionChanging;
if (selChange != null)
{
try
{
selChange(this, EventArgs.Empty);
}
catch{}
}
}

Сервис выбора обрабатывает выбор компонентов в рабочей области дизайнера. Другие сервисы, такие как IMenuCommandService, используют этот сервис, чтобы получать информацию о выбранных компонентах. Чтобы предоставлять эту информацию, сервис хранит внутренний список, содержащий выбранные в данный момент компоненты. Когда в набор выбранных компонентов вносят изменения, среда периода разработки вызывает SetSelectedComponents, передавая ему набор компонентов. Например, если пользователь выбирает один компонент, а затем, удерживая клавишу Shift, выбирает еще три, метод вызывается при каждом добавлении в список выбранных компонентов. При каждом вызове метода среда периода разработки передает информацию о том, какие компоненты выбраны и как они выбраны (через перечислимое SelectionTypes). Реализация проверяет, какие изменения внесены в набор выбранных компонентов, чтобы определить, что ей надо сделать, — добавить или удалить компоненты из внутреннего списка выбранных компонентов. После изменения списка выбранных компонентов я вызываю событие Selection Changed (см. метод selectionService_SelectionChanged в файле SelectionServiceImpl.cs), чтобы обновить сетку свойств с учетом изменений набора выбранных компонентов. Главная форма приложения, MainWindow, подписывается на события changed сервиса выбора, чтобы обновлять сетку свойств, в которой показываются свойства выбранных компонентов.

Также заметьте, что в сервисе выбора определено свойство PrimarySelection. Это свойство (основной выбранный компонент) всегда содержит последний выбранный элемент. Я буду использовать это свойство при рассмотрении IMenuCommandService, когда речь пойдет о выводе контекстного меню дизайнера.

Сервис выбора наиболее сложен в разработке. В реальном приложении приходится обрабатывать события клавиатуры (например нажатие Ctrl+A) и решать задачи, связанные с обработкой большого списка выбранных компонентов.

Реализация ISite — одна из самых важных реализаций:

Листинг 5. Реализация ISite

public class SiteImpl : ISite, IDictionaryService
{
private IComponent component;
private string name;
private DesignerHostImpl host;
private DictionaryServiceImpl dictionaryService;

public SiteImpl(IComponent comp, string name,
DesignerHostImpl host)
{
if(comp==null) throw new ArgumentException("comp");
if(host==null) throw new ArgumentException("host");
if(name==null || name.Trim().Length==0)
throw new ArgumentException("name");

component = comp;
this.host = host;
this.name = name;

// Создаем словарный сервис для этого объекта связи
dictionaryService = new DictionaryServiceImpl();
}

public object GetService(Type service)
{
// Передаем запрос хосту
if (service == typeof(IDictionaryService)) return this;
return host.GetService(service);
}

public object GetKey(object value)
{
return dictionaryService.GetKey(value);
}

public object GetValue(object key)
{
return dictionaryService.GetValue(key);
}

public void SetValue(object key, object value)
{
dictionaryService.SetValue(key,value);
}
}

Как видите, SiteImpl реализует IDictionaryService, что немного необычно, так как все остальные сервисы, которые я разрабатывал, были связаны с хостом дизайнера. Оказывается, среда периода разработки требует, чтобы вы реализовали IDictionaryService для каждого компонента, подключенного к объекту связи. Среда периода разработки обращается к IDictionaryService каждого объекта связи, чтобы поддерживать таблицу данных, повсеместно используемую инфраструктурой дизайнера. Еще одно замечание по реализации объекта связи: поскольку ISite расширяет IServiceProvider, класс содержит реализацию GetService. Инфраструктура дизайнера вызывает этот метод, чтобы получить реализацию сервиса для объекта связи. Если запрашивается сервис IDictionaryService, реализация просто возвращает себя, т. е. класс SiteImpl. В случае других сервисов запрос передается контейнеру объекта связи (например хосту).

У каждого компонента должно быть уникальное имя. Когда вы перетаскиваете компоненты из окна инструментария на рабочую область, среда периода разработки использует реализацию INameCreationService для генерации имени каждого компонента. Имя компонента — это свойство Name, показываемое в окне свойств при выборе компонента. Определение интерфейса INameCreationService имеет вид:

public interface INameCreationService
{
string CreateName(IContainer container, Type dataType);
bool IsValidName(string name);
void ValidateName(string name);
}

В примере приложения реализация CreateName формирует новое имя по параметрам container и dataType. Если в двух словах, то метод подсчитывает количество компонентов, тип которых эквивалентен dataType, а затем по этому количеству и dataType генерирует уникальное имя.

Сервисы, которые мы пока рассматривали, прямо или косвенно работали с компонентами. В отличие от них сервис команд меню специфичен для дизайнеров. Он управляет командами меню и командами дизайнера (операциями) и выводит контекстное меню, когда пользователь выбирает определенный дизайнер.

Сервис команд меню добавляет, удаляет, ищет и выполняет команды меню. Кроме того, он содержит методы обработки команд дизайнера и вывода контекстного меню для дизайнеров, поддерживающих такие меню. Ядром моей реализации является код, отвечающий за вывод контекстного меню. Саму реализацию предлагаю посмотреть в примере приложения, а в статье поясню, как выводить контекстные меню.

Обработка команд дизайнера и вывод контекстных меню

Существует два типа команд дизайнера: глобальные и локальные. Глобальные команды предназначены для всех дизайнеров, а локальные специфичны для конкретного дизайнера. Локальные команды можно увидеть, например, щелкнув правой кнопкой мыши элемент управления «набор вкладок» на рабочей области (рис. 5).

Dd335950.docs_gif(ru-ru,MSDN.10).gif Рис. 5. Рабочая область

По щелчку элемента управления «набор вкладок» правой кнопкой мыши в контекстное меню добавляются локальные команды, которые позволяют добавлять и удалять вкладки этого элемента. Пример глобальной команды можно увидеть в дизайнере форм Visual Studio, щелкнув правой кнопкой мыши любое место рабочей области. Независимо от того, где и какой объект вы щелкаете, вы всегда увидите два пункта меню: View Code и Properties. У каждого дизайнера есть свойство Verbs, которое содержит команды, представляющие специфичную для этого дизайнера функциональность. Например, у дизайнера элемента «набор вкладок» набор команд содержит два члена: Add Tab и Remove Tab.

Когда пользователь щелкает правой кнопкой мыши элемент «набор вкладок» на рабочей области, среда периода разработки вызывает метод ShowContextMenu интерфейса IMenuCommandService:

Листинг 6. Обработка контекстных меню для рабочей области

System.ComponentModel.Design.CommandID menuID, int x, int y)
{
ISelectionService selectionService = host.GetService(
typeof(ISelectionService)) as ISelectionService;

// Получаем основной компонент
IComponent primarySelection =
selectionService.PrimarySelection as IComponent;

if (lastSelectedComponent != primarySelection)
{
// Удаляем из контекстного меню все локальные команды
ResetContextMenu();

// Получаем дизайнер
IDesigner designer = host.GetDesigner(
primarySelection);

// Не у всех элементов управления имеется дизайнер
if(designer!=null)
{
// Добавляем элементы меню для команд дизайнера
DesignerVerbCollection verbs = designer.Verbs;
foreach (DesignerVerb verb in verbs)
{
CreateAndAddLocalVerb(verb);
}
}
}

// Я показываю контекстное меню только
// для элементов управления
if(primarySelection is Control)
{
Control comp = primarySelection as Control;
Point pt = comp.PointToScreen(new Point(0, 0));
contextMenu.Show(comp, new Point(x - pt.X, y - pt.Y));
}

// Сохраняем выбранный компонент до следующего раза
lastSelectedComponent = primarySelection;
}

Этот метод отвечает за вывод контекстного меню для дизайнера выбранного объекта. Как видно из листинга 6, выбранный компонент метод получает от сервиса выбора, дизайнер компонента — от хоста, набор команд — от дизайнера, а затем добавляет элемент меню для каждой команды. После добавления команд показывается контекстное меню. Заметьте: при создании элементов меню для команд добавляется обработчик событий Click элемента меню. Собственный обработчик Click обрабатывает события щелчка для всех элементов меню (см. метод MenuItemClickHandler в примере приложения).

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

ITypeDescriptorFilterService

Как я уже говорил, TypeDescriptor — вспомогательный класс, используемый для получения информации о свойствах, атрибутах и событиях типа. ITypeDescriptorFilterService может фильтровать эту информацию для компонентов, подключенных к объектам связи. Класс TypeDescriptor использует ITypeDescriptorFilterService, когда пытается вернуть информацию о свойствах, атрибутах и/или событиях компонента, подключенного к объекту связи. Для дизайнеров, которые должны изменять метаданные своего компонента, доступные среде периода разработки, можно реализовать IDesignerFilter. В ITypeDescriptorFilterService три метода, позволяющие фильтрам дизайнеров обращаться к метаданным компонентов, подключенных к объектам связи, и изменять их. Реализация ITypeDescriptorFilterService проста и интуитивно понятна (см. файл TypeDescriptorFilterService.cs в примере приложения).

Сводим все воедино

Если вы уже посмотрели пример приложения и запустили дизайнер форм, вас может заинтересовать, как удалось добиться, чтобы все эти сервисы работали совместно. Дизайнер форм нельзя создавать поэтапно, т. е. реализовать один сервис, протестировать приложение, затем написать следующий сервис и т. д. Вы должны реализовать все требуемые сервисы, создать UI и связать их друг с другом; только потом можно тестировать приложение. Это плохая новость. А хорошая в том, что я выполнил большую часть этой работы в реализованных мной сервисах. От вас требуется приложить совсем немного усилий.

Для начала посмотрите метод CreateComponent хоста дизайнера. При создании нового компонента важно определить, является ли он первым (имеет ли rootComponent значение null). Если это первый компонент, вы должны создать специальный дизайнер для компонента. Специальный дизайнер — экземпляр IRootDesigner, поскольку дизайнер, находящийся на самом верхнем уровне иерархии дизайнеров, должен иметь тип IRootDesigner:

Листинг 7. Создание корневого дизайнера

// Если это корневой компонент
IDesigner designer = null;
if(rootComponent==null)
{
// Задаем корневой компонент и создаем корневой дизайнер
rootComponent = component;
rootDesigner = (IRootDesigner)TypeDescriptor.CreateDesigner(
component,typeof(IRootDesigner));
designer = rootDesigner;
}
else designer = TypeDescriptor.CreateDesigner(
component,typeof(IDesigner));

designers.Add(component,designer);
designer.Initialize(component);

Итак, вы знаете, что первый компонент должен быть корневым, но как гарантировать, что первым будет именно тот компонент, который и должен быть? Ответ на этот вопрос таков: первым компонентом становится рабочая область, поскольку вы создаете этот элемент управления как Form в процедуре инициализации основного окна:

Листинг 8. Инициализация дизайнера форм

private void InitWindow()
{
serviceContainer = new ServiceContainer();

// Создаем хост
host = new DesignerHostImpl(serviceContainer);

AddBaseServices();

Form designSurfaceForm = host.CreateComponent(
typeof(Form),null) as Form;

// Теперь, когда у нас есть корневой дизайнер,
// создаем дизайнер форм
FormDesignerDocumentCtrl formDesigner =
new FormDesignerDocumentCtrl(this.GetService(
typeof(IDesignerHost)) as IDesignerHost,
host.GetDesigner(designSurfaceForm) as IRootDesigner);
formDesigner.InitializeDocument();
formDesigner.Dock = DockStyle.Fill;
formDesigner.Visible = true;
formDesigner.Focus();
designSurfacePanel.Controls.Add(formDesigner);

// Необходимо подписаться на события changed
// выбранных компонентов, чтобы обновлять сетку свойств
ISelectionService selectionService = host.GetService(
typeof(ISelectionService)) as ISelectionService;
selectionService.SelectionChanged += new EventHandler(
selectionService_SelectionChanged);

// Активизируем хост
host.Activate();
}

Работа с корневым компонентом — единственный сложный момент в организации взаимодействия хоста дизайнера, среды периода разработки и UI. В остальном легко разобраться, потратив немного времени на изучение кода.

Отладка проекта

Реализация дизайнера форм — задача нетривиальная. Документации по этой тематике очень мало. А после того, как вы разберетесь, с чего начать и какие сервисы реализовать, придется потратить много усилий на отладку, поскольку вы должны реализовать набор необходимых сервисов и подключить их и лишь потом отлаживать отдельные сервисы. Наконец, после реализации требуемых сервисов окажется, что сообщения об ошибках не особо полезны. Например, вы можете получить NullReferenceException в строке, где вызываются внутренние сборки среды периода разработки. Отлаживать эти сборки нельзя, и придется ломать голову над тем, в каком сервисе произошла ошибка.

Кроме того, поскольку инфраструктура периода разработки основана на шаблоне сервисов, рассмотренном выше, отладка сервисов может стать проблемой. Чтобы облегчить отладку, следует вести журнал запросов к сервисам. Журнал инфраструктуры, содержащий данные о том, какой запрос к сервису выполнен, чем он закончился (успехом или неудачей), откуда вызван сервис (для этого применяется Environment.StackTrace), — очень полезный отладочный инструмент, который следует взять на вооружение.

Заключение

Я привел обзор базовых сервисов, которые надо реализовать, чтобы получить работоспособный дизайнер. Вы также узнали, как с помощью файла конфигурации настроить окно инструментария в соответствии с требованиями вашего приложения. Остается лишь поэкспериментировать с существующими сервисами и реализовать некоторые другие сервисы по своему усмотрению.


Комментариев нет:

Отправить комментарий