Pull to refresh

Использование DSL в Visual Studio

Reading time 11 min
Views 6K

Введение


Раньше я ни когда не задумывался над разработкой инструментов, упрощающих разработку в Visual Studio, и чаше создавал различные сторонние утилиты для помощи себе в разработке. Но, как обычно бывает, настал переломный момент.
Однажды встала задача разработки платформы, на базе которой надо было бы разрабатывать специализированные решения.
Хотелось максимально упростить разработку решений на платформе, и при этом не урезать возможность гибкой настройки.
Возникло два основных направления решения проблемы:
  • Разработать собственные инструменты для создания решений
  • Создать вспомогательные средства, встроенные в среду разработки


Собственные инструменты

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

Вспомогательные средства для Visual Studio

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

Мой выбор

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

Предварительно


Перед тем как приступить, нам необходимо скачать и установить следующие дистрибутивы:

Постановка задачи


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

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


Поставив перед собой задачу, без лишних размышлений, создадим свой проект.
Как всегда, приступая к работе, запускаем нашу любимую Visual Studio 2010.
Заходим в окно создания проекта и выбираем тип проекта Domain-Specific Language Designer.


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



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



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

Чистим все

Все что у нас появилось, не особо нам пригодится, поэтому удалим все, что было добавлено автоматически.
Переходим в DSL Explorer и удаляем оттуда все, кроме стандартных типов в Domain Types.
В результате должно получиться следующее:



Опишем основные объекты Toolbox

Domain Class – объект данных.
Named Domain Class – именованный объект данных, который содержит свойство Name. Дальше система будет интерпретировать это свойство как название объекта, и автоматически будет генерировать его уникальное имя.
Geometry Shape – визуальное представление, которое используется, для визуализации на диаграмме объектов данных
Compartment Shape – тоже что Geometry Shape, только имеет другой вид
Embedding Relationship – связывает объекты связью принадлежности
Reference Relationship – связывает объекты типом ссылка на объект
Inheritance – родительский тип связи, то есть наследование одного объекта от другого
Connector – визуализатор связей
Diagram Element Map – используется для связи объектов и связей с их визуальным представлением

Создание основных объектов и их отображений

Перед тем как приступить к созданию непосредственных наших объектов, необходимо создать подготовительные элементы:
  • Designer – дизайнер для визуализации наших объектов. В DSL Explorer у нас создастся объект Editor. Необходимо в нем задать заново расширение для нашего проекта (Это свойство FileExtension), которое пускай будет «wcfw».
  • Diagram – основной элемент визуализации, который представляет наши объекты. Назовем ее «WCFDiagram».
  • Необходимо создать корневой элемент (Domain Class), на который надо будет настроить созданный ранее Editor (Это свойство Root Class) и Diagram (Это свойство Class Represented). Дадим название нашему корневому элементу «WCFCollection».
  • Explorer Behavior – нечто похожее на DSL Explorer, который будет показываться при работе с файлом нашего типа.

Это минимальный список того, что надо сделать, чтобы наш файл сохранился и проект можно было откомпилировать. Но перед компиляцией необходимо перегенерировать код по внесенным в файл изменениям. Для того чтобы это сделать, необходимо в Solution Explorer нажать кнопку «Transform All Templates».



Весь код генерируется по файлам *.tt (которые описывают T4-преобразование), которые лежат в папках GeneratedCode в проектах Dsl и DslPackage. После того, как были сгенерированы новые объекты, можно собрать наши проекты и даже запустить. Запускается все та же отладочная студия, с все тем же проектом. В проекте добавлены файла, которые относились к экспериментальному проекту, по этому, их все можно удалить. Добавляем свой тип файла (так же как и любой тип), находя его по имени WCFWizard.

Создание объекта описывающего «сервис»

Начнем с создания объекта «сервис». Для этого можно с Toolbox перетащить элемент Named Domain Class, и назовем его «WCFService». У нас автоматически у объекта появилось свойство Name, у которого стоит флаг Is Element Name, что говорит, что данное свойство выступает именем объект.
Настало время описать основные свойства объектов Domain Class и Named Domain Class
Название свойства Описание
Access Modifier Уровень доступа к сгенерированному классу.
Base Class Базовый класс. Мы можем здесь выбрать другой созданный объект, и тогда в сгенерированном коде текущий класс будет унаследован от класса, указанного базовым.
Custom Attributes Здесь происходит навешивание дополнительных атрибутов на объект.
Description Описание объекта.
Display Name Отображаемое имя объекта, которое будет видеть пользователь
Generates Double Derived Если стоит True, то происходит генерация двух классов (а обычно одного), один абстрактный, с реализацией всего необходимого, а второй генерируется partial класс, который пустой внутри. Это позволяет в дальнейшем вам переопределять виртуальные функции, которые объявлены в базовых классах.
Has Custom Constructor Если стоит True, то вам необходимо будет в вашем классе расширения определить необходимые конструкторы. Это удобно, когда вам при создании объекта необходимо выполнять дополнительные действия.
Inheritance Modifier Позволяет сделать ваш класс абстрактным, или чтобы от него нельзя было наследоваться.
Name Название объекта, с этим именем будет сгенерирован класс.
Namespace Пространство имен для вашего класса.

