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

.Net Explorer

Автор: Андрей Мартынов
The RSDN Group
Опубликовано: 24.12.2002
Исправлено: 13.03.2005
Версия текста: 2.0.2
Идея и принципы
Схема работы
Назначение
Подробности
Добавление типа
Конструирование объектов и массивов
Вызов методов
Получение и установка свойств
Исследование полей
Предопределённые значения.
Подключение к событиям и инициирование оных
Работа с атрибутами
Пример
Работа с COMпонентами
Получение интерфейсов
Приведение к базовым классам и интерфейсам.
Скрипты
Генерирование сборок
Структура классов
Классы-элементы
Элемент-конструктор.
Элемент-объект.
Элемент-событие.
Страницы свойств элементов
Пользовательские управляющие элементы
Наследование форм
Классы – узлы дерева
Отражение вместо шаблонов
Скрипты - это просто
Предыстория
А что дальше?

Дистрибутив (~200 кб)
Исходные тексты (~230 кб)

Идея и принципы

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

Побудительным мотивом разработки (соблазном), было то, что, несмотря на довольно жёсткие ограничения, изложенные выше, реализация идеи оказывается совсем не сложным делом. Это связано с тем, что среда .Net предоставляет богатые возможности по рефлексии типов. Подробнее про механизмы рефлексии можно прочесть в статье "Метаданные в среде .Net" (RSDN#2), продолжением (иллюстрацией) которой можно считать эту программу.

Схема работы

Ниже приведён скриншот главного окна программы (рис.1). Как видите, окно делится на две главные части: слева дерево типов и объектов, а справа семейство закладок, отвечающих различным элементам классов.


Рис. 1. Главное окно программы. Создание экземпляра типа.

Основной (примерный) способ работы с программой таков:

  1. Вы выбираете тип(ы) данных, с которым(и) собираетесь работать. Операция "Type | Add...". Выбранные типы будут показываться в дереве типов (левая часть окна, папка "Types").
  2. Ветка «Тип» содержит в себе конструкторы, статические методы, статические свойства и статические поля.
  3. Используя конструктор, вы создаёте экземпляр типа – объект (операция "Type | Instantiate"). При этом можно выбрать необходимый конструктор и задать его аргументы. Созданные объекты помещаются в дерево объектов (папка Objects).
  4. Ветка объекта в дереве содержит методы, свойства, поля и события.
  5. Теперь можно вызывать методы объекта, просматривать и изменять значения свойств и полей, подключаться к событиям. Методам можно задавать аргументы, для индексируемых свойств и полей (массивов) можно задавать индексы.
  6. При вызове методов в качестве параметров могут задаваться как литералы (для простых типов), так и ссылки на уже существующие объекты из пула объектов. Ссылка параметра на объект устанавливается "перетаскиванием" объекта на параметр. В результате вызова метода возвращаемое значение помещается в пул объектов.

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

Здесь пора признаться, что программу я не совсем по праву называл средством программирования, скорее это средство вызова уже имеющегося кода. Средства конструирования типов пока развиты недостаточно (об этих средствах позже). Отсюда название программы – Explorer. Ведь это не редактор, не генератор кода, а всего лишь "проводник" ("исследователь", "открыватель") кода.

Назначение

На мой взгляд, программа может иметь несколько применений, например:

  1. Для экспресс-тестирования программных библиотек. Если вам надо на скорую руку убедиться в работоспособности принесённого вам кода, нет ничего проще, чем дёрнуть пару-тройку ключевых методов и сделать предварительные выводы (отправить на доработку).
  2. Для исследования и макетирования простейших алгоритмов. Работа программиста часто связана с исследованиями. По разным причинам (неполнота документации, неочевидность поведения программы в сложных условиях) часто бывает проще попробовать и проверить некоторые вещи экспериментально, чем заниматься теоретическими изысканиями. "Как отработает этот метод вот такую комбинацию входных параметров?" С помощью .Net Explorer это можно легко и быстро проверить.
СОВЕТ

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

  1. Для изучения и обучения основам работы с библиотекой .Net Framework Class Library (FCL) и другими библиотеками. Представьте себе, что вы проводите семинар, посвященный программированию в .Net, и при этом легко и непринуждённо показываете работу различных классов FCL, не используя никакого языка программирования. Думаю, руководство должно оценить простоту и ясность изложения материала.

Прошу заметить, что через .Net Explorer вы получаете доступ ко всей мощи программного интерфейса .Net, и при этом не требуется установка Visual Studio .Net! Да и зачем Visual Studio, ведь знания языков и программистской квалификации не требуется!

ПРИМЕЧАНИЕ

Справедливости ради надо сказать, что основы объектного подхода и азы программирования в среде .Net для успешной работы с этим средством всё-таки знать надо.

Подробности

Как вы уже знаете, в программе два основных окна (см. рис. 1): дерево объектов и страницы свойств элементов. Поэтому перед началом работы необходимо чётко понять взаимосвязь между содержимым этих двух окон.

На самом деле дерево объектов и страницы элементов живут независимой жизнью. Имеется в виду, что в дереве может быть выбран один элемент, а активная страничка при этом может относиться совсем к другому элементу. Страница свойств элемента, выбранного в дереве, открывается при вызове команды "Open" из контекстного меню, главного меню приложения или двойным щелчком мыши.

СОВЕТ

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

Обратное действие – поиск в дереве элемента, соответствующего активной странице – выполняется командой "Sync". Эта команда доступна по кнопке, которая расположена в правом верхнем углу каждой страницы.

Теперь рассмотрим по очереди все основные операции.

Добавление типа

Обычно работа с программой начинается с выбора типов. По команде меню Type | Add... вызывается диалог выбора типа (см. рисунок 2).


Рисунок 2. Выбор типа.

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

  1. Browse File... – произвольный выбор файла сборки.
  2. Browse Framework... – то же, что Browse File..., только со стартом в каталоге .Net Framework. Там расположено большое количество файлов-сборок, и, наверное, вы часто будете туда заходить.
  3. Browse GAC... – выбор сборки из Global Assembly Cach'а.
  4. mscorlib.dll – самая часто (почти всегда) используемая сборка.

Конструирование объектов и массивов

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

ПРИМЕЧАНИЕ

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

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


Рис. 3. Конструирование массива.

Намерение создать массив объектов выражается нажатием переключателя "Array". После этого можно задать нижнюю границу индекса и диапазон (длину) индекса по каждому измерению. Измерения добавляются и удаляются через контекстное меню в окне измерений (Команды "Add Dimension" и "Remove Dimension").

Вызов методов

Вызов методов – это самое простое мероприятие при работе с этой программой. Достаточно открыть страницу метода, заполнить параметры и нажать кнопку "Invoke" (см. рис. 4). Если метод возвращает значение, оно будет помещено в дерево объектов.


Рис. 4. Вызов метода.

Пару слов о параметрах. Параметры бывают простыми и ссылочными. Способы работы с этими типами существенно различаются.

Объекты простых типов отличаются тем, что их значения можно (нужно) задавать в виде строки. Это значит, что вы можете просто набрать с клавиатуры ту строку, которая будет автоматически преобразована в объект простого типа.

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

Чтобы вам было легче ориентироваться, где простой тип, а где ссылочный, поля ввода ссылочных типов выделены специальным цветом (COLOR_INFOTEXT).

Получение и установка свойств

Получить значение свойства объекта очень просто. Достаточно открыть страницу свойств свойства (такая вот тавтология получается 8-). В поле Value можно посмотреть значение свойства (см. рисунок 5.). Чтобы установить новое значение свойства, его значение можно набрать в поле Value и затем нажать кнопку "Set".

ПРИМЕЧАНИЕ

Если свойство доступно "только для чтения", кнопка "Set" будет закрыта.


Рис. 5. Работа со свойством.

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

ПРИМЕЧАНИЕ

Свойства могут иметь параметры (в конце концов, свойства – это всего лишь специальные методы!). Если такое случится, в нижней части страницы станет доступным список этих параметров (у свойств, как правило, параметр бывает только один), работа с которым ведётся точно так же, как при вызове методов и конструкторов.

Исследование полей

С полями всё обстоит точно так же, как и со свойствами. Получение значения, установка нового значения, клонирование... (см. рис. 6.).


Рис. 6. Работа с полем данных.

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

Страница свойств поля имеет два режима работы. Если поле – это массив, вам могут быть интересны отдельные элементы этого массива . В таком случае достаточно включить опцию "View As Array". При этом станут доступными поля ввода индексов массива. А в поле Value будет выводиться значение элемента массива в соответствии с заданными индексами. Действия по кнопкам Set и Clone теперь будут относиться уже не к массиву в целом, а к данному конкретному элементу массива.

Предопределённые значения.

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

СОВЕТ

Самой характерной разновидностью предопределённых значений являются значения типов-перечислений (типов-наследников класса Enum). Можно даже сказать, что типы-перечисления просто состоят из (и только из) своих предопределённых значений.

Имена всех предопределённых значений типа перечислены в меню, которое вызывается по кнопке справа от поля ввода значения свойства (или поля) (см. рис. 7.). Выбор пункта в этом меню приводит к автоматической установке величины поля или свойства.


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

Подключение к событиям и инициирование оных

Чтобы подключить обработчик к некоторому событию, откройте страницу свойств этого события и "бросьте" метод-обработчик из дерева в список обработчиков (этот список находится внизу страницы, см. рис. 8).


Рис. 8. Работа с событиями.

Инициирование события производится точно так же, как вызов методов: заполняются параметры, и нажимается кнопочка "Raise".

Работа с атрибутами

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


Рис. 9. Работа с атрибутами.

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

ПРИМЕЧАНИЕ

У элемента может быть несколько атрибутов одного и того же типа (multi-use attribute). В таком случае в дереве объектов экземпляры этого атрибута искусственно пронумерованы (для удобства работы). На самом деле эти экземпляры совершенно равноправны, и никакой нумерации атрибутов в природе не существует.

Пример

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

  1. Добавляем тип System.Windows.Forms.Form.
  2. Создаем экземпляр этого типа и получаем объект theForm.
  3. Вызываем метод theForm.Show. Наблюдаем появление на экране пустой формы.
  4. Вызываем метод theForm.CreateGraphics, получаем объект theGraphics.
  5. Открываем метод theGraphics.DrawEllipse и смотрим на параметры. Все параметры простые, за исключением параметра pen. Значит, нам надо сконструировать объект типа Pen, для этого...
  6. Щелкаем по pen правой кнопкой и в контекстном меню выбираем команду "Add Type". Тип Pen добавлен.
  7. Открываем конструктор Pen(Color color) и смотрим на параметры. Тип Color – не простой. Значит, нам надо сконструировать объект типа Color, для этого...
  8. Щелкаем по параметру color правой кнопкой и в контекстном меню выбираем команду "Add Type". Тип Color добавлен.
  9. Заполняем параметры вызова и вызываем статический метод Color.FromArgb. Получаем объект RedColor.
  10. Снова открываем конструктор Pen(Color color) и бросаем RedColor на параметр color. Создаем экземпляр. Получаем объект RedPen.
СОВЕТ

Пункты 7, 8, 9, 10 можно выполнить проще, наступив правой кнопкой на параметр color конструктора класса Pen, и выбрав в контекстном меню нужный цвет по имени.

  1. Снова открываем метод theGraphics.DrawEllipse и бросаем RedPen на параметр pen. Заполняем остальные параметры.
  2. Вызываем theGraphics.DrawEllipse.


Рис. 10. Рисуем на форме.

Теперь можно открыть свойство theGraphics.SmoothingMode, дать ему значение, например, AntiAlias, и повторить вызов theGraphics.DrawEllipse (результат показан на рисунке 10.). Можно вызвать theGraphics.DrawLine. Поскольку pen у нас уже готов, это очень просто. Можно изменить толщину и цвет пера... Можно… Можно ещё очень многое...

Работа с COMпонентами

Кстати. Чуть не забыл... 8-) Вы можете использовать эту программу для работы с COM-компонентами! При добавлении типа можно смело указывать dll, содержащую COM-компонент или библиотеку типов компонента.

