Сообщений 0    Оценка 660        Оценить  
Система Orphus

Элементы управления Windows Forms и компоненты

Краткий путеводитель

Автор: Илья Рыженков
Источник: RSDN Magazine #2-2004
Опубликовано: 16.10.2004
Исправлено: 10.12.2016
Версия текста: 1.0
Введение
Disclaimer
Проектируйте
Сохраняйте гибкость
Создание контрола
Коллекции элементов
Отрисовка
Придерживайтесь стандартов
Оптимизируйте
Обработка действий пользователя
Взаимодействие с мышью
Взаимодействие с клавиатурой
Поддержка дизайнера форм и редактора
Поведение в режиме дизайна
Использование атрибутов
Коллекции
Конверторы типов
Расширение компонентов
Улучшение компонента
Локализация
Источники данных
Доступность
Заключение
Ссылки
Приложения
Конвертор типа

Введение

При создании программных продуктов на платформе .NET периодически приходится создавать специализированные версии компонентов и элементов управления, а то и совершенно новые «контролы». По этому поводу я составил для себя небольшой справочник или, если хотите, путеводитель по созданию компонентов, просто чтобы ничего при этом не забывать. Слово «контрол» впредь будет употребляться в данной статье вместо термина «элемент управления» по причине устоявшейся терминологии, а также для краткости изложения.

Disclaimer

Статья не претендует ни на подробное изложение процесса создания контролов, ни на сколько-нибудь серьезное описание компонентной технологии .NET. Это скорее некоторый набор в целом полезных утверждений, к которым, тем не менее, стоит относиться скептически и не забывать о реальных задачах и уместности применения того или иного принципа. Помните, что в тексте автор выражает собственную точку зрения, делится личным опытом и выводами, которые он сделал за время разработки компонентов и контролов. Эта статья не может рассматриваться ни как обощенный опыт RSDN, ни как какой бы то ни было стандарт разработки компонентов и контролов, ни даже как на 100% верный источник информации. Существует множество способов добиться одного и того же результата, и изложенный здесь вариант не обязательно является лучшим. Данная статья также не принимает во внимание тот факт, что зачастую экономически выгоднее приобрести готовый компонент или отыскать в сети бесплатный вариант.

Проектируйте

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

Спроектируйте структуру классов, которые будут использоваться для реализации контрола. Опишите специальные стили в виде перечислений (enum). Подготовьте список основных событий, о которых стоит оповещать пользователей вашего контрола. Создайте соответствующие делегаты и классы данных для них. Помните, что Windows Forms содержит довольно много классов, которые можно использовать повторно.

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

Зафиксируйте информацию о том, что является согласованным состоянием объекта. В процессе работы компонент будет подвергаться самым различным воздействиям и, возможно, совсем не тем образом, на который вы рассчитывали. Поэтому очень важно отслеживать согласованность состояния объекта, и при изменении некоторых внутренних данных соответственным образом менять и связанные. Например, в контроле вроде ListBox все выделенные (selected) элементы должны также находиться в коллекции всех имеющихся элементов, поэтому при удалении элемента из контрола необходимо также проверить, не является ли он выделенным, и удалить его из коллекции выбранных тоже.

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

Для проверки соблюдения согласованности состояния вы можете создать метод VerifyConsistency(). Вызовы метода можно расставить везде, где изменяется внутреннее состояние компонента. Метод может проверить состояние объекта на согласованность и либо просто вернуть управление, либо сгенерировать исключение с описанием проблемы согласованности. Если снабдить этот метод атрибутом [Conditional(“DEBUG”)], то он будет вызываться только в debug-режиме, а в release-режиме компилятор не сгенерирует его вызов. Таким образом, вы не потеряете производительности при реальном использовании, а в случае проблемы при отладке получите её описание. Вы также можете создать собственную константу условной компиляции (например, “VERIFY”) и использовать её. Для этого откройте свойства проекта (Configuration Properties, Build) и в строке Conditional Compilation Constants добавьте свою константу.

Сохраняйте гибкость

Помните о том, что проще, дешевле и удобнее создать и сопровождать один настраиваемый контрол, чем десяток похожих контролов, отличающихся незначительными деталями отрисовки или поведения. Однако же не увлекайтесь созданием суперуниверсальных монстров, умеющих всё. Зачастую достаточно позволить пользователю настраивать цвета, размеры и шрифты основных частей вашего контрола. Не забывайте про различные состояния, в которых может находиться контрол, например, Selected, Pushed, Focused. Предоставьте пользователю возможность задавать альтернативное отображение контрола в некоторых состояниях. Например, для контрола вроде списка имеет смысл предоставить пользователю возможность изменить цвет, которым подсвечивается текущий элемент.

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

Предоставляйте несколько вариантов методов там, где возможен разный формат входных данных. Например, варианты (Point pt) и (int x, int y), (Rectangle rect) и (Point pt, Size size).

Предоставляйте события, информирующие клиента изменения свойств контрола. Стандартное название для события – NNNNChanged, где NNNN – имя свойства. Тип делегата должен быть EventHandler. Используя эти события, клиенты вашего класса смогут отслеживать изменения в состоянии контрола, даже если они сложно связаны. Это также позволит системе времени дизайна правильно отслеживать состояние вашего контрола. Кроме того, наличие подобных событий является необходимым условием для подключения к источнику данных в режиме read-write, иначе система Data Binding не узнает, что именно изменилось.

Создание контрола

Коллекции элементов

Если контрол должен содержать в себе некий набор элементов, создайте классы, представляющие эти элементы, а также типизированные коллекции для их хранения. Контрол должен получать уведомления об изменениях в составе и свойствах элементов, чтобы он мог изменить своё представление адекватно состоянию. Для этого создайте в контроле специальный internal-метод, вызываемый из коллекций и их элементов при изменении состояния. Можно, конечно, использовать события и делегаты, но в данном случае это не особенно оправдано из соображений производительности, а клиенты таких сообщений заранее известны. Наследуйте коллекции от CollectionBase, переопределите методы OnClear, OnInsert, OnRemove, OnSet для того, чтобы уведомлять контрол об изменениях в составе коллекций. Определите типизированные Insert, Add, Remove, IndexOf, AddRange и индексер. Если контрол достаточно сложен, подумайте о специализированных методах, которые принимают во внимание произведенное изменение, а не используют универсальный метод вроде SomethingChangedUpdateAll.

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

