.Net – классы, компоненты и контролы

Часть 1. Компоненты

Автор: Владислав Чистяков
The RSDN Group

Источник: RSDN Magazine #3
Опубликовано: 09.05.2003
Версия текста: 1.0.1
Введение
Компоненты
Класс Component и интерфейс IComponent
CodeDom
Ручная сериализация
Свойства
Иконка компонента
Дизайнер компонентов
MarshalByValueComponent
Редактор типов (UITypeEditor)
Заключение

Введение

Такой метод создания ПО, как компонентное программирование, появился относительно недавно. Его можно охарактеризовать как технологию создания ПО из готовых блоков. Т.е. программисты пытаются украсть идею у строителей, занимающихся крупнопанельным домостроением. Создание ПО из компонентов подразумевает, что компоненты будут добавляться к проекту во время разработки. При этом будет производиться их начальная настройка. Компоненты как таковые не подразумевают (вернее сказать, не обязаны иметь) пользовательского интерфейса (ни для программиста, ни для конечного пользователя). В этом качестве выступают части IDE и дополнительные программные дизайнеры. Первой компонентной средой был продукт, купленный Microsoft на заре своего существования. Впоследствии на его базе родился VB. Далее была Delphi… в общем, к концу двадцатого века компоненты стали поддерживаться почти везде (даже в Visual C++, хотя он и по сей день не очень-то визуальный).

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

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

Компоненты

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

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

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

Интересно, что практически любой класс в .NET отвечает этим требованиям. Метаинформация создается для любого элемента класса (будь он трижды скрытым), экземпляр любого класса можно динамически создать, и любой класс помещается в сборки (один или более исполнимых модулей), которые можно распространять независимо. В общем, можно было бы спокойно сказать, что любой класс в .NET – это компонент, если бы… Если бы не наличие отдельного класса с именем Component.

Класс Component и интерфейс IComponent

Класс Component является стандартной реализацией интерфейса IComponent, а интерфейс IComponent указывает на то, что реализующий его класс является компонентом. Зачем, спросите вы? Ведь, по сути, в .NET любой класс является компонентом! Все очень просто. Хотя это и не обязательно, но обычно, когда говорят о компонентах, подразумевают, что экземпляры класса будут использоваться в той или иной визуальной среде разработки (RAD). При этом для тесной интеграции компонента с IDE ему может понадобиться обратная связь с этой самой IDE. Такая обратная связь предоставляется через так называемый сайт. Сайт – это некоторая абстракция, ассоциируемая с каждым компонентом и дополняющая компонент специфической для контейнера информацией, например, такой, как имя компонента (в VS.NET у компонента во время исполнения имени нет, оно есть только во время разработки), а также обеспечивающая обратную связь с контейнером. В .NET сайт реализуется через интерфейс ISite. ISite назначается компоненту через свойство IComponent.Site. IComponent к тому же происходит от интерфейса IDisposable, который отвечает за освобождение unmanaged-ресурсов. Но это скорее уже дополнительная функция, которая, хотя и часто встречается, необходимой не является.