В статье Алексея Дубовцева объяснено, каким образом можно использовать COMпоненты из .Net-приложений. Но, работая с .Net Explorer, вам можно не заботиться ни о чем таком. .Net Explorer берёт на себя рутинную работу, делая работу с COMпонентами столь же лёгкой, как и со сборками .Net.

Вот как выполняется с помощью .Net Explorer пример из вышеупомянутой статьи.


Рис. 11. Работа с COMпонентом Shell32.dll.

ПРИМЕЧАНИЕ

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

Получение интерфейсов

При работе с СОМ-объектом необходимо иметь возможность запрашивать его интерфейсы. В среде .Net эта операция выполняется с помощью приведения к типу интерфейса.

В .Net Explorer приведение выполняется так. Сначала надо добавить в пул типов интерфейс, к которому будет проводиться приведение. Затем надо открыть страницу объекта, и бросить из дерева интерфейс на имя типа объекта на странице его свойств.


Рис. 12. Приведение типа.

Если приведение возможно, результатом выполнения операции будет появление в дереве объектов новой ссылки нужного типа. Конечно, эта операция работает не только с СOM-объектами, но и с любыми .Net-типами.

* * *

Последнее свойство (работа с COMпонентами), надеюсь, будет привлекательно для COM-программистов. Они в своей работе постоянно используют незаменимую утилиту OleView. Эта утилита позволяет просматривать информацию о типах объектов. Но, к сожалению, эта утилита не позволяет создавать объекты и вызывать их методы.