Довольно часто требуется произвести некоторые вычисления извне контрола или воздействовать на контрол из кода. Предоставьте полезные методы для таких случаев, например, такие:

        public ItemType GetItemAt(Point pt);
public ItemType GetItemAt(int x, int y);
public Rectangle GetItemRect(ItemType item);
publicvoid EnsureVisible(ItemType item);

Предоставляйте отдельный виртуальный защищенный метод для отрисовки элемента. В качестве параметров, кроме собственно элемента, используйте Graphics и Rectangle. Рассмотрите необходимость ограничить область рисования путём установки Graphics.Clip. Обратите внимание на отрисовку частично видимых элементов.

        protected
        virtual
        void DrawItem(Graphics g, ItemType item, Rectangle rect);

Отрисовка

Метод OnPaint должен заниматься только максимально быстрой отрисовкой контрола. Зачастую, в нетривиальном контроле в этом методе стоит воспользоваться предварительными расчётами, произведенными один раз. Для этого лучше создать специальный метод Recalculate, которые и рассчитает необходимые размеры, прямоугольники и местоположение элементов. В конце этого метода вызывается Invalidate для обновления внешнего вида контрола на экране. Свойства и обработчики событий, меняющие расположение составляющих контрола, вызывают Recalculate после установки новых значений. Если свойство не меняет расположения (layout), достаточно вызвать лишь Invalidate. Например, свойство BorderWidth меняет расположение частей контрола, поэтому требует вызова Recalculate. Напротив, после изменения свойства ForeColor достаточно вызвать Invalidate.

Если контрол состоит из элементов, предоставьте событие PaintItem, чтобы пользователь мог изменить рисование элементов, не прибегая к наследованию. В данных события (наследнике EventArgs) предусмотрите устанавливаемое свойство Handled, с помощью которого пользователь сможет отменить стандартное рисование. Также можно рассмотреть режим рисования вроде OwnerDraw, при котором стандартное рисование вообще игонрируется. В простом случае метод рисования элемента будет выглядеть примерно так:

        public
        event PaintItemEventHandler PaintItem;
protectedvirtualvoid DrawItem(Graphics g, ItemType item, Rectangle rect)
{
  if (PaintItem != null) 
  {
    PaintItemEventArgs e = new PaintItemEventArgs(g, item, rect);
    PaintItem(this, e);
    if (e.Handled)
      return;
  }
  ... стандартная отрисовка элемента ...
}

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

Расчёт таких параметров, как ширина или высота текста, производится при помощи объекта Graphics. Его можно получить, используя метод Control.CreateGraphics(). Имеет смысл рассмотреть кэширование полученного объекта Graphics, если он используется часто и наблюдаются проблемы со скоростью.

При выводе текста можно использовать StringFormat для указания параметров выводимой строки. Например, для центрирования текста лучше использовать StringFormat, чем вручную рассчитывать координаты. Если выводимые строки могут быть длинными и не помещаться в отводимое для них место, используйте StringFormat.Trimming. Не забывайте также выставить StringFormat.HotkeyPrefix в значение HotkeyPrefix.None если не нужна специальная обработка символа “&”.

Для отрисовки стандартных элементов контрола (вроде рамок и кнопок) используйте класс ControlPaint. Если вы создаете библиотеку контролов, имеющих много общих элементов, предусмотрите собственный класс такого вида. Это не только позволит избежать ошибок и сократит код, но и позволит в будущем добавлять новые возможности, стили и виды для всей библиотеки. Учтите, однако, что ControlPaint не поддерживает стили Windows (XP-themes).

Придерживайтесь стандартов

Используйте хорошо известные имена для свойств и методов. Например, GetItemAt вместо PointToItem, BackColor вместо BackgroundColor. Если в Windows Forms найдутся аналогичные методы, используйте их названия. Помните, что привычки пользователя важнее, чем синтаксическая правильность, а пользователи чаще сталкиваются со стандартными контролами, нежели с вашими.

Придерживайтесь стандартного стиля для описания делегатов:

        public
        delegate
        void SomeEventHandler(object sender, SomeEventArgs e);

publicclass SomeEventArgs : EventArgs
{
  privateint _intValue;
  privatestring _stringValue;

  public SomeEventArgs(int intValue, string stringValue)
  {
    _intValue = intValue; 
    _stringValue = stringValue;
  }

  publicint IntValue { get { return _intValue; } }
  publicstring StringValue { get { return _stringValue; } }    
}

Для инициации события используйте защищенный виртуальный метод

        public
        event SomeEventHandler Some;
protectedvirtualvoid OnSome(SomeEventArgs e)
{
  if (Some != null)
    Some(this, e);
}

Взаимодействуйте с операционной системой касательно предпочтений пользователя. Используйте системные шрифты, цвета и другие настройки в качестве значений по умолчанию. При необходимости, подпишитесь на событие Microsoft.Win32.SystemEvents.UserPreferenceChanged и обновляйте кэшированные значения для Brush, Pen, Font, используя статические свойства класса SystemInformation. Перегрузите OnSystemColorsChanged для той же цели. Некоторые классы .NET Framework самостоятельно отслеживают эти вещи, например SolidBrush, созданный на базе константы, соответствующей предопределенному системному цвету, будет поддерживать себя в актуальном состоянии, а вот LinearGradientBrush не будет этого делать.

Оптимизируйте

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