Создать компонент очень просто. Нужно создать сборку (в виде DLL, в визарде проектов VS.NET нужно выбрать проект «Class Library»), в которую поместить класс, унаследованный от Component (который также можно создать с помощью визарда – если в контекстном меню проекта выбрать «Add->Add New Item» и затем выбрать «Component Class») или IComponent. Вот простой вариант (на C#):

namespace TestNameSpace 
{
  public class TestComponent : System.ComponentModel.Component
  {
    private System.ComponentModel.IContainer components;
    public TestComponent(System.ComponentModel.IContainer container)
    {
      /// <summary>
      /// Required for Windows.Forms Class Composition Designer support
      /// </summary>
      container.Add(this);
      InitializeComponent();

      //
      // TODO: Add any constructor code after InitializeComponent call
      //
    }
    public TestComponent()
    {
      /// <summary>
      /// Required for Windows.Forms Class Composition Designer support
      /// </summary>
      InitializeComponent();

      //
      // TODO: Add any constructor code after InitializeComponent call
      //
    }
    #region Component Designer generated code
    /// <summary>
    /// Required method for Designer support - do not modify
    /// the contents of this method with the code editor.
    /// </summary>
    private void InitializeComponent()
    {
      components = new System.ComponentModel.Container();
    }
    #endregion
  }
}

Данный код создан визардом, хотя ничто не мешает создать его вручную.

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


Рисунок 1. Дизайнер компонентов VS.NET.

Самое интересное, что сериализация компонентов (в том числе и в дизайнере самого компонента) по возможности осуществляется в код. Так, если открыть дизайнер компонента (двойным щелчком на соответствующем файле в «Solution Explorer», см. рисунок 1) и бросить на его поверхность компонент ImageList (из Toolbox, палитры компонентов), то в описание компонента будет добавлена строка, описывающая переменную-член, описывающую добавленный ImageList:

private System.Windows.Forms.ImageList imageList1;

А в метод InitializeComponent – инициализация этого компонента:

this.imageList1 = new System.Windows.Forms.ImageList(this.components);
// 
// imageList1
// 
this.imageList1.ColorDepth = System.Windows.Forms.ColorDepth.Depth8Bit;
this.imageList1.ImageSize = new System.Drawing.Size(16, 16);
this.imageList1.TransparentColor = System.Drawing.Color.Transparent;
ПРИМЕЧАНИЕ

Имя InitializeComponent используется также при создании форм и контролов. В конце концов, форма и контролы – тоже компоненты.

«По возможности» – означает, что то или иное свойство не удается превратить в код, содержимое свойства сериализуется в поток (Stream) который записывается в ресурсы. В код при этом вставляется вызов функции, загружающий объект из ресурсов. Для сериализации в Stream объект должен или быть помечен атрибутом Serializable или реализовать интерфейс ISerializable. Так, если выделить брошенный нами ImageList и в его свойствах выбрать свойство Images, нажать на появившуюся кнопку и добавить картинки, то в InitializeComponent будет добавлено еще две строчки:

System.Resources.ResourceManager resources = 
    new System.Resources.ResourceManager(typeof(TestComponent));
...
// 
// imageList1
// 
...
this.imageList1.ImageStream = ((System.Windows.Forms.ImageListStreamer)
    (resources.GetObject("imageList1.ImageStream")));

Первая создает и инициализирует вспомогательный объект – ResourceManager. Вторая загружает (из ресурсов) экземпляр ImageListStreamer и помещает получившееся значение в свойство ImageStream компонента ImageList.

Сам поток сохраняется в ресурсы дизайнером (этот код скрыт в недрах VS.NET).

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

Сам дизайнер (компонентов, форм или любой иной) загружает состояние создаваемого компонента или формы путем компиляции кода инициализации. Для упрощения своей задачи VS помечает генерируемый и компилируемый в дизайн-тайме код специальной директивой препроцессора #region (и именем региона «Component Designer generated code»). VS честно пытается скомпилировать код, находящийся внутри InitializeComponent, но (при генерации кода) безжалостно затирает все содержимое этого метода. Можете произвести следственный эксперимент. Замените строку:

this.imageList1.ImageStream = ((System.Windows.Forms.ImageListStreamer)
    (resources.GetObject("imageList1.ImageStream")));

На:

this.imageList1.ImageStream = ((System.Windows.Forms.ImageListStreamer)
    (resources.GetObject("imageList1" + ("." + "ImageStream"))));

Сохраните файл. Закройте дизайнер. Откройте его еще раз. Сдвиньте ImageList. Это нужно для того, чтобы дизайнер понял, что состояние компонента изменилось, и его нужно снова сериализовать в код. Переключитесь в код... И вы увидите, что строка, задающая имя ресурса, снова изменилась на "imageList1.ImageStream".

Для пущей визуальности можно разбить окно VS на два и поместить код и дизайнер компонента в разные области (это делается путем перетаскивания закладки в центр редактируемой области).

Если не редактировать ничего в дизайнере компонента, можно скомпилировать проект и убедиться (под отладчиком) что в runtime прекрасно работает код, занимающийся конкатенацией.

Как альтернативу этому эксперименту можно предложить описать и инициализировать переменную внутри InitializeComponent. Если переменная будет простого типа, то она просто бесследно исчезнет (после первой записи содержимого дизайнера). Если же объявить компонент, то он появится в дизайнере (при его активизации) и превратится в член класса при первом же изменении кода (вернее, при первой же сериализации дизайнера компонента в код).

Все это очень похоже на сериализацию компонентов и контролов в таких RAD-средах, как Delphi и VB. За тем лишь исключением, что VB и Delphi сериализуют компоненты в специальные структурированные хранилища, имеющие синтаксис, очень похожий на код, а VS.NET генерирует стопроцентный код! Попробуйте произвести какие-либо расчеты в *.dfm или *.frm файлах внутри инициализации компонентов. Красноречию дизайнеров не будет предела. В том, что это спокойно пройдет в VS.NET, мы уже убедились.

CodeDom

Как же устроена генерация и компиляция кода в VS.NET? Сможет ли простой смертный познать тайну мудрецов, писавших эту среду? И можно ли заставить эти механизмы работать без VS.NET? Да и как VS.NET умудряется генерировать и компилировать код для одного и того же компонента под такие разные языки, как VB.NET и C#?

Дело в том, что генерация и дизайнтайм-компиляция кода осуществляются VS не напрямую, а с помощью механизма называемого CodeDom. CodeDom – это нечто вроде XmlDom-парсера, но не для XML, а для кода. В принципе, это и есть ответ на вопросы из предыдущего абзаца, но думаю, что вы потребуете более глубокого объяснения. Не так ли? :) Пожалуйста...

Спецификация CLI (Common Language Infrastructure) определяет набор требований к языкам программирования. Эти ограничения позволяют создать универсальные генераторы кода под языки, совместимые с CLI. Для того, чтобы VS.NET поняла язык и смогла создавать на нем формы и ASP.NET-страницы, нужно, чтобы для этого языка был реализован CodeDom-провайдер. Microsoft на сегодня поставляет два CodeDom-провайдера:

Имеется также ряд провайдеров от независимых поставщиков. А в ближайшем будущем Microsoft планирует выпустить VS.NET 1.1 (а соответственно, и .NET SDK 1.1) в который также войдет провайдер для Java (вернее J#) и, скорее всего, долгожданный провайдер для C++.

Дизайнер получает указатель на интерфейс ICodeGenerator, которому можно передать абстрактную структуру, состоящую из отдельных веток (каждая из этих веток представляет атомарный участок кода), и превращает его в текст на некотором языке (зависящем от выбранного провайдера). Эти сервисы документированы (хотя и не лучшим образом). Ну и главное, что реализации провайдеров для VB.NET и C# поставляются в составе .NET-рантайм (даже не SDK!).

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

Пример генерации и компиляции кода через CodeDom

Вот пример генерирующий код простого приложения типа "Hello World!" :):