Зато это позволяет .Net Explorer.

Приведение к базовым классам и интерфейсам.

Обратите внимание - для "родных" сборок есть ещё один способ приведения типов. Среди подэлементов объекта в дереве указаны все его базовые типы и все реализованные им интерфейсы. Контекстное меню для этих элементов позволяет произвести приведение типа объекта к базовому типу или к интерфейсу.


Скрипты

Первый опыт использования данной программы показал настоятельную необходимость автоматического повторения некоторых действий в программе, необходимость автоматической проверки некоторых условий, и т.п. Короче, нужен скриптовый язык. Несмотря на всё желание придерживаться заявленных принципов, приходится (как временную меру) вводить ещё одну страницу свойств под названием "Script" (см. рис 14.).


В качестве языка сценариев используется JScript. В сценарии можно использовать все типы, добавленные в дерево типов, и все созданные в дереве объекты. Все пространства имён, фигурирующие в используемых типах, автоматически включаются в сценарий (не нужно явно писать операторы import). При компиляции сценария на все сборки, содержащие используемые типы, автоматически проставлены ссылки (referenced assembly).Это позволяет сосредоточиться исключительно на содержательном кодировании.

СОВЕТ

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

В следующих версиях программы планируется добавить в дерево папку “Scripts”. В ней можно будет хранить несколько текстов скриптов.