Создание Brush, Pen является относительно дорогостоящей операцией, а изменение свойств контрола, влияющих на них, происходит достаточно редко. Поэтому вместо создания таких объектов прямо в OnPaint, храните соответствующие Brush, Pen и другие аналогичные данные в классе вместе с переменными, хранящими соответствующие цвет, толщину и другие параметры. Меняйте их синхронно с изменением соответствующих свойств. Не забывайте вызывать Dispose для таких объектов при смене свойств и в Dispose самого контрола. Поскольку до создания Handle контрола такие данные вряд ли понадобятся, разумно создавать эти кэширующие объекты в переопределенном методе CreateHandle. Схему работы с такими данными можно изобразить так:

        public
        class MyControl : Control
{
  private Color _someColor;
  private Brush _someBrush;

  public Color SomeColor
  {
    get { return _someColor; }
    set
    {
      if (_someColor == value)
        return;
      if (IsHandleCreated)
        _someBrush.Dispose();

      _someColor = value;
      if (IsHandleCreated)
        _someBrush = new SolidBrush(_someColor); 
    }
  }

  protectedoverridevoid CreateHandle()
  {
    base.CreateHandle();
    _someBrush = new SolidBrush(_someColor);
  }

  protectedoverridevoid Dispose(bool isDisposing)
  {
    if (isDisposing && IsHandleCreated)
    {
      _someBrush.Dispose();        
    }
    base.Dispose(isDisposing);
  }
}

Унаследованные свойства вроде BackColor можно переопределить (override), чтобы поддерживать кэш для Brush.

Не используйте двойную буферизацию, если структура вашего контрола позволяет отрисовать его таким образом, чтобы каждая точка была выставлена ровно один раз. Например, простейший ProgressBar может быть отрисован двумя прямоугольниками. В таком случае, установите стиль ControlStyles.AllPaintingInWmPaint, при этом игнорируется сообщение Windows об очистке фона окна, и метод OnPaintBackground не вызывается.

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

SetStyle(
  ControlStyles.DoubleBuffer 
  | ControlStyles.AllPaintingInWmPaint 
  | ControlStyles.UserPaint, true);

Используйте специальные методы классов Graphics, ControlPaint где возможно. Например, DrawImageUnscaled быстрее простого DrawImage. Если вы рисуете элементы ImageList, используйте метод ImageList.Draw(...) вместо Graphics.DrawImage(_imageList.Images[0], ...) – это не только быстрее, но и единственный способ отрисовки иконку (Icon) без потери альфа-канала.

Если в вашем компоненте много событий, можно использовать встроенную в класс Component возможность унификации их обработки. Защищенное свойство Events поддерживает словарь событий и позволяет манипулировать событиями единообразно. Кроме того, если пользователь подписывается лишь на некоторые из них (а это самый частый вариант), то неиспользуемые события не будут занимать место в памяти. С другой стороны, на каждое использованное событие расходуется примерно в три раза больше памяти, чем если бы делегат был просто членом класса, поэтому если событий мало, или если часто используется больше трети событий, то овчинка не стоит выделки. Пример использования такой техники показан ниже:

        // создаем уникальный ключ для нашего события 
        private
        readonly
        static
        object EventDataChanged = newobject();

publicevent EventHandler DataChanged
{
  add
  {
    // добавить обработчик в общую таблицу
    Events.AddHandler(EventDataChanged, value); 
  }
  remove
  {
    // удалить обработчик из общей таблицы
    Events.RemoveHandler(EventDataChanged, value); 
  }
}

protectedvirtualvoid OnDataChanged(EventArgs e)
{
  // получить обработчик из таблицы
  EventHandler eventHandler = (EventHandler)Events[EventDataChanged]; 
  // если есть подписчики, то оповестить ихif (eventHandler != null) 
    eventHandler(this, e); 
}

Обработка действий пользователя

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

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

Одно из важнейших общих правил при описании взаимодействия с пользователем – это отделение обработки ввода от собственно действия. Не стоит сооружать огромных switch-case конструкций с логикой внутри OnKeyDown или OnMouseUp. В этих функциях надо лишь определиться, какое действие должно быть выполнено, и вызвать соответствующий метод, который произведет необходимые изменения. Например, при нажатии стрелочки вверх можно вызвать защищенный виртуальный метод MoveUp и всю логику перемещения курсора вверх произвести в нём. Впоследствии, можно будет изменять логику поведения контрола независимо от обработки действий пользователя. Кроме того, наследник вашего класса сможет переопределить поведение естественным образом.

Взаимодействие с мышью

При обработке событий от мыши, отслеживайте нажатие и отпускание кнопок внутри контрола. Для этого в обработчике OnMouseDown установите свойство Capture в значение true, в обработчике OnMouseUp проверьте значение Capture. Если оно равно true, то нажатие и отпускание кнопки произошло в вашем контроле – установите Capture в значение false и обработайте нажатие. Если контрол состоит из разнородных частей, запоминайте, в каком именно месте произошло нажатие и отпускание, т.е. рассматривайте части контрола как независимые единицы взаимодействия.

При обработке OnMouseMove отслеживайте движение внутри и вне контрола. Если вы не захватили (captured) мышь, то движения мыши вне контрола вы не получите, в противном случае можно использовать ClientRectangle.Contains(new Point(e.X, e.Y)) для определения положения курсора.

Отслеживайте различные кнопки мыши согласно требованиям поведения контрола. Если вам нужна только левая кнопка, делайте соответствующие проверки, не игнорируйте значение MouseEventArgs.Button.

Если нужно различать нажатие или движение с клавишами-модификаторами, такими как Control, Shift, Alt, используйте статическое свойство ModifierKeys класса Control. Учтите, что пользователь может в любой момент поменять модификаторы, поэтому лучше отслеживать их в момент отпускания кнопки, когда действие уже завершено. Желательно также изменять курсор мыши, чтобы пользователь получал визуальное подтверждение изменения режима работы. Это можно сделать в OnKeyDown и OnKeyUp, обрабатывая соответственно Key.ShiftKey, Keys.ControlKey и Keys.Menu (кнопка Alt).