string sModuleName = @"Y:\RSDN\2002-3\dotNet-Controls\test";
string sVbPath = sModuleName + ".vb";
string sCsPath = sModuleName + ".cs";
// StreamWriter для записи в файлы.
using(StreamWriter swCs = new StreamWriter(sCsPath, false))
using(StreamWriter swVb = new StreamWriter(sVbPath, false))
// CodeDom-провайдеры (VB и C#)
using(CodeDomProvider cdpCS = new Microsoft.CSharp.CSharpCodeProvider())
using(CodeDomProvider cdpVB = new Microsoft.VisualBasic.VBCodeProvider())
{
  // Создаем CodeDom-описание пространства имен "TestNs".
  CodeNamespace cns = new CodeNamespace("TestNs");
  // Добавляем ссылку на пространство имен "System".
  cns.Imports.Add(new CodeNamespaceImport("System"));
  // Создаем CodeDom-описание класса "TestClass".
  CodeTypeDeclaration TestClass = new CodeTypeDeclaration("TestClass");
  // Добавляем его к списку типов, входящих в пространство имен "TestNs".
  cns.Types.Add(TestClass);
  // Создаем CodeDom-описание метода Main (стартовой точки приложения).
  CodeEntryPointMethod MethodMain = new CodeEntryPointMethod();
  // Создаем CodeDom-описание вызова WriteLine("Hello World!")
  CodeMethodInvokeExpression cs1 = new CodeMethodInvokeExpression(
    new CodeTypeReferenceExpression("System.Console"), "WriteLine", 
      new CodePrimitiveExpression("Hello World!"));
  // Добавляем его к списку выражений метода Main.
  MethodMain.Statements.Add(new CodeExpressionStatement(cs1));
  // Добавляем описание метода Main к описанию класса.
  TestClass.Members.Add(MethodMain);

  // Создаем CodeDom-провайдер для VB.
  ICodeGenerator cg = cdpVB.CreateGenerator();
  // Генерируем код нашего приложения на VB.
  cg.GenerateCodeFromNamespace(cns, swVb, new CodeGeneratorOptions());

  // Создаем CodeDom-провайдер для C#.
  cg = cdpCS.CreateGenerator();
  // Генерируем код нашего приложения на C#.
  cg.GenerateCodeFromNamespace(cns, swCs, new CodeGeneratorOptions());

  // Компилируем исполняемый файл непосредственно из CodeNamespace.
  // Однако нам придется указать, какой компилятор будет использоваться.

  // Создаем CodeCompileUnit.
  CodeCompileUnit ccu = new CodeCompileUnit();
  // Добавляем в него наше пространство имен.
  ccu.Namespaces.Add(cns);
  // Задаем имя исполняемого файла в CompilerParameters.
  CompilerParameters cp = new CompilerParameters(new string[0], sModuleName);
  // Указываем, что нам нужен исполнямый (exe) файл, а не dll.
  cp.GenerateExecutable = true;
  // Компилируем...
  ICodeCompiler cc = cdpVB.CreateCompiler();
  cc.CompileAssemblyFromDom(cp, ccu);

Этот код генерирует исходный код (уж простите за каламбур) на C# и VB.NET. Он также компилирует исполняемый модуль test.exe.

Вот полученные исходные коды:

(VB)

Imports System

Namespace TestNs
  
  Public Class TestClass
    
    Public Shared Sub Main()
      System.Console.WriteLine("Hello World!")
    End Sub
  End Class
End Namespace

(C#)

namespace TestNs {
  using System;
  
  public class TestClass {
    
    public static void Main() {
      System.Console.WriteLine("Hello World!");
    }
  }
}

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

Логика работы сериализатора VS.NET

Как вы, видимо, уже поняли, рантайм-сериализация и CodeDom-сериализация – это разные вещи. Но пока вы вряд ли сможете оценить, насколько это разные вещи! CodeDom-сериализация производится в два этапа. На первом создается «дерево кода», а на втором это дерево превращается в текст на необходимом языке. Во вторую часть процесса вмешаться невозможно (и не нужно), а вот в первую можно. Итак, каков же алгоритм?

VS.NET:

  1. Создает экземпляр CodeDom-генератора.
  2. Перебирает все публичные (public) свойства сериализуемого компонента (это может быть, как уже было сказано, просто компонент, форма, контрол и т.п.).
  3. Определяет, подлежит ли свойство сериализации в код.
  4. Если подлежит, то сериализует содержимое свойства в CodeDom-дерево и передает его созданному на первом шаге CodeDom-генератору.
  5. CodeDom-генератор создает исходный текст, который помещается в надлежащее место.

При открытии разрабатываемого компонента среда просто находит нужный участок кода и запускает CodeDom-компилятор для компиляции кода инициализации. После этого с помощью скомпилированного кода она создает экземпляр компонента. Далее работа ведется только с этим экземпляром. Компонент (через свойство Site) может оповестить VS.NET, что он был изменен. При этом VS.NET помечает компонент как измененный, и перегенерирует код по описанной выше схеме. Таким образом, главным хранилищем данных является код.

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

Сериализация простых классов

Итак, для типов, реализующих IComponent, сериализация производится автоматически. Но компоненты могут иметь свойства типа структур или классов, которые не реализуют IComponent. Как быть в этом случае? В принципе неясно, почему Microsoft ввела такое ограничение, но, к сожалению, это так. Для обхода своего же ограничения и для увеличения гибкости была придумана идеология конвертера типов. Когда VS.NET пытается сериализовать свойство, тип которого она сериализовать не умеет, она пытается получить у этого типа (через рефлексию) значение атрибута TypeConverter. Этот атрибут указывает (или в строковом виде или в виде ссылки на тип) на класс, унаследованный от класса TypeConverter, и умеющий преобразовывать объекты искомого типа в некоторый другой тип. TypeConverter – это универсальная технология, позволяющая преобразовывать типы, но при сериализации VS.NET интересует только преобразование между исходным типом и InstanceDescriptor. InstanceDescriptor позволяет описать, как создать конкретный экземпляр объекта. То есть InstanceDescriptor содержит информацию, описывающую экземпляр. С его помощью VS.NET генерирует CodeDom-код конструктора.

В конечном счете, VS.NET получает (в текстовом виде) код конструктора. В итоге в описание формы (или разрабатываемого компонента) попадает примерно такой код:

this.lbl1.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F,
    System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, 
    ((System.Byte)(204)));
this. lbl1.Location = new System.Drawing.Point(712, 5);

Конструкторы для классов Font и Point были сгенерированны с помощью InstanceDescriptor, специально для этого написанным наследником от TypeConverter.

Наследник TypeConverter должен реализовать два метода:

public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)

и

public override object ConvertTo(ITypeDescriptorContext context, 
  CultureInfo culture, object value, Type destinationType)

Первый должен ответить на вопрос, может ли данный TypeConverter совершить требуемое преобразование:

public override bool CanConvertTo(ITypeDescriptorContext context, 
  Type destinationType)
{
  if(destinationType == typeof(InstanceDescriptor))
    return true;
  // Всегда вызываем базовый класс если не можем 
  // совершить преобразование сами.
  return base.CanConvertTo(context, destinationType);
}

Второй собственно и занимается преобразованием:

public override object ConvertTo(ITypeDescriptorContext context, 
  CultureInfo culture, object value, Type destinationType)
{
  SomeType obj = value as SomeType;

  if (destinationType == null)
    throw new ArgumentNullException("destinationType");
  if (destinationType == typeof(InstanceDescriptor) && obj != null) 
  {
    // Формируем список параметров и их описание...
    // Создаем и возвращаем InstanceDescriptor...
    return InstanceDescriptor(...);
  }
  return base.ConvertTo(context, culture, value, destinationType);
}

В задачи TypeConverter при преобразовании объекта в InstanceDescriptor входит выбор подходящего описания конструктора и заполнение списка значений его параметров. Эти значения берутся из сериализуемого объекта. Эта информация передается в InstanceDescriptor через его конструктор.

Описание конструктора передается в виде экземпляра класса MemberInfo. Его можно получить с помощью метода GetConstructor объекта Type, описывающего сериализуемый тип. Этому методу нужно передать массив, содержащий перечисление типов параметров конструктора.

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

Задача усложняется тем, что количество вариаций параметров конструктора сериализуемого типа может быть довольно велико. Нужно нагородить кучу if-ов. Нагромождение это может быть довольно большим, так как каждый if содержит громоздкий код формирования двух массивов (со списком параметров и с описанием типов параметров). Для упрощения жизни я создал небольшой класс-помощник:

public __gc class InstanceDescriptorBuilder
{
protected:
  ArrayList * m_aryArgTypes;
  ArrayList * m_aryArgs;
  Type * m_type;
public:
  InstanceDescriptorBuilder(Type * type) : m_type(type)
  {
    m_aryArgTypes = new ArrayList(10);
    m_aryArgs = new ArrayList(10);
  }
  void Add(Type * type, Object * obj)
  {
    m_aryArgTypes->Add(type);
    m_aryArgs->Add(obj);
  }
  void Add(Object * obj)
  {
    m_aryArgTypes->Add(obj->GetType());
    m_aryArgs->Add(obj);
  }
  __property int get_Count() { return m_aryArgTypes->Count; }

  // Вот таким странным образом в MC++ объявляются методы и свойства, 
  // возвращающие ссылку на массив.
  __property Type __gc * get_Types() []
  {
    Type __gc * ary[] = new Type*[m_aryArgTypes->Count];
    m_aryArgTypes->CopyTo(ary);
    return ary;
  }
  __property Object __gc * get_Args() []
  {
    Object __gc * ary[] = new Object*[m_aryArgs->Count];
    m_aryArgs->CopyTo(ary);
    return ary;
  }
  InstanceDescriptor * ToInstanceDescriptor()
  {
    MemberInfo * MembInf = m_type->GetConstructor(Types);
    return new InstanceDescriptor(MembInf, 
      dynamic_cast<ICollection __gc *>(Args));
  }
};

Этот класс создан на C++, так как мне нужно было использовать его в MC++-проекте. Но его несложно переписать на C#. Основная его идея заключатся в том, что он позволяет

добавлять описания параметров простым вызовом метода Add вместо манипуляций с двумя массивами. Вот как можно использовать этот класс-помощник:

Object * ColumnTypeConverter::ConvertTo(ITypeDescriptorContext * context,
  CultureInfo * culture, Object * value, Type * destinationType)
{
  Column * pCol = dynamic_cast<Column __gc *>(value);
  
  if(destinationType == NULL)
    throw new System::ArgumentNullException(S"destinationType");
  if(destinationType == __typeof(InstanceDescriptor) && pCol != NULL)
  {
    InstanceDescriptorBuilder * cb = 
      new InstanceDescriptorBuilder(__typeof(Column));
    cb->Add(__box(pCol->Width));
    if(pCol->Name && pCol->Name->Length)
      cb->Add(pCol->Name);
    else
      cb->Add(__typeof(String), NULL);
    if(pCol->Alignment)
      cb->Add(__box(pCol->Alignment));
    if(pCol->Text)
      cb->Add(pCol->Text);
    if(pCol->Type != ColType::Normal)
      cb->Add(__box(pCol->Type));
    return cb->ToInstanceDescriptor();
  }
  return TypeConverter::ConvertTo(context, culture, value, destinationType);
}

Этот пример (написанный также на MC++) взят из кода контрола TreeGrid, который можно найти на прилагаемом к журналу CD.

В нем производится преобразование объекта Column (колонка) в InstanceDescriptor. С использованием InstanceDescriptorBuilder эта операция становится довольно простой. В приведенном примере производится анализ свойств объекта Column и, если они содержат значения, отличные от значений по умолчанию, в конструктор добавляется соответствующий параметр . В конце этого действа у InstanceDescriptorBuilder-а вызывается метод ToInstanceDescriptor, создающий и возвращающий InstanceDescriptor, описывающий сериализуемый объект.

Получив InstanceDescriptor VS.NET, сериализуем все параметры конструктора, которые были описаны в InstanceDescriptor. Процесс этот по своей природе рекурсивен. Так что если в качестве параметра конструктора окажется тип, реализующий TypeConverter, операция будет повторена для него. Таким образом, получается список вложенных конструкторов. Код может быть сложнее, если в процессе сериализации была применена ручная CodDom-сериализация.

Атрибут TypeConverter

Чтобы VS.NET могла узнать о том, что с типом ассоциирован конвертер, это нужно указать с помощью атрибута TypeConverterAttribute (или в сокращенной форме TypeConverter):