Генерирование сборок

Т.к. скриптовая машина .Net позволяет не только исполнять скрипты, но и сохранять их в виде сборок, грех не воспользоваться этой возможностью. Итак, конструируем класс (см. рис. 15.).


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


Ну вот. Получен ещё один вариант «Hello World!», а потому теперь .Net Explorer можно с полным правом считать настоящим средством программирования. 8-)

* * *

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

ПРИМЕЧАНИЕ

Для тех, кто захочет сам реализовать свои идеи, сделать исправления и добавления, открыты исходники. Программа написана на C#. Имеется проект для её сборки в среде Visual Studio .Net.

1. Тем, кто пока делает только первые шаги в программировании на С#, советую прочитать статью Владислава Чистякова на бумажных страницах RSDN #1. Многие приёмы, описанные в этой статье, использовались в работе над .Net Explorer.

2. Книжки Арчера "Основы C#" и Гуннерсона "Введение в C#" дадут вам основополагающие знания по программированию на C#.

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

Структура классов

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

Быть может это очень старомодно, но в качестве архитектурного шаблона был избран, знакомый всем по MFC, стиль Document/View. После этого файл MainForm.cs был разрезан на части и разложен по классам-документам и классам-представлениям (view). Что из этого получилось вы видите на рисунке 17.


Рис. 17. Структура классов .Net Explorer.

Центральный класс, класс вокруг которого строится вся структура программы - Element. Element - это всё, что имеет свой отдельный значок в дереве - это объект, тип, конструктор, событие... Дерево, в которое собраны элементы, является документом. Для хранения и представления дерева элементов используется TreeView. Конечно, это означает, что архитектура Document/View не соблюдена не вполне, документ слился с одним из своих view (это обозначено пунктирной рамкой на диаграмме). Но зато...

.. зато на уровне элементов (микро-документов), всё строго. У элемента есть два представления - узел в дереве (ElementNode) и страница свойств (ElementPage). Представления знают всё об элементе, который они представляют, а вот элемент и не догадывается о своих представлениях, при этом представления не догадываются друг о друге. Всё это согласно концепции Document/View. Вот тут я подошёл к тому ради чего собственно затеял этот разговор: как выразить зависимости (или отсутствие оных) между частями программы на C#?

Для программиста на C/C++ тут нет вопросов. Чтобы гарантировать независимость модуля А от модуля В, достаточно не включать в модуль А h-файл модуля В. Любая попытка использовать модуль В из модуля А при этом будет пресечена компилятором. Но как быть в программе на C#. Ведь тут нет никаких h-файлов. Не разбивать же программу на отдельные сборки только ради этого?!


Рис. 2. Основные классы и пространства имён.

Можно поступить так - поместить части программы, которые не должны зависеть от друг друга, в отдельные пространства имён. После этого объявить using этих пространств только в тех модулях, которые должны использовать эти пространства. Модули, между которыми были необоснованные зависимости сразу перестанут компилироваться, это заставит навести порядок с зависимостями. Короче, получается довольно похоже на C/C++ - смотришь в начало файла и видишь те модули (пр-ва имён), от которых этот файл зависит.

ПРЕДУПРЕЖДЕНИЕ