Взаимодействие с клавиатурой

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

При необходимости различать нажатие клавиш с клавишами-модификаторами, такими как Control, Shift, Alt, используйте свойство Modifiers полученного в обработчике объекта KeyEventArgs или отдельные свойства Control, Shift и Alt. Используйте свойство KeyCode этого объекта, если вы хотите получить код нажатой клавиши отдельно, и свойство KeyData, если вам нужно значение, комбинированное с управляющими клавишами. Также имеется свойство KeyValue, которое является целым числом, представляющим KeyData.

Для получения управляющих клавиш, вроде Tab, Return, Escape и стрелочек, которые обычно обрабатываются формой или другим контейнером, необходимо перегрузить метод IsInputKey и вернуть для нужных клавиш значение true. В противном случае такие кнопки будут обработаны контейнером согласно собственным правилам и не достигнут вашего контрола. В некоторых случаях может понадобиться перегрузить ProcessCmdKey или ProcessDialogKey.

Поддержка дизайнера форм и редактора

Хороший контрол должен быть удобен не только во время выполнения, но и при использовании его в дизайнере форм. Подробный рассказ о собственном дизайнере или редакторе выходит за рамки данной статьи, однако многие вещи довольно просты и могут резко повысить удобство использования вашего контрола во время дизайна.

Поведение в режиме дизайна

Не полагайтесь на порядок установки значений свойств контрола дизайнером и тем более не подразумевайте, что некоторое свойство будет уже выставлено к моменту выставления другого свойства. Если избежать этого невозможно в силу особых свойств контрола, используйте интерфейс ISupportInitialize. Если некоторый компонент реализует этот интерфейс, дизайнер вставит вызов BeginInit перед началом инициализации и EndInit по окончании. Таким образом, вы сможете отследить, что инициализация закончена и, например, вызвать Recalculate для расчёта необходимых данных.

Во многих случаях необходимо знать, используется ваш контрол в данный момент в нормальных условиях (в работающем приложении) или же в режиме дизайна. Для этого необходимо использовать свойство DesignMode у сайта контрола (объекта, реализующего ISite). Если контрол уже сконструирован и находится в своём контейнере (например, форме), то его сайт можно получить, используя свойство Site – оно есть у любого компонента и контрола.

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

Если это невозможно по каким-либо причинам, придётся использовать медленный способ трассировки стека вызовов. В данном примере реализации такой проверки мы сначала пытаемся использовать быстрый способ. Если это не удалось, то мы просматриваем стек вызовов в поисках объекта, реализующего IDesignerHost. При нахождении такового мы делаем вывод, что мы находимся в дизайн-тайме. В реальном приложении может иметь смысл ограничить глубину просмотра стека.

        private
        bool IsDesignMode()
{
   if(Site != null )
      return Site.DesignMode;

   StackTrace stackTrace = new StackTrace();
   int frameCount = stackTrace.FrameCount - 1;

   for( int frame = 0; frame < frameCount; frame++ )
   {
      Type type = stackTrace.GetFrame(frame).GetMethod().DeclaringType;
      if (typeof(IDesignerHost).IsAssignableFrom(type))
         returntrue;
   }
   returnfalse;
}

Также можно проверить, что исполняемой design-средой является Visual Studio: if (Application.ExecutablePath.ToLower().IndexOf("devenv.exe") > -1). Этот способ, хотя и быстрее, не позволит вам правильно определить другие возможные дизайнеры, например SharpDevelop.

Использование атрибутов

Дизайнер форм не знает предназначения свойства вашего контрола - предназначено ли оно для дизайнера или только для работы из кода, и какое у него значение по умолчанию. Поэтому всегда размечайте каждое публичное свойство соответствующими атрибутами Category, Description, DefaultValue, Browsable, DesignerSerializationVisibility. При отсутствии некоторых из этих атрибутов среда самостоятельно предполагает некоторое значение по умолчанию, но тем не менее лучше размечать все свойства, поскольку в какой-либо другой среде, другом дизайнере форм или вообще отдельном приложении используемые по умолчанию значения могут быть совершенно другими. Краткое описание этих атрибутов:

Помечайте свойства атрибутом DefaultValue, чтобы сообщить дизайнеру форм о значении по умолчанию. Если значение по умолчанию вашего свойства не может быть сохранено в метаданных сборки (т.е. это не простой тип, не строка и не Type), то вы не сможете применить атрибут DefaultValue напрямую. Если у вас есть TypeConverter для типа свойства, и он может преобразовать строку к экземпляру нужного типа, то атрибут DefaultValue можно использовать в виде [DefaultValue(Type, string)], например, для свойства типа Size можно использовать такой вариант: [DefaultValue(typeof(Size), "32;32")]. Учтите, что при этом всегда используется Invariant Culture.

Если вы не можете использовать атрибут DefaultValue из-за отсутствия TypeConverter или нетривиальной логики, можно реализовать bool ShouldSerializeNNNN() и void ResetNNNN(), где NNNN – это имя свойства. ShouldSerialize должен возвращать true, если значение свойства отлично от значения по умолчанию. Reset должен вернуть свойство к значению по умолчанию. Дизайнер, обнаружив эти методы и не обнаружив атрибута DefaultValue, будет использовать их по необходимости. Следите за тем, чтобы значение по умолчанию действительно выставлялось в конструкторе. Эти методы не обязаны быть публичными, потому что среда использует их через рефлексию (reflection). Тем не менее, часто их выгодно делать внутренними (internal) для последующего использования в TypeConverter.

Используйте атрибут DefaultProperty у класса контрола для того, чтобы сказать дизайнеру, какое свойство является главным. При выборе вашего контрола такое свойство будет сделано текущим. Учтите, что если вы редактировали свойство одного контрола, а потом переключились на другой контрол, в котором есть свойство с таким же именем, дизайнер сохранит текущее свойство независимо от атрибута DeafultProperty. Аналогично выставляйте атрибут DefaultEvent у класса, указывая основное событие, например Click. При двойном щелчке на контроле в режиме дизайна автоматически будет создан (или открыт, если уже создан) обработчик этого события.

