Оценка 311 Оценить ![]() ![]() ![]() ![]() ![]() ![]()
|
Основная идея программы – продемонстрировать возможность программирования в среде .Net без использования алгоритмического языка, опираясь исключительно на графический интерфейс пользователя. Основное ограничение, основной принцип программы – не использовать никаких синтаксических конструкций. Пользователь не должен набирать никаких текстов, за исключением ввода значений литералов – инициализаторов простых типов. В остальных случаях обеспечивается пользовательский интерфейс, позволяющий создавать объекты, вызывать их методы, просматривать и изменять значения полей и свойств.
Побудительным мотивом разработки (соблазном), было то, что, несмотря на довольно жёсткие ограничения, изложенные выше, реализация идеи оказывается совсем не сложным делом. Это связано с тем, что среда .Net предоставляет богатые возможности по рефлексии типов. Подробнее про механизмы рефлексии можно прочесть в статье "Метаданные в среде .Net" (RSDN#2), продолжением (иллюстрацией) которой можно считать эту программу.
Ниже приведён скриншот главного окна программы (рис.1). Как видите, окно делится на две главные части: слева дерево типов и объектов, а справа семейство закладок, отвечающих различным элементам классов.

Рис. 1. Главное окно программы. Создание экземпляра типа.
Основной (примерный) способ работы с программой таков:
Ваша главная забота – это сконструировать объекты-параметры для тех вызовов методов, которые вам интересны.
|
Здесь пора признаться, что программу я не совсем по праву называл средством программирования, скорее это средство вызова уже имеющегося кода. Средства конструирования типов пока развиты недостаточно (об этих средствах позже). Отсюда название программы – Explorer. Ведь это не редактор, не генератор кода, а всего лишь "проводник" ("исследователь", "открыватель") кода. |
На мой взгляд, программа может иметь несколько применений, например:
| СОВЕТ Важно то, что с помощью .Net Explorer’а вы можете привлечь к этой работе людей, хорошо знающих предметную область, но не имеющих программистских навыков. |
Прошу заметить, что через .Net Explorer вы получаете доступ ко всей мощи программного интерфейса .Net, и при этом не требуется установка Visual Studio .Net! Да и зачем Visual Studio, ведь знания языков и программистской квалификации не требуется!
| ПРИМЕЧАНИЕ Справедливости ради надо сказать, что основы объектного подхода и азы программирования в среде .Net для успешной работы с этим средством всё-таки знать надо. |
Как вы уже знаете, в программе два основных окна (см. рис. 1): дерево объектов и страницы свойств элементов. Поэтому перед началом работы необходимо чётко понять взаимосвязь между содержимым этих двух окон.
На самом деле дерево объектов и страницы элементов живут независимой жизнью. Имеется в виду, что в дереве может быть выбран один элемент, а активная страничка при этом может относиться совсем к другому элементу. Страница свойств элемента, выбранного в дереве, открывается при вызове команды "Open" из контекстного меню, главного меню приложения или двойным щелчком мыши.
| СОВЕТ Такая «независимость» дерева объектов и страниц свойств, очень пригодится вам при операциях, требующих «перетаскивания» элементов классов из дерева на страницы свойств – при этих операциях выбранный в дереве (перетаскиваемый) элемент, как правило, не соответствует текущей странице свойств. |
Обратное действие – поиск в дереве элемента, соответствующего активной странице – выполняется командой "Sync". Эта команда доступна по кнопке, которая расположена в правом верхнем углу каждой страницы.
Теперь рассмотрим по очереди все основные операции.
Обычно работа с программой начинается с выбора типов. По команде меню Type | Add... вызывается диалог выбора типа (см. рисунок 2).

Рисунок 2. Выбор типа.
Типы располагаются в сборках, так что первым делом необходимо выбрать сборку. Нажатие на кнопку в правом верхнем углу позволяет выбрать сборку несколькими путями.
Чтобы создать экземпляр объекта, надо выбрать в дереве соответствующий конструктор. Если конструктор имеет параметры, их надо заполнить. После этого достаточно нажать на кнопку "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). В таком случае в дереве объектов экземпляры этого атрибута искусственно пронумерованы (для удобства работы). На самом деле эти экземпляры совершенно равноправны, и никакой нумерации атрибутов в природе не существует. |
Ниже приведена последовательность действий, которая приведёт вас к некоторому осязаемому результату и продемонстрирует основные приёмы работы с программой.
| СОВЕТ Пункты 7, 8, 9, 10 можно выполнить проще, наступив правой кнопкой на параметр color конструктора класса Pen, и выбрав в контекстном меню нужный цвет по имени. |

Рис. 10. Рисуем на форме.
Теперь можно открыть свойство theGraphics.SmoothingMode, дать ему значение, например, AntiAlias, и повторить вызов theGraphics.DrawEllipse (результат показан на рисунке 10.). Можно вызвать theGraphics.DrawLine. Поскольку pen у нас уже готов, это очень просто. Можно изменить толщину и цвет пера... Можно… Можно ещё очень многое...
Кстати. Чуть не забыл... 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 в рамках заявленных принципов ("никакого синтаксиса!"), чтобы разработка не потеряла смысл.
Вот ещё список самых неотложных дел.
Вот такой небольшой план. Это только самые неотложные и довольно простые задачи (они будут решены в течение ближайших нескольких недель), но есть проблемы и посерьезнее. Я о них уже писал, повторю ещё раз - это разработка пользовательского интерфейса без синтаксиса. Как графически выразить оператор for или if? Как сделать это удобно, понятно даже ребёнку. Тут у меня ничего кроме трудновыразимых ощущений нет. Может у вас есть идеи?
Оценка 311 Оценить ![]() ![]() ![]() ![]() ![]() ![]()
|