А если использовать полные имена классов с указанием пространства имён, то этот способ не сработает! - скажите вы, и будете правы. На это я скажу, что и в C/C++ файле тоже можно написать "руками" прототип функции из "запрещённого" модуля и скрыть тем самым зависимость. Короче, да, предложенный способ работает только в условиях определённой дисциплины программирования.

Кстати, вот аргумент за то, что использовать полные имена типов без необходимости не стоит.

ПРИМЕЧАНИЕ

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

Классы-элементы

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


Рис. 2. Классы-элементы.

Элемент-конструктор.

Рассмотрим, например, элемент-конструктор:


public class CtorData : Element
{
  public CtorData(Document doc, TreeNode node, ConstructorInfo ci)
    : base(doc, node)
  {
    this.ci = ci;
    this.pars = ParamData.Init(this.ci.GetParameters());
  }
  override public string Name 
    { get { return this.ci.ReflectedType.Name; } }    

  override public Type ExposedType 
    { get { return this.ci.ReflectedType; } }   

  public object Instantiate()
  {
    object[] parsCtor = ParamData.CreateCallParams(this.doc
                                    , this.pars);
    return Activator.CreateInstance(this.ci.ReflectedType
                                    , parsCtor, null);
  }

  public readonly ConstructorInfo ci;
  public ParamData[] pars;
}

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

Задача страницы свойств сводится к предоставлению пользователю возможности задать параметры конструктора и к вызову метода элемента - Instantiate(). Давайте посмотрим как с этой задачей справляется CtorPage.

private void OnInstantiate(object sender, System.EventArgs e)
{
  CtorData ctor = this.element as CtorData;
  Debug.Assert(ctor != null);

  try
  {
    Write(ctor);
    object obj = ctor.Instantiate();
    ctor.Doc.InsertAndBeginRename(ctor.ci.ReflectedType, obj); // *
  }
  catch (Exception ex)
  {
    Util.Error("Can't instantiate type {0}\r\n{1}"
      , ctor.ci.ReflectedType.Name, ex.Message);
    return;    
  }
}

Первым делом данные со страницы пишутся в элемент (в данном случае это значения параметров конструктора), затем вызывается метод Instantiate() элемента-конструктора, который возвращает новый объект, который потом вставляется в дерево элементов. По этой схеме проходит взаимодействие всех элементов со своими представлениями. В работе элементов для вас не будет ничего особенно сложного. Особенно если вы прочитаете статью про метаданные на станицах RSDN#2. Я же только скажу пару слов о вещах, которые могут вызвать вопросы.

Элемент-объект.

Взглянув на описание элемента-объекта (ObjectData) у вас может возникнуть вопрос: зачем хранить отдельно тип объекта, ведь его всегда можно получить с помощью метода GetType()?

public class ObjectData : Element
{
  ...
  public  readonly Type type; // зачем?
  public  Object obj;
}

Во-первых не всегда. Объект может быть пустым (нулевой ссылкой). А у нулевой ссылки не спросишь – «какого ты типа?»

Во-вторых, даже когда объект не пустой, тип его может не совпадать с типом, возвращаемым GetType(). Как так? - удивитесь вы. Ничего удивительного. Например, если вы имеете элемент-ссылку на интерфейс, то под этой ссылкой всё равно лежит настоящий объект, реализующий этот интерфейс. Только наружу он выставляет всего одну свою грань. Другими словами, внутренний тип элемента-объекта - это тот его интерфейс (в широком смысле слова), который предоставляет этот элемент-объект. При этом, операция приведения типа порождает элемент-объект ссылающийся на тот же самый объект, но имеющий другой внутренний тип. Именно по этому, при вставлении объекта в дерево указывается какой тип приписать объекту, в качестве чего его вставлять (см *).

Элемент-событие.

Элемент-событие содержит в себе делегат и позволяет добавлять обработчики и инициировать событие. Как видно из приведённого ниже листинга, операции эти просты.

public class EventData : Element
{
  public EventData(Document doc, TreeNode node
                  , Object obj, EventInfo ei)
    : base(doc, node)
  {
    this.obj  = obj;
    this.ei   = ei;
    this.pars = ParamData.Init(
      this.ei.EventHandlerType.GetMethod("Invoke").GetParameters());
  }
  override public string Name 
    { get { return ei.Name; } }    

  override public Type ExposedType 
    { get { return ei.EventHandlerType; } }   

  public void Raise()
  {
    if (this.del != null)
    {
      object[] pars = ParamData.CreateCallParams(this.doc, this.pars); 
      this.del.DynamicInvoke(pars);
    }
  }