Если некоторый метод или свойство являются потенциально опасными и требуют дополнительных знаний об использовании, имеет смысл пометить такой член класса атрибутом [EditorBrowsable(EditorBrowsableState.Advanced)]. В таком случае его увидят в intellisense только те пользователи, у которых в свойствах Visual Studio сброшен флажок Hide advanced members. Можно и вообще спрятать свойство, используя [EditorBrowsable(EditorBrowsableState.Never)]. Например, в классе Control есть метод ResetBackColor, являющийся публичным и виртуальным. Однако он не подсказывается редактором, и набрать его можно только вручную.

Если некоторый класс является компонентом (реализует интерфейс IComponent), а вы не хотите, чтобы он появлялся под редактором формы в области компонентов (component tray), используйте атрибут [DesignTimeVisible(false)].

Чтобы сообщить среде, что вы хотите видеть свой класс на панели доступных контролов и компонентов (Tool Box), используйте атрибут ToolboxItem(true). Задать картинку для контрола можно с помощью атрибута ToolboxBitmap. Например, пометка атрибутом ToolboxBitmap(typeof(MyControl)] приведет к поиску ресурса с именем namespace.MyControl.bmp в сборке с типом MyControl и использование этой картинки на панели инструментов. Картинка должна быть 16-цветной, размером 16х16 пикселей. Цвет левого нижнего пикселя будет считаться прозрачным. Если вы хотите использовать иконку, а не bmp-файл, вам придётся указать это явно, используя вариант атрибута: [ToolboxBitmap(typeof(MyControl), "ToolboxIcons.MyControl.ico")].

Если некоторое свойство должно наследовать своё значение от контейнера, иначе говоря, быть прозрачным при некоторых условиях, используйте атрибут AmbientValue вместо DefaultValue. Это сообщит дизайнеру, знакомому со структурой объектов, что реальное значение нужно брать у контейнера. Это особенно важно использовать для локализуемых свойств, потому что иначе для них будет сгенерирован код чтения из ресурсов. Например, атрибут [AmbientValue(null)] указывает на то, что при выставлении свойства в null контрол начинает использовать аналогичное свойство родителя. Примерами таких свойств могут быть Font или BackColor. Учтите, что, несмотря на этот атрибут в коде getter-а, всё равно необходимо опрашивать родителя самостоятельно из кода геттера (getter) свойства, дизайнер просто учитывает это при отображении значений в PropertyGrid и генерации кода в InitializeComponent.

Если изменение некоторого свойства влечет за собой изменение значения другого свойства, необходимо сообщить об этом системе дизайна. Для этого используется атрибут RefreshProperties. Например, если свойство помечено [RefreshProperties(RefreshProperties.All)], то при его изменении все свойства данного компонента будут заново опрошены, и вся информация обновлена.

Если необходимо отображать свойство как «специальное», т.е. с круглыми скобками вокруг имени, наподобие (Name) или (DataBindings), используйте атрибут [ParenthesizeProperty(true)]. Не стоит злоупотреблять этой возможностью, поскольку такие свойства выбиваются из общей сортировки по именам – они отображаются в самом верху окна свойств (PropertyGrid) или соответствующей категории.

Коллекции

Для сериализации свойств, которые являются коллекциями элементов, используйте атрибут DesignerSerializationVisibility со значением DesignerSerializationVisibility.Content. При этом у коллекции должен быть метод Add и/или AddRange, принимающий массив объектов и типизированный индексер. Дизайнер сериализует коллекции, используя этот метод, например, так:

        this.mainMenu.MenuItems.AddRange(new System.Windows.Forms.MenuItem[] { this.menuItem1 });

Тогда как в классе Menu:

[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public MenuItemCollection MenuItems
{
  get
  {
    if (itemsCollection == null)
      itemsCollection = new MenuItemCollection(this);
    return itemsCollection;
  }
}

В классе коллекции имеется:

        public
        virtual
        void AddRange(MenuItem[] items)
{
  if (items == null)
          thrownew ArgumentNullException("items");
    for (int i = 0; i < items.Length; i++)
    Add(items[i]);
}

Если вы не зададите собственный редактор для своей коллекции, то будет использован CollectionEditor, который вполне сносно редактирует простые коллекции. Тем не менее, он обладает рядом недостатков. В частности, CollectionEditor использует тип индексера для определения типа объекта, который он создает при нажатии кнопки Add. Изменить это очень просто, достаточно создать редактор, производный от CollectionEditor и переопределить один метод.

        public
        class MyCollectionEditor : CollectionEditor
{
  public MyCollectionEditor()
    : base(typeof(MyCollection))
  {}

  protectedoverride Type[] CreateNewItemTypes() 
  {
    returnnew Type[] {typeof(MyItem1), typeof(MyItem2), typeof(MyItem3)};
  }
}

Таким образом, вы сообщите редактору коллекций, что в вашей коллекции могут находиться элементы типа MyItem1, MyItem2 и MyItem3. Если вы возвращаете более одного типа, то редактор поместит рядом с кнопкой Add «стрелочку», нажав на которую пользователь сможет выбрать нужный тип. При нажатии на Add создаётся элемент самого первого типа в возвращаемом из CreateNewItemTypes() массиве. Помните, однако, что все типы, создаваемые редактором коллекций должны иметь конструктор без параметров.

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

[Editor(typeof(MyCollectionEditor), typeof(UITypeEditor))]
public MyCollection Items { get { ... } }

Такой атрибут можно поместить как на класс коллекции, чтобы все свойства такого типа редактировались вашим редактором, так и на конкретное свойство в конкретном классе.

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

Если у вас есть некоторый собственный тип, который используется в качестве типа свойства в контроле, то дизайнер не будет знать, как с ним работать, до тех пор, пока вы ему этого не расскажете. Примерами таких типов могут быть Size, Point или Rectangle. Самый простой способ разрешить эту проблему – добавить простые свойства вроде Width и Height и редактировать через них, а составное свойство спрятать от дизайнера [Browsable(false)] и сериализатора [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]. Однако это далеко не всегда удобно, да и при большом количестве таких свойств это перегрузит окно свойств. Поэтому рекомендуется для таких типов создавать конвертор типа (TypeConverter). Конвертор типа – это класс, производный от TypeConverter или его наследника и определяющий ряд операций по преобразованию типов. Основными преобразованиями, используемыми дизайнером, являются преобразование в строку и из строки, а также преобразование в InstanceDescriptor, используемое при сериализации объекта в код. Пример конвертора вы можете найти в приложении к статье, здесь же перечислим основные вещи, которые необходимо помнить при создании своего конвертора.

Основными операциями конвертора являются следующие:

При реализации этих методов не забывайте вызывать базовый класс, если не знаете, как поступить с некоторым типом.

В большинство методов конвертора передаётся информация о культуре, с которой в настоящий момент нужно работать. Не игнорируйте этот параметр, используйте CultureInfo при операциях над списками, валютой, временем, датами и другими зависимыми от региональных установок типами данных. При отсутствии информации о культуре используйте CultureInfo.CurrentCulture. Например, если вы конвертируете из строки в ваш тип, и строка представляет список значений (например, “x; y”), то можно поступить так:

        public
        override
        object ConvertFrom(ITypeDescriptorContext context, 
  CultureInfo culture, object value) 
{
  string stringValue = value asstring;
  if (stringValue != null) 
  {
    // убираем лишние пробелы слева и справа
    stringValue = stringValue.Trim();
    if (culture == null)
      // берем текущую культуру, если не задано
      culture = CultureInfo.CurrentCulture;

    // разделяем строку на части используя разделитель списка из культурыstring[] strs = stringValue.Split(culture.TextInfo.ListSeparator[0]);

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

Для конвертации вложенных данных используйте TypeConverter.GetConverter(Type) для получения конвертера типа и используйте его методы ConvertTo, ConvertFrom соответственно. Старайтесь ничего не предполагать об используемых вами типах, особенно если они находятся в другой сборке – они могут измениться без вашего ведома. Вероятность того, что автор этих типов сохранит совместимость на уровне TypeConverter, значительно выше. Старайтесь при изменении типов, конструкторов, добавлении и изменении свойств сохранять обратную совместимость хотя бы на уровне своего конвертора. В этом случае формы, созданные с предыдущей версией вашего контрола смогут быть легко сконвертированы дизайнером автоматически.

Для сериализации в код предоставьте конвертацию в тип InstanceDescriptor используя ConstructorInfo. Если ваш объект целиком описывается конструктором, то используйте вариант InstanceDescriptor(MemberInfo, ICollection); Если необходимо дополнительное задание свойств объекта, используйте конструктор по умолчанию и InstanceDescriptor(MemberInfo, ICollection, bool); с последним параметром false. При этом в процессе сериализации в код для вашего объекта будут установлены все модифицируемые (settable) свойства, не равные значению по умолчанию (или для которых метод ShouldSerializeNNNN вернул true). Заметьте, что дизайнер не знает, что именно вы инициализировали в конструкторе, поэтому выставит все имеющиеся свойства, а следовательно передавать какие-либо данные в конструкторе особого смысла не имеет. При необходимости будет создана временная переменная внутри метода InitializeComponent, например, если сложный объект не является компонентом. Аналогично, если происходит сериализация коллекции элементов, не являющимися компонентами, то при полной инициализации конструктором объекты будут создаваться прямо внутри AddRange. В противном случае будут созданы временные объекты.

Если некоторое свойство возвращает ссылку на объект, можно сделать так, чтобы в окне свойств рядом с названием такого свойства появился «+», а при раскрытии ветки – возможность редактировать отдельные свойства объекта. В отсутствие setter-ов свойств, редактирование значений в окне свойств будет недоступно. Чтобы изменить это, можно реализовать методы CreateInstance и GetCreateInstanceSupported. В этом случае при изменении свойств объект создается заново при помощи CreateInstance. Если GetCreateInstanceSupported возвращает false, то редактировать свойства такого объекта будет нельзя.

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

По умолчанию для классов, производных от Component используется ComponentConverter, который для свойства этого типа позволяет посмотреть его свойства и выбрать в выпадающем меню один из компонентов подходящего типа. Если раскрытие свойств не желательно, например, по причине большого их количества, используйте [TypeConverter(typeof(ReferenceConverter))] для свойства или всего типа.

Некоторые стандартные конверторы типов:

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

Несколько в стороне от общей системы режима дизайна находится возможность расширения компонентов дополнительными свойствами. Иными словами, компонент, находящийся в некотором контейнере, например, форме, имеет возможность добавить ряд «виртуальных» свойств к другим компонентам в этом же контейнере. Эти псевдо-свойства отображаются как свойства расширенного компонента, однако на самом деле задание и считывание их значений осуществляются через компонент-расширитель. Это делается посредством интерфейса 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) // не наш типreturnfalse;
  if (ext == this) // не расширяем себяreturnfalse;

  // дальше, собственно, логика расширенияif (...) 
    returntrue;
  returnfalse;
}

Учтите, что система дизайна вызывает 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 { returnbase.Site; }
  set
  {
    base.Site = value;
    // получаем сервис
    IPropertyValueUIService uiService = 
      (IPropertyValueUIService)this.GetService(
      typeof(IPropertyValueUIService));
    // если сервис присутствует, добавляем свой обработчикif( uiService != null )
      uiService.AddPropertyValueUIHandler(
        new PropertyValueUIHandler(MyPropertyValueUIHandler));
  }
}

// описываем обработчик, который выбирает редактируемые свойства, // помеченные атрибутом [Bindable(true)]privatevoid 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") );
  }
}