[TypeConverter(typeof(RowTypeConverter))]
public class Row : RowBase
{
  ...

Управление сериализацией отдельных свойств компонента

Итак, компоненты сериализуются в код автоматически. Но что делать, если некоторые свойства компонента сериализовать не нужно? Для управления сериализацией отдельных свойств используется атрибут DesignerSerializationVisibilityAttribute (в сокращенной форме DesignerSerializationVisibility). Этот атрибут может принимать значения из перечисления DesignerSerializationVisibility:

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

Hidden – позволяет скрыть свойство от сериализатора. Помеченные этим атрибутом свойства сериализоваться не будет.

Content – говорит сериализатору, что свойство должно сериализоваться «по содержимому». Это значение применяется для сериализации коллекций. Чтобы сериализовать свойство по содержимому, нужно, чтобы возвращаемый им объект реализовывал один из следующих методов:

public int Add(SomeElementType value);
public void AddRange(SomeElementType [] values) ;

Кроме этого объект должен реализовать интерфейсы IList и ICollection. Методы AddRange и Add определяются по имени и не имеют никакого отношения к этим интерфейсам.

Вот код, формируемый VS.NET при сериализации свойства MenuCommands компонента MenuCommand:

menuAction.MenuCommands.AddRange(new MenuCommand[]
  {miNewMessage, miReply, miDel, miSep1, miMarkRead, miMarkUnread, 
   miMarkConvRead, miMarkConvUnread, miMarkAllRead, miMarkAllUnread,
   miShowTree,miOpenAll
  });

Сами MenuCommand создаются и настраиваются как отдельные компоненты.

Зачем оно надо?

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

Многие дизайнеры прошлого поколения производили сериализацию в поток. Это давало довольно быструю загрузку приложения, что может только приветствоваться во время выполнения программы, но приводило к серьезным проблемам во время разработки. Дело в том, что в процессе разработки зачастую приходится переходить на новые версии компонентов. При этом сериализованное состояние компонентов может различаться, что приведет к тому, что компонент попросту не сможет загрузить старое состояние. Это происходит из-за того, что в потоке обычно не хранится формат записанных данных. Вместо этого компонент, загружая данные, самостоятельно определяет, что и в какой последовательности должно идти. В принципе, если сериализация осуществляется вручную, можно предпринять действия, позволяющие загрузить состояние из старых версий потоков, но при этом появляются дополнительные проверки и, как следствие, непроизводительные затраты. К тому же это довольно сложно и почти всегда приводит к нагромождению специфичного кода. Ну и, естественно, бинарный поток нельзя подправить вручную. Для обхода этих проблем были придуманы более высокоуровневые технологии сериализации, такие, как property bag в COM/VB или DFM в Delphi. Эти технологии предполагали сериализацию отдельных свойств компонента. За счет этого добавление свойств в компонент уже не вызывало особых проблем. Так как при загрузке со старой версии, в которой, например, отсутствует то или иное свойство, компоненту попросту не предлагается загрузить его состояние. При этом компонент попросту использовал для этого свойства значение по умолчанию. Естественно, что при следующей сериализации значение этого свойства сохранялось и никаких проблем не возникало. Вторым достоинством этого способа было то, что обычно сериализация производилась или непосредственно в текст (как в VB и последних версиях Delphi), или в структурированный бинарный вид (как в ранних версиях Delphi), который (с помощью дополнительной утилиты) можно было превратить в текстовую форму (и обратно). Текстовое представление позволяло в крайнем случае подправить что-нибудь руками. Собственно порча данных может произойти и без смены версий компонентов. Ну и, в конце концов, хранение состояния компонентов в текстовом виде позволяло делать генераторы кода и более удобно работать с версиями проекта. Системы контроля кода значительно лучше работают с текстовыми файлами, нежели с бинарными. Например, достаточно просто сравнить разные версии текстового файла.

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

Чтобы совместить высокую скорость загрузки компонентов в рантайме с удобством работы и хранения состояния компонентов во время разработки, способы сериализации для рантайма и дизайнтайма различались. В рантайме (т.е. при создании exe- или dll-модуля) состояние компонентов сохранялось в бинарный поток, а в дизайтайме в текстовый формат типа property bag или DFM. Это часто приводило к проблемам. Так, многие COM-дизайнеры, в том числе и VB, при сериализации в property bag поддерживали только значительно урезанную функциональность.

Сериализация в код позволяет избавиться от двойственности при сохранении состояния компонентов. Компоненты всегда сериализуются в единый формат. При создании приложения происходит компиляция в код, так что в конечном приложении нет никаких потоков. Загрузка формы происходит путем компиляции загрузочного кода. При этом, если у компонента появилось новое свойство, оно попросту не будет инициализировано (что приведет к использованию настроек по умолчанию). Более того, в конечном приложении отсутствует блок интерпретации сериализованного состояния, что приводит к ускорению загрузки приложения и уменьшению его размеров. Как и в случае с property bag, код загрузки можно эффективно хранить в системах контроля версий и при необходимости изменять вручную. Более того, появляется возможность компилировать проекты, созданные в VS.NET, в отсутствие VS.NET. Ну, и поскольку генерируемый код – это просто код на целевом языке, отпадает необходимость изучать дополнительные форматы.

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

Ручная сериализация

Сериализацией можно непосредственно управлять. Для этого нужно создать наследника CodeDomSerializer и переопределить его методы Serialize и Deserialize. Этого наследника нужно подключить к классу, для которого нужна ручная сериализация. Делается это с помощью атрибута DesignerSerializer (или RootDesignerSerializer). Я не буду в этой статье подробно рассматривать вопрос ручной сериализации, так как он заслуживает отдельной статьи. Скажу только, что с помощью ручной сериализации можно добиться довольно интересных эффектов. Если вас интересует эта тема, могу посоветовать посмотреть статью «.NET Shape Library: A Sample Designer» (http://gotdotnet.com/team/windowsforms/shapedesigner.aspx), в которой приводится пример работы с этой техникой.

Свойство Site

При загрузке компонента под управлением VS.NET в свойство Site помещается ссылка на интерфейс ISite. Это мало чем примечательный интерфейс. Вот список входящих в него свойств и методов:

[ComVisible(true)]
IComponent Component {get;}

Component – возвращает компонент, ассоциированный с сайтом.

[ComVisible(true)]
IContainer Container {get;}

Container – возвращает контейнер, в котором находится компонент.

[ComVisible(true)]
bool DesignMode {get;}

DesignMode – говорит, находится ли компонент в режиме разработки, то есть, загружен компонент в дизайнере типа VS.NET, или что компонент создан кодом приложения. В принципе, совершенно бесполезное свойство, так как когда компонент находится в рантайме, ему попросту не подключают site. А стало быть, проверить данное свойство совершенно невозможно.

[ComVisible(true)]
string Name {get; set;}

Свойство Name позволяет установить или считать имя компонента.

Container – возвращает ссылку на интерфейс IContainer, ассоциированный с сайтом. С помощью метода Components этого интерфейса можно добраться до формы и других компонентов и контролов. Например, следующий код выведет имена всех компонентов, лежащих на форме.

foreach(Component c in Site.Container.Components)
  System.Diagnostics.Trace.WriteLine(c.Site.Name);

В ISite, кроме документированных свойств, входит не упомянутый в документации метод GetService.

object GetService(Type service)

Функциональность интерфейса ISite, честно говоря, невелика, но этот метод расширяет ее до воистину потрясающих размеров. Дело в том, что GetService – это своеобразная замена QueryInterface из COM. Вот пример использования этого метода:

IExtenderProviderService eps = (IExtenderProviderService)value.GetService(
  typeof(IExtenderProviderService));

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

IMenuCommandService mcs = (IMenuCommandService)GetService(
  typeof(IMenuCommandService));
DesignerVerb dv = new DesignerVerb("Мое меню", 
  new EventHandler(MyMenuHandler));
mcs.AddVerb(dv);
...
public void MyMenuHandler(object sender, System.EventArgs e)
{
  MessageBox.Show("MenuHandler");
}

Свойства

Еще со времен VB 1.0 настройка состояния компонентов во время разработки производится через настройку свойств компонента. Собственно, для этого они и были придуманы. Как и в те незапамятные времена, настройка осуществляется через универсальный редактор свойств. В VS.NET он получил имя PropertyGrid. PropertyGrid в VS.NET только внешне похож на свой прототип из VB 1.0. Теперь его возможности могут быть расширены, причем довольно гибко. Но, в конце концов, гибкий редактор свойств уже не в новинку. В VB 6, и тем более в Delphi 7, редакторы свойств тоже предоставляли интерфейс расширения. А вот доступность редактора свойств в виде отчуждаемого элемента управления – это действительно оригинально! Причем PropertyGrid доступен даже на машинах, где не установлена VS.NET. Достаточно .NET Runtime. PropertyGrid можно даже использовать не по назначению. Так в диалогах конфигурации VS.NET используется именно PropertyGrid. Об опыте применения PropertyGrid в качестве средства конфигурации приложения рассказывается в статье Андрея Корявченко "Конфигурирование .NET-приложений" в этом номере журнала.

Интересно, что расширение возможностей PropertyGrid сделано не непосредственно, а через расширение настроек компонентов и подключение так называемых дизайнеров (см. ниже). Так, с помощью атрибутов можно задать описание свойства, категорию, к которой принадлежит свойство, и многое другое. Вот список атрибутов, применимых к свойствам и классу:

Название свойстваОписаниеОбласть применения
DefaultProperty(string Name)Указывает свойство, которое PropertyGrid должен активизировать по умолчанию.Класс
DefaultEvent(string Name)Указывает событие, которое PropertyGrid должен активизировать по умолчанию. Обработчик этого события также будет автоматически создаваться при двойном щелчке по компоненту (если не переопределено поведение действия, предпринимаемого по умолчанию).Класс
Browsable(bool)Запрещает отображать свойство в PropertyGrid. Это может быть полезно, если свойство не может быть настроено во время разработки, или если свойство может быть настроено другим (более простым) путем. Например, сложное комплексное свойство можно скрыть, а вместо него добавить несколько виртуальных свойств простых типов (о том, как это можно сделать, рассказано в разделе «Дизайнер компонентов»).Свойство
Category(string CategoryName)Позволяет задать категорию, к которой относится свойство или событие. PropertyGrid имеет два режима отображения: «по алфавиту» и «по категориям». Во втором режиме PropertyGrid группирует свойства и события по категориям. Имя категории может быть задано на любом языке и может содержать пробелы.Свойство, Событие
DefaultValueПозволяет определить для свойства значение по умолчанию. Если этот атрибут задан, PropertyGrid позволяет пользователю сбросить значение свойства в исходное состояние, а также позволяет не сохранять значение этого свойства, если его значение совпадает со значением по умолчанию. Это позволяет уменьшить размер конечного приложения и ускорить его загрузку.Свойство
DescriptionОписание свойства или события. PropertyGrid выводит его в своей нижней части.Свойство, Событие
DesignerПозволяет задать дизайнер для компонента (см. раздел «Дизайнер компонентов»).Класс
DesignerSerializationVisibilityПозволяет управлять сериализацией свойства (см. раздел «Управление сериализацией отдельными свойствами компонента»).Свойство
DesignOnlyОбычно назначается виртуальным свойствам, существующим только во время разработки.Свойство
EditorПозволяет назначить редактор для свойства или целого типа. Подробнее см. раздел «UITypeEditor».Класс, Свойство
EditorBrowsableУказывает, будет ли виден тот или иной элемент библиотеки (программы) в визуальных средствах и через IntelliSense.EditorBrowsableState.Never – никогда не виден.EditorBrowsableState.Always – всегда виден (по умолчанию).EditorBrowsableState.Advanced – виден «продвинутым» разработчикам. Такими считаются C#- и C++-программисты. VB- и Jscript-программисты, соответственно, пролетают. :)К чему угодно
MergablePropertyЕсли выбрано более одного компонента, PropertyGrid отображает (и, соответственно, дает возможность изменить) только те свойства, для которых данный атрибут установлен в true.Свойство
PropertyTabПозволяет ассоциировать свойство со страницей в PropertyGrid. Сама страница должна быть реализована.Свойство
ReadOnlyЕсли этот атрибут установлен, свойство будет невозможно изменить во время разработки.Свойство
RefreshPropertiesЕсли задано значение RefreshProperties.Repaint или RefreshProperties.All, VS.NET перечитывает свойства и перерисовывает PropertyGrid. Применяется, если от значения некоторого свойства зависят другие свойства.Свойство
TypeConverterО TypeConverter уже говорилось выше. Этот атрибут позволяет задать класс-конвертер, используемый для преобразования одного типа в другой. Если реализовать TypeConverter, преобразующий тип в строку и обратно, VS.NET будет выводить возвращаемую им строку в PropertyGrid. Более того, можно будет задавать состояние свойства, изменяя эту строку. Так, стандартные типы вроде шрифта и цвета поддерживают конвертацию в строку.Класс
ToolboxBitmapПозволяет задать пиктограмму для компонента. Задаваемое изображение будет отображаться в списке компонентов (окно Toolbox) и в Component-tray (в нижней части дизайнера форм, где изображаются невизуальные компоненты). Более подробно см. ниже.Класс

Иконка компонента

Задать иконку для компонента можно с помощью атрибута ToolboxBitmap. В качестве иконки можно задать битмап из ресурса, или находящийся на диске (по относительному пути).

Чтобы задать компонент из ресурса, в конструкторе атрибута нужно указать имя битмапа и любой тип, расположенный в том же пространстве имен и в той же сборке. Например:

[ToolboxBitmap(typeof(MyComponent), "MyComponent.BMP")]
public class MyComponent : System.ComponentModel.Component

При этом MyComponent.BMP должен быть подключен к проекту VS.NET и его свойство «Build Action» должно быть установлено в «Embedded Resource». Но это справедливо только для C# и VB.NET. В MC++ придется извращаться более сложным образом. В MC++ нужно открыть свойства проекта, найти раздел «Linker\Command Line» и дописать туда следующий текст:

/ASSEMBLYRESOURCE: MyComponent.BMP

Если вы хотите использовать иконку от уже имеющегося компонента, можно воспользоваться конструктором, задающим только тип. Например, следующий код задает компоненту иконку от контрола «Button» (кнопка):

public class MyComponent : System.ComponentModel.Component
 [System.Drawing.ToolboxBitmap(typeof(System.Windows.Forms.Button))]

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

[ToolboxBitmap("MyComponent.BMP")]
public class MyComponent : System.ComponentModel.Component

Дизайнер компонентов

Любой компонент (в том числе контролы и формы) может иметь свой дизайнер. Дизайнер – это класс, предназначенный для настройки компонента во время разработки.

Чтобы VS.NET могла определить, что для некоторого компонента нужно подключить дополнительный дизайнер, нужно установить этому компоненту атрибут System.ComponentModel.DesignerAttribute:

[Designer(typeof(MyComponentDesigner))]
public class MyComponent : System.ComponentModel.Component { ... };

Дизайнер компонентов должен как минимум реализовать интерфейс System.ComponentModel.Design.IDesigner:

void Initialize(IComponent component);

Вызывается VS.NET при подключении дизайнера к компоненту. В параметре component указывается компонент, к которому подключается дизайнер.

void DoDefaultAction();

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

IComponent Component { get; }

Через это свойство дизайнер должен возвратить ссылку на компонент, полученный в методе Initialize.

DesignerVerbCollection Verbs { get; }

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

Действие представляется классом System.ComponentModel.Design.DesignerVerb, позволяющим задать свойства меню (такие, как Text, Visible, Checked), а также обработчик события выбора пункта описываемого действием.

Интерфейс IDesigner можно реализовать самостоятельно, но еще проще воспользоваться готовой реализацией – классом ComponentDesigner. Кроме вышеуказанного интерфейса он реализует еще интерфейс System.ComponentModel.Design.IDesignerFilter. Этот интерфейс позволяет виртуально (на время разработки) изменять состав атрибутов, событий и, что самое важное, свойств объекта. Виртуальные и измененные свойства/события/атрибуты будут видны в редакторе свойств, и их можно будет настраивать так, как будто они настоящие. Вот список методов этого интерфейса:

void PostFilterAttributes (System.Collections.IDictionary attributes);
void PostFilterEvents (System.Collections.IDictionary events);
void PostFilterProperties (System.Collections.IDictionary properties);
void PreFilterAttributes (System.Collections.IDictionary attributes);
void PreFilterEvents (System.Collections.IDictionary events);
void PreFilterProperties (System.Collections.IDictionary properties);

Pre-методы позволяют добавить свои (виртуальные) свойства/события/атрибуты к уже имеющимся.

Post-методы позволяют удалить или изменить описание свойств/событий/атрибутов компонента.

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

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

public class MyComponentDesigner : ComponentDesigner
{
  public override DesignerVerbCollection Verbs 
  {
    get
    {
      // Заполняем и возвращаем массив действий для контекстного меню.
      return new DesignerVerbCollection(
        new DesignerVerb[] 
        {
          // Создаем действие для отдельного пункта меню.
          new DesignerVerb(
            "Мой пункт контекстного меню компонента!", 
            new EventHandler(OnVerbClicked))
        }
      );
    }
  }

  private void OnVerbClicked(object sender, EventArgs e) 
  {
    
    MessageBox.Show(
      "Здесь можно было открыть диалог дизайнера для компонента "
      // В любом месте дизайнера нам доступен настраиваемый компонент
      // и его сайт.
      + Component.Site.Name);
  }

  protected override void PreFilterProperties(
    System.Collections.IDictionary properties)
  {
    base.PreFilterProperties(properties);
    const string sPropName = "Тестовое свойство";
    // Добавляем виртуальное свойство, доступное только во время разработки.
    properties[sPropName] = new MyPropertyDescriptor(this, 
      TypeDescriptor.CreateProperty(
        Component.GetType(),
        sPropName,
        typeof(bool),
        new Attribute[] 
        {
          // Задаем категорию, в которой будет отображаться новое свойство.
          CategoryAttribute.Design,
          // Говорим VS.NET, что при изменении этого свойства нужно 
          // перечитать значение остальных свойств.
          RefreshPropertiesAttribute.All
        }));
  }
}

public class MyPropertyDescriptor : PropertyDescriptor
{
  MyComponentDesigner _CompDes;
  public MyPropertyDescriptor(MyComponentDesigner CompDes, 
    PropertyDescriptor PropDesc) : base(PropDesc)
  {
    _CompDes = CompDes;
  }
  public override bool ShouldSerializeValue(object component){ return false; }
  public override bool DesignTimeOnly {get { return true; } }
  public override Type PropertyType{ get{ return typeof(bool); } }
  public override Type ComponentType{ get { return typeof(MyComponent); } }
  public override bool IsReadOnly { get{ return false; } }

  public override bool CanResetValue(object component)
  {
    // Подразумевается, что компонент, к которому подключается данный 
    // дизайнер (в данном случае MyComponent), имеет свойство «Test».

    // По умолчанию виртуальное свойства «Тестовое свойство»
    // будет иметь значение false. Причем значение этого виртуального
    // свойства зависит от значения реального свойства «Test».
    // Если реальное свойство «Test»
    // равно true, значит и виртуальное свойство можно сбросить в
    // исходное состояние.

    // Метод CanResetValue вызывается у дизайнера, когда пользователь
    // открывает контекстное меню PropertyGrid, которое содержит 
    // пункт «Reset». Если CanResetValue возвратит true, пункт меню
    // будет доступен.
    return ((MyComponent)component).Test;
  }

  public override void ResetValue(object component) 
  {
    // Этот метод вызывается, когда пользователь выбирает 
    // пункт «Reset» контекстного меню PropertyGrid.
    ((MyComponent)component).Test = false;
  }

  public override object GetValue(object component) 
  {
    // Этот метод вызывается, когда читается значение свойства.
    // (в данном случае добавленного нами свойства «Тестовое свойство»)
    return ((MyComponent)component).Test;
  }

  public override void SetValue(object component, object value) 
  {
    // Этот метод вызывается, когда изменяется значение свойства.
    ((MyComponent)component).Test = (bool)value;
  }
};

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

Подключить дизайнер к компоненту можно следующим образом:

[Designer(typeof(MyComponentDesigner))]
public class MyComponent : System.ComponentModel.Component

MarshalByValueComponent

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

Редактор типов (UITypeEditor)

Когда вы выбираете свойство в PropertyGrid, в правой его части появляется текстовое поле, позволяющее ввести значение, или кнопка, позволяющая открыть выпадающее окно или диалог. Это выпадающее окно или диалог позволяют визуально настроить значение свойства. Для простых типов вроде строки или целого используется встроенный редактор (который и выглядит как текстовое поле). Для сложных же можно создать свой редактор. Такой редактор должен быть унаследован от UITypeEditor и ассоциирован с некоторым свойством или типом через атрибут Editor. Естественно, что если редактор ассоциирован с типом, этот редактор будет появляться у всех свойств этого типа.

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

public class WavFileEditor : System.Windows.Forms.Design.FileNameEditor
{
  protected override void InitializeDialog(System.Windows.Forms.OpenFileDialog ofd)
  {
    ofd.Filter = "Wav file (*.wav)|*.wav";
  }
}

в котором можно произвести тонкую настройку диалога выбора файла.

FolderNameEditor позволяет выбирать путь к некоторому каталогу.

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

Собственный дизайнер создать довольно просто. Нужно всего лишь создать наследника класса System.Drawing.Design.UITypeEditor и переопределить методы:

public object EditValue(ITypeDescriptorContext context,
  IServiceProvider provider, object value)

public override UITypeEditorEditStyle GetEditStyle(
  ITypeDescriptorContext context)

VS.NET поддерживает два вида редакторов: модальный и выпадающий. То, какого типа должен быть ваш дизайнер, задается через возвращаемое значение метода GetEditStyle. UITypeEditorEditStyle.DropDown – означает, что ваш редактор будет выглядеть как выпадающее окно, а UITypeEditorEditStyle.Modal – модальным диалогом.

В функции EditValue вы должны создать и открыть диалог или контрол (для DropDown-режима). Параметр context этого метода позволяет получить доступ к компоненту и контейнеру, а параметр provider к runtime-сервисам. Самым важным сервисом, предоставляемым через этот параметр, является IWindowsFormsEditorService. С помощью его метода DropDownControl можно показать контрол в виде выпадающего окна. В конце работы этот метод должен вернуть измененное значение (или то же самое, если пользователь решил отказаться от изменений).

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

public class TextEditor : System.Drawing.Design.UITypeEditor
{
  public override object EditValue(ITypeDescriptorContext context,
    IServiceProvider provider, object value)
  {
    IWindowsFormsEditorService wfes;
    wfes = (IWindowsFormsEditorService)provider.GetService(
      typeof(IWindowsFormsEditorService));

    // Создаем обычное текстовое окно...
    TextBox tbText = new TextBox();
    // настраиваем его свойства и задаем редактируемый текст.
    tbText.Text = (string)value;
    tbText.Size = new System.Drawing.Size(200, 140);
    tbText.Multiline = true;
    tbText.ScrollBars = ScrollBars.Both;
    tbText.BorderStyle = BorderStyle.None;
    // Показываем контрол в виде выпадающего окна. Управление вернется к нам
    // только по окончании редактирования (после того, как пользователь
    // нажмет Enter или Esc (новую строку можно ввести с помощью Ctrl+ Enter).
    wfes.DropDownControl(tbText);
    return tbText.Text;
  }

  public override UITypeEditorEditStyle GetEditStyle(
    ITypeDescriptorContext context)
  {
    // Говорим VS.NET, что хотим отображать немодальныое выпадающее окно.
    // При этом VS.NET отобразит кноку со стрелочкой.
    // Если возвратить в этом методе UITypeEditorEditStyle.Modal, VS.NET
    // отобразит кнопку с троеточием.
    return UITypeEditorEditStyle.DropDown;
  }
};

// Используем новый редактор...
string _MultilineText;

[Editor(typeof(TextEditor),typeof(System.Drawing.Design.UITypeEditor))]
public string MultilineText
{
  get{ return _MultilineText; }
  set{ _MultilineText = value; }
}

Модальный редактор мало чем отличается. Единственное, что нужно сделать – это заменить вызов метода IWindowsFormsEditorService.DropDownControl на вызов IWindowsFormsEditorService.ShowDialog, ну и, естественно, вместо контрола использовать собственный диалог.

Заключение

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


Эта статья опубликована в журнале RSDN Magazine #3. Информацию о журнале можно найти здесь