  public void AddHandlers(Element elem, string nameMethod)
  {
    FieldData od = elem.ParentElem as FieldData;
    Debug.Assert(od != null);

    Delegate delNew = Delegate.CreateDelegate(
        this.ei.EventHandlerType, od.obj, nameMethod);

    if (this.del == null)
      this.del = delNew;
    else
      this.del = Delegate.Combine(this.del, delNew);

    this.ei.AddEventHandler(this.obj, this.del);
  }

  public readonly Object    obj;
  public readonly EventInfo ei;

  public Delegate     del;
  public ParamData[]  pars;
}

Но есть только одна неочевидная деталь. Она связана с получением параметров события. Как получить список параметров (количество параметров, их типы и имена) имея в руках EventInfo? Ответом является строчка:

this.ei.EventHandlerType.GetMethod("Invoke").GetParameters();

В этом выражении сначала получается тип делегата (this.ei.EventHandlerType). Делегат - это какой же класс как и всякий другой в .Net. Этот класс имеет метод Invoke, параметры которого совпадают с параметрами делегата (или, что тоже самое, с параметрами методов-обработчиков события). Это и можно использовать для получения параметров события.

Страницы свойств элементов

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

Пользовательские управляющие элементы

Как я уже рассказывал, в первых версиях этой программы все контролы всех страниц свойств принадлежали главной форме приложения. Так получалось из-за того, что при конструировании формы в Visual Studio все элементы принадлежащие сложному контролу (контейнеру), становятся членами формы, а не членами контейнера. Например, страницы TabControl’a становятся членами не самого TabControl’a, а членами формы, которой принадлежит TabControl. В результате форма становится владельцем всех контролов принадлежащих ей и прямо и косвенно. Но это ещё полбеды. Настоящая беда в том, что форма при этом становится обработчиком всех событий, которые могут инициировать все эти конторолы. В результате форма становится монолитным куском мало управляемого, неструктурированного кода.

Для борьбы с этим злом можно поступить так. Добавляем в проект новый User Control. Методом copy&paste перетаскиваем содержимое страницы из формы в этот новый User Control. После этого в Toolbox’е, на закладке WinForms вы сможете увидеть имя только что созданного вами Control’а. Кидайте его обратно на форму (drag&drop). Форма вернётся в прежний вид, но внутренняя структура формы кардинально изменится. С точки зрения формы страница станет теперь одним неделимым контролом. Всё его поведение теперь должно быть определено им самим.


Рис. 3. Страница свойств конструктора как User Control.

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

Наследование форм

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

public AttributePage()
{
  // This call is required by the Windows.Forms Form Designer.
  InitializeComponent();

  this.DoubleClick += new System.EventHandler(base.OnSync); // было this.OnSync
  this.btMenu.Click += new System.EventHandler(base.OnMenu);
  this.miSync.Click += new System.EventHandler(base.OnSync);
  this.miAddType.Click += new System.EventHandler(base.OnAddExposedType);
  this.miExploreType.Click += new System.EventHandler(base.OnExploreType);
}

Тут главное не совершить ошибку и не вставить эти строчки рядом с другими подобными, сгенерированными студией внутри метода InitializeComponent. Если вы поступите так, то при первом редактировании ресурсов, ваши строчки пропадут. Студия считает себя полной хозяйкой внутри метода InitializeComponent. Не надо с ней спорить, просто вставьте свои строчки поле вызова InitializeComponent.

Классы – узлы дерева

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

public class AttrNode : ElementNode
{
  public AttrNode(Document doc, string name)
    : base(doc)
  {
    this.Text               = name;
    this.ImageIndex         = TreePane.idxImageLAttr;
    this.SelectedImageIndex = TreePane.idxImageLAttr;
  }

  override public ContextMenu MenuNode 
  { get { return AttrNode.menu; } }

  private static ContextMenu  menu = null; // такое поле есть у всех XXXNode
}

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

public class CtorNode
{  
  public static void Init(ContextMenu  menu)
  {
    CtorNode.menu = menu;
  }
}
… // и так каждый класс XXXNode
…
CtorNode.Init(cmCtor); // дерево передаёт каждому типу узлов его меню
AttrNode.Init(cmCtor);
…

В общем ничего особенного, но повторять код метода Init() больше десятка раз?! Это в то время, когда на C++ эта задача с лёгкостью решается с помощью шаблона?

template <class T>
static void Init(ContextMenu  menu)
{
  T::menu = menu;
}
…
Init<CtorNode>(cmCtor); // использование шаблона
Init<AttrNode>(cmAttr);
 …

Отражение вместо шаблонов