// обработчик нашего встроенного элементаprivatevoid MyUIHandlerInvoke(ITypeDescriptorContext context, 
  PropertyDescriptor propDesc, PropertyValueUIItem item)
{
  MessageBox.Show("Clicked: " + propDesc.DisplayName 
    + " on " + context.Instance.ToString());
}

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

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

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

Локализация

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

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

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

Используйте атрибут Localizable с параметром true, чтобы указать, что данное свойство является локализуемым. Свойства, не предназначенные для пользователя, и не влияющие на отображение и расположение контрола, помечайте атрибутом [Localizable(false)], чтобы уменьшить объем ресурсов. Например, свойство object Tag { get; set; } обычно является нелокализуемым.

Помните о том, что существуют языки, в которых нормальным направлением текста является справа налево, например, Арабский или Иврит. Для этого существует свойство RightToLeft. Если в вашем контроле существует определенная ориентация представления информации, вам понадобится учитывать это свойство. Например, выравнивание текста по умолчанию производится к правой границе, полосы прокрутки расположены слева от контрола, информация о прогрессе (ProgressBar) отображаются справа налево и так далее.

Если в вашем контроле используется какой-либо вид выравнивания, используйте ContentAlignment для физического выравнивания, независимого от свойства RightToLeft. Используйте StringAlignment, если необходимо выравнивать строки и учитывать RightToLeft. При этом значение Near означает слева для RightToLeft.No и справа для RightToLeft.Yes, значение Far, соответственно, наоборот.

Источники данных

Связь контролов со структурами данных, такими, как DataTable, коллекциями или просто объектами сокращает количество кода и упрощает разработку. В Windows Forms за это отвечает механизм Data Binding. К сожалению, Data Binding имеет, пожалуй, самую неудачную реализацию из всей библиотеки .NET Framework. По этой причине многие авторы библиотек компонентов реализуют свою собственную стратегию связи с данными. Если же вы решитесь-таки использовать Data Binding, при создании контролов вам понадобится следовать нескольким простым правилам.

Привязка к данным может быть двух видов – к записи или списку. Некоторые контролы, вроде ListBox привязываются к источнику данных (DataSource) и отображают все его элементы тем или иным способом. Другие, вроде TextBox, могут быть привязаны к некоторой колонке текущей записи в источнике данных, и при смене позиции их содержимое обновляется. Источником также может служить обычная коллекция, не имеющая отношения к базам данных.

Свойства, которые могут быть связаны с данными, помечайте атрибутом [Bindable(true)]. В этом случае они появятся в выпадающем списке (DataBindings), и к ним можно будет привязать источник. Очевидно, что такие свойства должны быть доступны как на чтение, так и на запись. Если атрибут Bindable не использован или использован со значением false, то к свойству всё равно можно подсоединить источник данных, хотя и без гарантий, что всё будет работать как нужно. Для этого надо открыть пункт Advanced в списке (DataBindings).

Предоставляйте событие <PropertyName>Changed, где PropertyName – имя свойства. Пользуясь этим событием, среда разработки и другие компоненты смогут узнать об изменении свойства и обновить информацию в связанных данных. Система связи с данными также подписывается на событие Validating, если оно есть. При этом происходит попытка передать данные в источник данных для обновления, а при ошибке выставляется флаг Cancel в CancelEventArgs.

Если вы предоставите свойство <PropertyName>IsNull типа bool, то в случае true значением свойства будет считаться DbNull, а само свойство опрашиваться не будет.

Если вы разрабатываете контрол, который должен привязываться ко всему источнику данных, используйте CurrencyManager в качестве менеджера связи. Название этого класса неудачно, но именно этот наследник BindingManagerBase используется в контролах Windows Forms для управления связью со списками. Для связи с одним объектом используется другой класс – PropertyManager.

Доступность

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

Для передачи базовой информации о доступности в систему, используйте свойства AccessibleName, AccessibleDescription, AccessibleDefaultActionDescription и AccessibleRole.

Если вам нужно больше контроля над информацией о доступности, придётся подготовить специальный объект, которым воспользуется система. Для этого перегрузите метод CreateAccessibilityInstance и верните экземпляр класса, производного от AccessibleObject. Для упрощения жизни есть также класс Control.ControlAccessibleObject, берущий часть забот на себя.