Пока оставим все поля по умолчанию. И опишем дополнительные свойства элемента Domain Property
Название свойства Описание
Getter Access Modifier Права доступа к свойству на чтение.
Setter Access Modifier Права доступа к свойству на запись.
Is UI Read Only Показывать ли свойство только на чтение.
Is Browsable Необходимо ли показывать свойство пользователю.
Default Value Значения поля по умолчанию.
Element Name Provider Внешний тип, который будет поставлять значения для свойства. У свойства должно стоять значение Is Element Name равное True. Позволяет вам генерировать уникальные имена по определенному алгоритму.
Is Element Name Является ли свойство именем объекта. Если стоит True, то начальное значение будет создаваться уникальным.
Kind Тип поля, которое влияет на его хранение:
  • None – хранение происходит в сгенерированном коде как обычная переменная
  • Calculated – говорит, что поле вычисляемое, и оно не сохраняется в переменной. Нам необходимо будет переопределить функции, которые отвечают за вычисление.
  • CustomStorage – пользовательское хранение. Нам необходимо будет переопределить функции отвечающие за получение и сохранение данных свойства

Type Тип свойства. Выпадающий список генерируется по списку объектов, определенных в Domain Types (можно найти в DSL Explorer). Вы можете самостоятельно добавлять свои объекты.

Теперь нам надо связать наш WCFService с WCFCollection родительской связью, которая будет говорить, что WCFService принадлежит WCFCollection. Для это протягиваем Embedding Relationship от WCFCollection к WCFService. В результате у нас получилось нечто похожее на картинке.



У нас создался объект Domain Relationship с именем «WCFCollectionHasWCFService». Этот объект отвечает за связь между WCFCollection к WCFService. Все свойства данного объекта похожи как у Domain Class, а дополнительные опишем отдельно
Название свойства Описание
Allows Duplicates Если возвести это свойство в True, то будет позволено создавать дополнительные Embedding Relationship на объект WCFService.
Base Relationship Базовая связь, свойства которой мы хотим унаследовать.
Is Embedding Показывает, что связь является Embedding, или нет.

Справа от Domain Relationship находиться «WCFService / 0..*» (имеет тип Domain Role), где WCFService – название свойства у объекта WCFCollection, которое и содержит ссылки на объекты WCFService. 0..* – тип связи, которая говорит, что в WCFCollection может быть количество WCFService от нуля до бесконечности.

Слева от Domain Relationship находиться «WCFCollection / 1..1» (имеет тип Domain Role), где WCFCollection – название свойства у объекта WCFService, которое и содержит ссылку на объект WCFCollection. 1..1 – тип связи, которая говорит, что в WCFService должна быть ровно одна (обязательная) ссылка на объект WCFCollection.
Domain Role описывает роли, которые существуют в связке, как мы можем видеть из нашего примера, каждый объект (WCFService и WCFCollection) имеет свою роль, которая описывает поведение связи с каждой стороны.

Опишем основные отличительные свойства объекта Domain Role
Название свойства Описание
Collection Type Позволяет задать тип ключа для получения объектов. В этом случае у нас сгенерируется не список, а коллекция.
Is Property Browsable Определяет видимость свойства для пользователя.
Is Property Generator Определяет необходимость генерации свойства.
Property Custom Attributes Позволяет навешать атрибуты на свойство.
Property Getter Access Права на доступ к свойству на чтение.
Property Setter Access Права на доступ к свойству на запись.
Multiplicity Показывает количество создаваемых объектов:
  • 0..* – от нуля до бесконечности
  • 1..1 – в единственном экземпляре
  • 0..1 – от нуля до одного
  • 1..* – минимум от одного и до бесконечности

Propagates Copy Объект, которому принадлежит эта роль в связке, при копировании связки тоже будет копироваться.
Propagates Delete Объект, которому принадлежит эта роль в связке, при удалении связки тоже будет удаляться.
Property Name Название генерируемого свойства.
Role Player Тип объекта, которому принадлежит роль.

Теперь сохранимся, нажмем кнопочку «Transform All Templates», соберем решение и запустимся. Сейчас мы можем добавлять через WCFWizard Explorer в WCFCollection объекты типа WCFService и задавать им имена.

Создание отображения на диаграмме