В C# подобная задача тоже имеет решение. На помощь опять приходит механизм Reflection.

public abstract class ElementNode 
{
  // Реализуем этот метод один раз в базовом классе
  public static void Init(Type type, ContextMenu  menu)
  {
    FieldInfo fi = type.GetField("menu", BindingFlags.Public 
                                       | BindingFlags.NonPublic
                                       | BindingFlags.Static);
    if (fi != null)
        fi.SetValue(null, menu);
  }
}
…
ElementNode.Init(typeof(AttrNode), cmAttr); // использование квазишаблона
ElementNode.Init(typeof(CtorNode), cmCtor);
…

Как вы видите, получившийся «квазишаблоный» метод с точки зрения вызывающего кода используется почти также как и С++ шаблон. Разница лишь в том, что теперь тип играет роль не параметра шаблона, а роль обычного параметра (аргумента) метода.

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

ПРИМЕЧАНИЕ

Справедливости ради надо сказать, что такой универсализм квазишаблонов требуется далеко не всегда. Часто типы, с которыми предстоит работать, точно известны в момент компиляции, поэтому старые добрые шаблоны в стиле С++ были бы очень уместны, и во многих случаях упростили бы программирование и существенно повысили бы эффективность C# кода. Будем надеяться, что такие шаблоны появятся в следующем поколении средств разработки для .Net.

Скрипты - это просто

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

Общеизвестен способ встраивания скриптов (сценариев) в пользовательские приложения на основе Windows Scipting Host. Любой объект вашей программы, поддерживающий интерфейс IDispatch, может управляться из скрипта, исполняющегося на этих скриптовых машинах (движках). Так было в мире COM. А как автоматизировать .Net приложения?

Для это предусмотрена технология Visual Studio for Application (пространство имён Microsoft.VSA). Это свои .Net’овские скриптовые движки (пока поддерживается только два языка - JScript и VBScript). Работают эти движки уже не через IDispatch, а через Reflection.

СОВЕТ

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

Встроить сценарии в своё приложение совсем не сложно.

Начать надо с реализации в своём приложении интерфейса IVsaSite, в котором наиболее важными являются два метода:

bool IVsaSite.OnCompilerError(IVsaError error)
{
  Util.Trace(error.Description); 
  // error содержит много другой полезной информации об ошибке.
  return true;
}

object IVsaSite.GetGlobalInstance(string name)    
{
  // Через этот вызов движок получает 
  // доступ к объектам вашего приложения       
  return this.doc.FindObject(name); // поиск в дереве в объекта по имени
}

Затем надо вызвать на исполнение скрипт, что делается примерно таким кодом:

void RunScript(string script)
{
  this.engine = null;
  try
  {
    this.engine = new VsaEngine();
    this.engine.RootMoniker = "script://root"; 
    // Если в вашем приложении исполняется несколько движков, 
    // RootMoniker’ы должны быть у них разные.
    this.engine.Site = this; 
    this.engine.InitNew();
    this.engine.RootNamespace = "Generated_By_Dne";

    AddReferences(this.engine); // Объявляем ссылки на другие сборки 
    AddRootObject(this.engine); // Объявляем о своих объектах

    IVsaCodeItem code = (IVsaCodeItem)this.engine.Items.CreateItem("Script"
      , VsaItemType.Code, VsaItemFlag.None);

    code.SourceText = RootNamespaces() // Добавляем import’ы пр-в имён.
                             + script; // Добавляем текст скрипта.
    if (! this.engine.Compile())
      return;

    this.engine.Run(); // Поехали !  
  }
  catch (Exception ex)
  {
    Util.Trace(ex.ToString());
  }
  finally
  {
    this.engine.Close(); 
    this.engine = null;
  }
}

В процедуре вызова скрипта обращу ваше внимание на три вещи.

Первое, что необходимо сделать – сообщить движку, какие внешние сборки будет использовать ваш скрипт (действие аналогичное простановке reference в проекте Visual Studio). Делается это созданием так называемого «item’а» в движке (метод CreateItem) . В .Net Explorer проставляет reference на все сборки, содержащие типы добавленные в дерево.


void AddReferences(VsaEngine engine)
{
  // для всех сборок, используемых элементами в дереве
  foreach(AssemblyName asmName in this.doc.AllAssemblyNames())
  {
    Assembly asm = Assembly.Load(asmName);
    IVsaReferenceItem refItem = (IVsaReferenceItem)engine.Items.CreateItem(
      asm.Location, VsaItemType.Reference, VsaItemFlag.None);
  }
}

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

.Net Explorer позволяет оперировать любыми объектами, помещёнными в дерево, поэтому все они добавляются как Item’ы.