Разработчикам, создающим программные продукты на экспорт, а особенно тем, кто взаимодействует с правительственными структурами США и европейских стран обязательно придётся столкнуться с такими понятиями, как Section 508 (http://www.section508.gov/) и аналогичными. Однако, полноценный обзор разработки приложений с учётом доступности выходит за рамки этой статьи.

Заключение

В этой статье мы рассмотрели основные принципы и методы создания собственных компонентов и контролов для среды .NET и подсистемы Windows Forms. Далеко не все возможности были раскрыты, не все варианты рассмотрены, но я надеюсь, что эта статья поможет вам начать проектировать и реализовывать собственные качественные и полезные компоненты. Экспериментируйте, изучайте и не останавливайтесь на достигнутом, ведь хороший компонент неоднократно сослужит вам, а может и другим пользователям, добрую службу. В заключение хотелось бы еще раз повторить главные принципы разработки хороших компонентов:

Ссылки

Для получения начальных знаний о том, что такое компоненты, и как с ними работать, рекомендуется прочитать статью Влада Чистякова. В ней рассказывается о базовых концепциях компонентной модели библиотеки классов .NET, даны рекомендации по созданию собственных компонентов и раскрыты некоторые нетривиальные приёмы и способы взаимодействия с дизайнером Visual Studio .NET.

Подробную информация о методах, свойствах, типах и интерфейсах смотрите в MSDN. Помните, MSDN – это документация, написанная создателями того, о чём мы с вами говорим. Несмотря на то, что зачастую информация в MSDN не полна и местами запутана, это всё таки лучший источник знаний о создании программного обеспечения на платформе Windows вообще и .NET в частности.

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

Прочитайте Design Guidelines for Class Library Developers перед тем, как браться за создание собственных контролов и компонентов, особенно если вы собираетесь выпускать ваш контрол или библиотеку в большой мир. Это сохранит нервы вам, вашим клиентам, а также избавит от многих звонков и электронных писем в службу поддержки.

Приложения

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

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

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

[Serializable]
[TypeConverter(typeof(SpaceSizeConverter))]
publicstruct SpaceSize
{
  privateint _left;
  privateint _top;
  privateint _right;
  privateint _bottom;

  public SpaceSize(int left, int top, int right, int bottom)
  {
    _left = left;
    _top = top;
    _right = right;
    _bottom = bottom;
  }

  publicint Left { get { return _left; } }
  publicint Top { get { return _top; } }
  publicint Right { get { return _right; } }
  publicint Bottom { get { return _bottom; }  }
}

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

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

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

        public
        class SpaceSizeConverter : TypeConverter

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

        public
        override
        bool 
  CanConvertFrom(ITypeDescriptorContext context, Type sourceType) 
{
  if (sourceType == typeof(String))
    returntrue;
  returnbase.CanConvertFrom(context, sourceType);
}

publicoverridebool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
{
  if (destinationType == typeof(InstanceDescriptor))
    returntrue;
  if (destinationType == typeof(string))
    returntrue;
  returnbase.CanConvertTo(context, destinationType);
}

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

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

        public
        override
        object ConvertFrom(ITypeDescriptorContext context, 
  CultureInfo culture, object value)
{
  if (value isstring) 
  {
    try 
    {
      // убираем лишние пробелы слева и справаstring str = ((string)value).Trim();
      // мы не должны возвращать null, поскольку SpaceSize – value-typeif (str.Length == 0)
        returnnew SpaceSize(0,0,0,0);

      if (culture == null)
        // берем текущую культуру, если не задано
        culture = CultureInfo.CurrentCulture;

      // разделяем строку на части используя разделитель списка из культурыstring[] strs=((string)value).Split(culture.TextInfo.ListSeparator[0]);
      if (strs.Length != 4) // проверяем, что получилось 4 элементаthrownew ArgumentException("Can not convert '" 
          + (string)value + "' to type SpaceSize");

      int[] nums = newint[strs.Length];
      
      // используем конвертер для целых чисел и преобразуем каждый элемент
      TypeConverter typeConverter = TypeDescriptor.GetConverter(typeof(int));
      for (int i = 0; i < nums.Length; i++)
        nums[i] = (int)typeConverter.ConvertFromString(
          context, culture, strs[i]); 
      // создаем новый объектreturnnew SpaceSize(nums[0], nums[1], nums[2], nums[3]);
    }
    catch 
    {
      // грубая обработка ошибокthrownew ArgumentException("Can not convert '" 
        + (string)value + "' to type SpaceSize");
    }
  }  
  returnbase.ConvertFrom(context, culture, value);
}

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

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

        public
        override
        object ConvertTo(ITypeDescriptorContext context,CultureInfo culture, object value, System.Type destinationType) 
{
  if (destinationType == typeof(string) && value is SpaceSize)
  {
    // преобразуем к нашему типу
    SpaceSize spaceSize = (SpaceSize)value;
    if (culture == null) 
      // используем культуру по умолчанию, // если она не была передана в параметре
      culture = CultureInfo.CurrentCulture;

    // получаем разделитель спискаstring str = culture.TextInfo.ListSeparator + " ";

    // получаем конвертор типа для целых чисел // и преобразуем каждое свойство к строке
    TypeConverter typeConverter = TypeDescriptor.GetConverter(typeof(int));
    string[] strs = newstring[4];
    int i = 0;
    strs[i++] = typeConverter.ConvertToString(
      context, culture, spaceSize.Left);
    strs[i++] = typeConverter.ConvertToString(
      context, culture, spaceSize.Top);
    strs[i++] = typeConverter.ConvertToString(
      context, culture, spaceSize.Right);
    strs[i++] = typeConverter.ConvertToString(
      context, culture, spaceSize.Bottom);
    // объединяем строки в одну и возвращаем результатreturn String.Join(str, strs);
  }

  // здесь будет добавлена конвертация в InstanceDescriptor ...returnbase.ConvertTo(context, culture, value, destinationType);
}

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

        if (destinationType == typeof(InstanceDescriptor) && value is SpaceSize)
{
  SpaceSize spaceSize = (SpaceSize)value;

  // получить информацию о конструкторе с данными типами параметров
  ConstructorInfo constructorInfo = typeof(SpaceSize).GetConstructor(
    new Type[]{ typeof(int), typeof(int), typeof(int), typeof(int) }
    );
  
  if (constructorInfo != null)
    // создать описатель экземпляра из информации о конструкторе // и конкретных параметров конструктораreturnnew InstanceDescriptor(constructorInfo, 
        newobject[]{spaceSize.Left, spaceSize.Top, spaceSize.Right, spaceSize.Bottom});
}

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

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

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

        public
        override
        object CreateInstance(ITypeDescriptorContext context, 
  IDictionary propertyValues)
{
  returnnew SpaceSize((int)propertyValues["Left"], 
                       (int)propertyValues["Top"], 
                       (int)propertyValues["Right"], 
                       (int)propertyValues["Bottom"]);
}
publicoverridebool GetCreateInstanceSupported(ITypeDescriptorContext context)
{
  returntrue;
}

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

        public
        override PropertyDescriptorCollection 
  GetProperties(ITypeDescriptorContext context, 
    object value, Attribute[] attributes)
{
  return TypeDescriptor.GetProperties(typeof(SpaceSize), attributes).Sort(
    newstring[]{"Left", "Top", "Right", "Bottom"});
}

publicoverridebool GetPropertiesSupported(ITypeDescriptorContext context)
{
  returntrue;
}

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


Эта статья опубликована в журнале RSDN Magazine #2-2004. Информацию о журнале можно найти здесь
    Сообщений 0    Оценка 660        Оценить