Чтобы наши объекты начали появляться на диаграмме, нам необходимо для них создать визуальное представление. Для этих целей служат элементы Toolbox: Geometry Shape, Compartment Shape, Connector. Первые два визуализируют объекты типа Domain Class, а последний используется для Domain Relationship.
Добавим элемент Compartment Shape и дадим ему имя WCFServiceShape. Этот элемент имеет ряд свойств отвечающих за его визуальное представление, но также возможно произвести более гибкое визуальное представление через расширение сгенерированного класса.
Для связывания объектов с их представлением, необходимо воспользоваться элементом Diagram Element Map и протянуть связь между WCFService и WCFServiceShape.



Давайте посмотрим что мы можем добавить в наш Compartment Shape:
  • Compartment – отображение коллекции свойств.
  • Domain Property – свойство, которое будет появляться в Properties.
  • Expand Collapse Decorator – элемент, позволяющий сворачивать и разворачивать внутреннее содержимое.
  • Icon Decorator – отображение изображения.
  • Text Decorator – вывод текста.

Добавим Text Decorator и дадим ему имя NameDecorator. Теперь надо его связать с конкретным свойством в WCFService, для этого используется окно DSL Details. При выделении нашего Diagram Element Map, появиться информация о связке. Перейдем на закладку Decorator Maps и активируем наш NameDecorator. Элемент Path to display property можно оставить пустым, поскольку мы будем использовать свойство непосредственно WCFService, но с помощью этого элемента можно задать путь к связанному объекту и отобразить затем его свойство. В элементе Display property выбираем наше имя.



Теперь наш WCFService научился себя отображать на диаграмме.

Помещение объекта на Toolbox

Можно добавлять объекты через WCFWizard Explorer, но это не очень симпатично выглядит. Попробуем добавить возможность создания объекта через Toolbox.
Перейдем в DSL Explorer и в Editor создадим новый Toolbox Tab, которому дадим название WCFWizard Elements. Это мы создали группу элементов для Toolbox, в которой мы можем создавать элементы двух типов: Connection Tool (для связей) и Element Tool (для объектов).
Создадим элемент типа Element Tool, и дадим ему имя WCFServiceTool, а иконку возьмем из тех, которые уже есть в ресурсах. Самое главное это задать свойство Class, которое и характеризует объект который должен создаваться при перетаскивании. Выбираем WCFService.
Теперь на нашем Toolbox появился первый объект, который можно смело перетаскивать на диаграмму.

Создание объекта описывающего «функцию»

Проделываем аналогичные действия, как было сделано выше для WCFService, только с именем WCFFunction. И связываем WCFFunction с WCFService (используя Embedding Relationship), а не с WCFCollection. И у нас получиться нечто следующее.



Теперь у нас к сервису можно добавлять функции, но на диаграмме не происходит их отображение. Добавим отображение к связке.
Перетащим объект типа Connector (можно в свойствах настроить его внешнее отображение) и свяжем его с объектом WCFServiceHasWCFFunction с помощью Diagram Element Map.
Добавим объект, который будет описывать тип объекта, для задания параметров функции и возвращаемого типа.
Добавим объект Domain Class с именем WCFType, и создадим в нем два текстовых свойства: Namespace и Type, для задания пространства имен типа и самого типа. Поставим свойство Inheritance Modifier в состояние abstract.
Также добавим объект Domain Class с именами WCFParamType (представляющее набор параметров функции), который будет унаследован от WCFType (задав параметр Base Class равным WCFType).
Для WCFParamType добавим еще свойство Name, чтобы идентифицировать название параметра в функции и свяжем его с WCFService как показано на рисунке.



Теперь в объекте WCFFunctionShape добавим Compartment (назовем его Params). Перейдем в панель DSL Details с выделенной связью между WCFFunction и WCFFunctionShape. Выберем вкладку Compartment Maps и настраиваем как на рисунке.



В поле Displayed elements collection path указываться путь к коллекции элементов, которые мы ходим отображать (используются связки), а дальше мы указываем название свойства для отображения.
Теперь у нас есть достаточно информации, чтобы можно было приступить к генерации простого кода.
Данная статья не включает в себя генерацию кода, но скажу, что можно пойти разными путями, я использовал два:
  • Генерировать с помощью T4.
  • Генерировать непосредственно из кода, при сохранении.

Сборка пакета и его развертывание


Настало время прояснить вопрос установки нашего расширения в Visual Studio. При сборке проекта DSLPackage у нас появляется файл с расширением vsix, который и является установочным пакетом.
Для развертывания нашего проекта, достаточно запустить файл vsix и пройти по шагам установщика.
В результате наше расширение появиться в Visual Studio и его можно посмотреть в Extension Manager (находиться в меню Tools). Здесь же можно будет и удалить его.



Заключение


Мне удалось осветить только малейшую часть данного инструмента, но надеюсь, что это подвигнет кого-то к самостоятельному углубленному изучению.
Если данная тема будет интересна народу, то имею в планах осветить еще такие вопросы как:
  • Использование пунктов меню.
  • Проверка корректности данных.
  • Генерация кода.
Tags:
Hubs:
+36
Comments 3
Comments Comments 3

Articles