void AddRootObject(VsaEngine engine)
{
  // для всех корневых объектов в дереве
  foreach(string name in this.doc.AllRootObjectNames())
  {
    IVsaGlobalItem refItem = (IVsaGlobalItem)engine.Items.CreateItem(
      name, VsaItemType.AppGlobal, VsaItemFlag.None);
    refItem.Name = name;
  }
}

И последнее, что стоит сделать для упрощения программирования скриптов – автоматически подключить все пространства имён, которые могут использоваться в сценарии. Это не обязательно но, думаю пользователям вашего приложения это немного упростит написание сценариев.

string RootNamespaces()
{
  StringBuilder s = new StringBuilder();
  foreach(string ns in this.doc.AllRootObjectNamespaces())
    s.Append("import " + ns + ";\r\n");

  foreach(string ns in this.doc.AllTypesNamespaces())
    s.Append("import " + ns + ";\r\n");
  return s.ToString();
}

В .Net Explorer’e вся эта «премудрость» спрятана в классе-элементе Script, работу которого представляет пользователю страница ScriptPage.

СОВЕТ

Более подробную информацию по программированию с использованием VSA, можно почерпнуть в статье в августовским номере журнала MSDN.

* * *

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

Предыстория

У вас может возникнуть вопрос: как могла возникнуть идея реализации такого необычного инструмента? Ответ простой. Дело в том, что мне довелось убедиться, что инструмент, аналогичный представленному, может приносить большую пользу. Похожий инструмент разработан и с успехом применяется у нас в Вымпелкоме для работы с одной довольно сложной библиотекой Java-классов. (Большой привет Леониду Хохлову и Алексею Петрову!) Реализация идеи на платформе .Net напрашивалась сама собой и была просто делом техники...

А что дальше?

Во-первых, это всего лишь бета...

Во-вторых, надо признать, что реализованных на данный момент в программе возможностей по вызову кода совершено недостаточно для более или менее полноценного программирования. Скрипты немного смягчают ситуацию, но не являются решением проблемы. Надо попробовать обеспечить ту же функциональность с помощью дружественного графического пользовательского интерфейса. Его, этот интерфейс, надо придумать, чтобы удержать .Net Explorer в рамках заявленных принципов ("никакого синтаксиса!"), чтобы разработка не потеряла смысл.

Вот ещё список самых неотложных дел.

  1. Remoting! Надо срочно добавить возможность работы по Remoting’у. Каждый тип в дереве должен иметь возможность быть опубликованным, для удалённого вызова. И наоборот, для каждого типа в дереве должна быть возможность создать объект этого типа в другом AppDomain’е, в т.ч. удалённо.
  2. Поиск типа. Пользователь часто не знает в какой сборке определён тип, с которым он собирается работать. А часто он и тип не знает. Надо дать возможность поиска типа. Идеи можно позаимствовать из примера TypeFinder.
  3. Подключение справочников (chm’ов). Т.к. .Net Explorer позиционируется в том числе и как средство обучения, то как можно обойтись в этом обучении без MSDN ?! Надо научиться вызывать нужные статьи по ключевым словам, а лучше бы это сделать более интеллектуально.
  4. Ассемблерные листинги. Надо попытаться научиться показывать ассемблерные листинги, относящиеся к различным элементам классов. Например IL-код метода. Я пробовал тут что-то сделать, но мне не удалось укротить Ildasm.exe с первого раза. Думаю попытку надо повторить. А может замахнуться на листинги на более высоких языках? Вот если бы Анакрино разобрать на кусочки…
  5. Выгрузка сборок. Все сборки сейчас грузятся в единый домен приложения и никогда не выгружаются. С этим надо кончать. Наверное надо все сборки грузить в отдельный домен. Чтобы иметь возможность его закрыть и выгрузить тем самым все сборки, загруженные в него.
  6. Надо уходить от проекта Visual Studio и переходить на make файлы. В студии работать тяжело. Тут надо разработать универсальный make-файл для работы с .Net проектами. У меня душа лежит к GNU-make.
  7. И тестировать, тестировать и ещё раз тестировать… (заклинание). 8-)

Вот такой небольшой план. Это только самые неотложные и довольно простые задачи (они будут решены в течение ближайших нескольких недель), но есть проблемы и посерьезнее. Я о них уже писал, повторю ещё раз - это разработка пользовательского интерфейса без синтаксиса. Как графически выразить оператор for или if? Как сделать это удобно, понятно даже ребёнку. Тут у меня ничего кроме трудновыразимых ощущений нет. Может у вас есть идеи?


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