Всем привет
И вот, еще одна (и последняя) статья-пример по моему фреймворку для генерации TypeScript-ового glue-кода: Reinforced.Typings (перед этим была ещё одна и ещё). Сегодня мы научися автоматически генерировать TypeScript-обертки для вызовов методов MVC-контроллеров, снабжать их документацией и раскладывать по разным файлам. Надеюсь, вас порадует насколько быстро и легко решаются такие задачи с использованием RT. В рамках моего туториала мы реализуем генерацию класса-хэлпера для вызовов методов контроллеров с использованием jQuery и promise-ов, а так же сервис для angular.js, который готов для встраивания в ваше приложение и привнесения в него прозрачных обращений к методам сервера. Далее мы включим в генерируемый TypeScript документацию для этого дела (импортируем из XMLDOC) и разложим по файлам, чтобы не перемешивалось. Всем заинтересованным и желающим заиметь такую штуку в своих проектах, добро пожаловать под кат.
Если коротко, то RT использует систему кодогенераторов для генерации исходного TypeScript-кода. С недавних пор (версия 1.2.0) я отказался от практики писать TypeScript-код прямо в открытый текстовый поток (что было неудобно, зато весьма быстро) и пришел к практике использования очень несложного AST (Abstract Syntax Tree). В самом деле, это дерево очень базовое, далеко не полное и ориентировано оно на декларационные элементы TypeScript-а. Ну то есть без выражений, операторов, switch-ей, созданий переменных и т.п. Только классы/интерфейсы, методы, поля, модули. После введения AST, не скрою, стало легче. В том числе и форматировать выводимый код, в противовес старому решению, когда пользователю приходилось заботиться о форматировании самостоятельно. Да, конечно в определенном смысле это компромисс в ушерб гибкости, но в прибыль удобству использования, но в целом, как мне кажется, игра стоит свеч. К тому же в основных местах вас никто не ограничивает в типе узлов дерева. Что это значит? А это значит, что вставить в середину класса прямым текстом матерный комментарий (даже многострочный) через AST вы сможете. Да-да, и ему еще и корректно выставят табуляцию в начале строки. Ну что ж, приступим.
Давайте я объясню на простом примере — генерации jQuery-оберток с промисами для вызовов контроллеров. Потому что angular-way может оказаться чужд для кого-то, а с jQuery в общем-то все понятно.
Итак, дано: простое ASP.NET MVC-приложение, jQuery, контроллер с методами, которые хочется дергать из клиентского TypeScript-а, несколько View-моделек и ваши чешущиеся руки.
Задача: выставить серверный API в TypeScript-класс, чтобы не парится с постоянными $.post/$.load, или, чего доброго, $.ajax.
Решение:
Я обозначу здесь тестовый код, который мы имеем, чтобы было от чего отталкиваться.
Моделька:
Контроллер:
К тому же мы заведем TypeScript-файл (он у меня лежит в /Scripts/IndexPage.ts) для тестирования. Вот такой (да простят меня адепты MVVM, для вас будет материал ниже):
Важно! Не забудьте поставить себе тайпинги для jQuery из NuGet от DefinitelyTyped. В противном случае с компиляцией TS будут проблемы.
Ну и грех не сделать тестовую въюху (предполагается что jQuery у вас уже подключен). Перепишите ваш Index.cshtml, например, так:
Вы, должно быть, заметили включение query.js в документ. Не пугайтесь — так надо, в следующем же разделе я объясню что к чему.
Все, это была портянка тестовой инфраструктуры. Дальше будет легче.
Первым делом давайте вынесем в отдельный TypeScript-класс всего один метод, который мы будем использовать для запросов к серверу. Просто jQuery в этом плане несколько громоздок и мне бы не хотелось повторять весь этот шаблонный код в каждом методе в угоду читабельности. Итак, маленький класс с маленьким методом для того, чтобы делать запросы к серверу:
/Scripts/query.ts
Как видим, метод предельно прост, принимает на вход URL для запроса, данные (любой JS-объект), а так же, чтобы было удобнее использовать я сделал два параметра: один для обозначения селектора HTML-элемента, кой надлежит отключать на время запроса, и один… Ну, примерно то же самое, только для элемента, к которому будет подписываться надпись «Loading…». Полагаю самоочевидным, что надпись «Loading…» вы без труда можете заменить на что-нибудь свое. Возвращает этот метод jQuery-евский Promise, к которому вы можете дописать .then/.fail/.end и другие. Впрочем, я полагаю что целевая аудитория этой статьи и без меня в курсе как работать с промисами.:)
Так как через reflection невозможно определить что возвращают наши методы контроллера, мы сделаем атрибут, которым будем помечать все методы нашего JQueryController-а, для которых необходимо сгенерировать обертку. Выглядеть он будет, например, так:
Здесь мы убиваем двух зайцев сразу и указываем CodeGeneratorType для этого метода. Не волнуйтесь что его пока нет — мы напишем его в следующем разделе. В остальном — имеющийся атрибут можно разместить над нашими методами контроллера. Заодно мы поставим [TsClass], не отходя от кассы:
Так же не забудем поставить [TsInterface] над моделькой. Иначе, как говаривал Амаяк Акопян, «никакого чуда не произойдет“.
Теория сравнительно проста. RT собирает в кучу информацию о том, что из своих C#-типов вы хотите видеть в результирующем TypeScript-е и в каком виде, потом начинает это добро обходить в глубину, начиная с неймспейсов и спускаясь к членам класса и параметрам декларируемых методов. Встречая на своем пути ту или иную сущность, RT вызывает соответствующий кодогенератор (экземпляры кодогенераторов он инстанциирует лениво, с лайфтаймом «1 экземпляр на весь процесс экспорта»). На этот процесс можно повлиять, указав какой кодогенератор вы хотите использовать. Сделать это можно с помощью атрибутной конфигурации (поле CodeGeneratorType, которое есть в любом атрибуте RT) — тут нет «защиты от дурака», но предполагается что вы укажете typeof наследника интерфейса Reinforced.Typings.Generators.ITsCodeGenerator с правильным T, соответствующим сущности, над которой размещается атрибут. А еще можно использовать Fluent-вызов .WithCodeGenerator. Там уже есть «защита от дурака» и указать неподходящий кодогенератор у вас не получится.
Есть в наличии несколько встроенных кодогенераторов. Все они расположены в пространстве имен Reinforced.Typings.Generators. Имеются кодогенераторы для интерфейса, класса, конструктора, перечисления, поля, свойства, метода и параметра метода. Самый простой способ сделать свой кодогенератор — унаследоваться от существующего. Далее (по возрастанию сложности) — унаследоваться от TsCodeGeneratorBase<T1, T2>, где T1 — тип входной reflection-сущности, а T2 — тип результата в синтаксическом дереве (наследник RtNode). Ну и сложный вариант — реализовать интерейс ITsCodeGenerator самостоятельно, что в большинстве случаев делать не рекомендуется, ибо требует знания некоторых тонкостей и чтения исходнико, да и незачем.
Мы сделаем наш JQueryActionCallGenerator просто унаследовавшись от встроенного кодогенератора для методов. Итак, вот наш кодогенератор с детальными комментариями:
Кодогенератор готов. Осталось только сделать нашему проекту Rebuild. Если у вас возникают какие-либо сложности с отладкой генераторов, то вы можете использовать свойство генератора Context типа ExportContext. У того наличествует свойство Warnings, представляющее собой коллекцию RtWarning-ов. Добавьте туда свой — он выведется в окошко Warnings в студии при билде (эта фича была добавлена буквально недавно, в версии 1.2.2).
Для тех, кому не терпится, привожу результат работы кодогенратора:
Так же важный момент — если ваш кодогенератор отработал неправильно и выдал ошибку, которая сделала ваши TypeScript-ы несобираемыми и теперь у вас не собирается солюшен, чтобы сгенерировать новые тайпинги — пойдите в Reinforced.Typings.settings.xml, что в корне вашего проекта и установите там RtBypassTypeScriptCompilation в true. Это сместит MSBuild-задачу сборки TypeScript-ов так, чтобы она вызывалась после сборки проекта (а не до, как происходит по умолчанию). Однако, не забудьте вернуть потом обратно, ибо как будучи активным в ходе выполнения задач паблишинга, этот параметр может привести к тому, что тайпскрипты не будут пересобираться перед публикацией. И это не особо весело.
На этом моменте вы можете вернуться в ваш IndexPage.ts и использовать статические вызовы из сгенерированного нами класса JQueryController. В целом все достаточно прозаично и монотонно, поэтому я порекомендую вам самостоятельно этим заняться и испытать как работает IntelliSense. Код всего IndexPage.ts приведен ниже:
В принципе все то же самое. Однако, помимо генератора методов, нам понадобится генератор для всего класса, ибо как в лучших традициях ангуляра, неплохо будет экспортировать наш серверный контроллер в angular-сервис. Итак, поставьте DefinitelyTyped для angular.js, подключите сам angular через NuGet и давайте глянем на код генератора методов. Он немного отличается от jQuery-евского в силу того, что надо использовать другие промисы, а так же использовать $http.post вместо $.ajax:
Вот и вся магия. Не забудьте задать этот кодогенератор для всех angular-friendly методов контроллеров через атрибут или через Fluent-конфигурацию.
А теперь давайте сделаем кодогенератор для класса, который будет содержать эти методы. Специфика в том, что методы у нас отныне не статические, нам понадобится сделать приватное поле http, которое будет хранить сервис $http, конструктор, который будет инжектить этот сервис, а так же регистрацию нашего контроллер-сервиса в нашем приложении.
Здесь я предполагаю что у вас уже есть где-то создание модуля вашего приложения посредством вызова angular.module и сам модуль лежит в глобальной переменной app.
Присоедините этот кодогенератор к вашим контроллерам любым способом (атрибутом или Fluent-конфигурацией). После чего, пересоберите проект и смело инжектите полученный сервис в свои angular-контроллеры.
Опять же, результат работы этого кодогенератора для нетерпеливых:
Кстати, полная версия файла из примера вживую лежит вот тут.
Так же в RT присутствует возможность автоматически импортировать в полученный TypeScript-код XMLDOC из методов контроллеров. Помимо дописывания RtJsdocNode напрямую руками, возможен следующий подход:
1) Пройдите в Reinforced.Typings.settings.xml и установите там параметр RtGenerateDocumentation в True
2) Включите экспорт XMLDOC-а для вашего проекта. Чтобы сделать это, щелкните правой кнопкой мыши на вашем .csproj-е, выберите Properties, пройдите на вкладку Build и поставьте галочку «XML Documentation File» в секции Output (см. картинку). После чего сохраните проект через Ctrl-S
3) Пересоберите чтобы увидеть изменения Здесь важно заметить две особенности: во-первых по умолчанию, в Release-конфигурации эта галочка уже стоит. Поэтому как вариант вы можете установить параметр RtGenerateDocumentation и переключить конфиг сборки проекта на Release.
Вторая особенность — если волею судеб вы экспортируете типы с fluent-конфигурацией из другой сборки, то RT надо явно указать какой файл с документацией надо подключить (помимо основного). Сделать это можно с помощью Fluent-вызова ConfigurationBuilder.TryLookupDocumentationForAssembly, которому надо передать сборку из которой идет экспорт и полный путь к XML-файлу с документацией.
Для активации режима раскладки по разным файлам, вам понадобится включить еще одну настройку сборки. Итак
1) Пройдите в Reinforced.Typings.settings.xml, установите RtDivideTypesAmongFiles в true. Рядом же найдите параметр RtTargetDirectory — он указывает целевую папку, в которую свалятся сгенерированные typescript-файлы. Поправьте и этот параметр при необходимости
2) Определите какой код в какие файлы ляжет. Это можно сделать используя атрибут [TsFile], или же Fluent-вызов .ExportTo, доступный при использовании .ExportAsClass (es)/.ExportAsInterface (s)/.ExportAsEnum (s). Параметр принимает путь к файлу относительно директории, обозначенной в RtTargetDirectory.
3) Пересоберите, добавьте полученные файлы в проект
Казалось бы, ничего сложного, но тут всплывает сложный момент с директивой ///. Так вот — спешу обрадовать, никаких дополнительных настроек не требуется. RT достаточно умен, чтобы расставить эти директивы корректно без вашего участия. Однако, если вам необходимы референсы на какие-то другие файлы, то это легко решается путем атрибута [TsAddTypeReference] или Fluent-вызова .AddReference, который доступен примерно там же, где и .ExportTo. Эти методы позволяют добавить референс на файл по вашему экспортируемому CLR-типу, или же просто сырой строкой. Кстати, так же есть атрибут [assembly: TsReference], который добавит желаемый референс во все генерируемые файлы. То же самое делает fluent-вызов .AddReference, будучи примененным на ConfigurationBuilder-е.
Изложенные примеры доступны в репозитории Reinforced.Typings. Я открыт к предложениям, вопросам и пожеланиям в комментариях к этой статье, а так же в Issues к репозиторию RT. Еще у проекта есть замечательная github-wiki на английском, которую я развиваю по мере наличия свободного времени. Так же был (и есть) проект с русской документацией на RTFD, но он сейчас в суспенде.
Что касается статей и планов на будущее, то я вижу так что это последняя статья про RT, так как сам по себе RT является лишь компонентом моего другого интересного проекта — Reinforced.Lattice, который я выложу несколько позже, ибо как сейчас он проходит стадию обкатки и тестирования на живых людях. Мне почему-то кажется, что он вам понравится. Так что, следующая статья от меня будет уже про Lattice и, вероятнее всего, не скоро.
Спасибо что дочитали до конца. Это помогает мне не забрасывать проект:)
Reinforced.Typings на Github
Reinforced.Typings в NuGet
______________________
И вот, еще одна (и последняя) статья-пример по моему фреймворку для генерации TypeScript-ового glue-кода: Reinforced.Typings (перед этим была ещё одна и ещё). Сегодня мы научися автоматически генерировать TypeScript-обертки для вызовов методов MVC-контроллеров, снабжать их документацией и раскладывать по разным файлам. Надеюсь, вас порадует насколько быстро и легко решаются такие задачи с использованием RT. В рамках моего туториала мы реализуем генерацию класса-хэлпера для вызовов методов контроллеров с использованием jQuery и promise-ов, а так же сервис для angular.js, который готов для встраивания в ваше приложение и привнесения в него прозрачных обращений к методам сервера. Далее мы включим в генерируемый TypeScript документацию для этого дела (импортируем из XMLDOC) и разложим по файлам, чтобы не перемешивалось. Всем заинтересованным и желающим заиметь такую штуку в своих проектах, добро пожаловать под кат.
Пролог
Если коротко, то RT использует систему кодогенераторов для генерации исходного TypeScript-кода. С недавних пор (версия 1.2.0) я отказался от практики писать TypeScript-код прямо в открытый текстовый поток (что было неудобно, зато весьма быстро) и пришел к практике использования очень несложного AST (Abstract Syntax Tree). В самом деле, это дерево очень базовое, далеко не полное и ориентировано оно на декларационные элементы TypeScript-а. Ну то есть без выражений, операторов, switch-ей, созданий переменных и т.п. Только классы/интерфейсы, методы, поля, модули. После введения AST, не скрою, стало легче. В том числе и форматировать выводимый код, в противовес старому решению, когда пользователю приходилось заботиться о форматировании самостоятельно. Да, конечно в определенном смысле это компромисс в ушерб гибкости, но в прибыль удобству использования, но в целом, как мне кажется, игра стоит свеч. К тому же в основных местах вас никто не ограничивает в типе узлов дерева. Что это значит? А это значит, что вставить в середину класса прямым текстом матерный комментарий (даже многострочный) через AST вы сможете. Да-да, и ему еще и корректно выставят табуляцию в начале строки. Ну что ж, приступим.
С места в карьер
Давайте я объясню на простом примере — генерации jQuery-оберток с промисами для вызовов контроллеров. Потому что angular-way может оказаться чужд для кого-то, а с jQuery в общем-то все понятно.
Итак, дано: простое ASP.NET MVC-приложение, jQuery, контроллер с методами, которые хочется дергать из клиентского TypeScript-а, несколько View-моделек и ваши чешущиеся руки.
Задача: выставить серверный API в TypeScript-класс, чтобы не парится с постоянными $.post/$.load, или, чего доброго, $.ajax.
Решение:
Шаг 0. Инфраструктура
Я обозначу здесь тестовый код, который мы имеем, чтобы было от чего отталкиваться.
Моделька:
-
- public class SampleResponseModel
- {
- public string Message { get; set; }
- public bool Success { get; set; }
- public string CurrentTime { get; set; }
- }
-
Контроллер:
-
- public class JQueryController : Controller
- {
- public ActionResult SimpleIntMethod()
- {
- return Json(new Random().Next(100));
- }
-
- public ActionResult MethodWithParameters(int num, string s, bool boolValue)
- {
- return Json(string.Format("{0}-{1}-{2}", num, s, boolValue));
- }
-
- public ActionResult ReturningObject()
- {
- var result = new SampleResponseModel()
- {
- CurrentTime = DateTime.Now.ToLongTimeString(),
- Message = "Hello!",
- Success = true
- };
- return Json(result);
- }
-
- public ActionResult ReturningObjectWithParameters(string echo)
- {
- var result = new SampleResponseModel()
- {
- CurrentTime = DateTime.Now.ToLongTimeString(),
- Message = echo,
- Success = true
- };
- return Json(result);
- }
-
- public ActionResult VoidMethodWithParameters(SampleResponseModel model)
- {
- return null;
- }
- }
-
К тому же мы заведем TypeScript-файл (он у меня лежит в /Scripts/IndexPage.ts) для тестирования. Вот такой (да простят меня адепты MVVM, для вас будет материал ниже):
-
- module Reinforced.Typings.Samples.Difficult.CodeGenerators.Pages{
- export class IndexPage {
- constructor() {
- $('#btnSimpleInt').click(this.btnSimpleIntClick.bind(this));
- $('#btnMethodWithParameters').click(this.btnMethodWithParametersClick.bind(this));
- $('#btnReturningObject').click(this.btnReturningObjectClick.bind(this));
- }
-
- private btnSimpleIntClick() { }
-
- private btnMethodWithParametersClick() { }
-
- private btnReturningObjectClick() { }
- }
- }
-
Важно! Не забудьте поставить себе тайпинги для jQuery из NuGet от DefinitelyTyped. В противном случае с компиляцией TS будут проблемы.
Ну и грех не сделать тестовую въюху (предполагается что jQuery у вас уже подключен). Перепишите ваш Index.cshtml, например, так:
-
- <span id="loading"></span>
- Результат: <strong id="result"></strong>
- <button class="btn btn-primary" id="btnSimpleInt">Тынц</button>
- <button class="btn btn-default" id="btnMethodWithParameters">Клац</button>
- <button class="btn btn-default" id="btnReturningObject">Бумс</button>
- <button class="btn btn-default" id="btnReturningObjectWithParameters">Бдыщ</button>
- <button class="btn btn-default" id="btnVoidMethodWithParameters">Кря!</button>
- @section Scripts
- {
- <script type="text/javascript" src="/Scripts/IndexPage.js"></script>
- <script type="text/javascript" src="/Scripts/query.js"></script>
- <script type="text/javascript">
- $(document).ready(function () {
- var a = new Reinforced.Typings.Samples.Difficult.CodeGenerators.Pages.IndexPage();
- })
- </script>
- }
-
Вы, должно быть, заметили включение query.js в документ. Не пугайтесь — так надо, в следующем же разделе я объясню что к чему.
Все, это была портянка тестовой инфраструктуры. Дальше будет легче.
Шаг 1. Запросы
Первым делом давайте вынесем в отдельный TypeScript-класс всего один метод, который мы будем использовать для запросов к серверу. Просто jQuery в этом плане несколько громоздок и мне бы не хотелось повторять весь этот шаблонный код в каждом методе в угоду читабельности. Итак, маленький класс с маленьким методом для того, чтобы делать запросы к серверу:
/Scripts/query.ts
-
- class QueryController {
- public static query<T>(url: string, data: any, progressSelector: string, disableSelector:string = ''): JQueryPromise<T> {
- var promise = jQuery.Deferred();
- var query = {
- data: JSON.stringify(data),
- type: 'post',
- dataType: 'json',
- contentType:'application/json',
- timeout: 9000000,
- traditional: false,
- complete: () => {
- if (progressSelector && progressSelector.length > 0) {
- $(progressSelector).find('span[data-role="progressContainer"]').remove();
- }
- if (disableSelector && disableSelector.length > 0) {
- $(disableSelector).prop('disabled', false);
- }
- },
- success: (response: T) => {
- promise.resolve(response);
- },
- error: (request, status, error) => {
- promise.reject({ Success: false, Message: error.toString(), Data: error });
- }
- };
-
- if (progressSelector && progressSelector.length > 0) {
- $(progressSelector).append('<span data-role="progressContainer"> Loading ... </span>');
- }
-
- if (disableSelector && disableSelector.length > 0) {
- $(disableSelector).prop('disabled',true);
- }
-
- $.ajax(url,<any>query);
- return promise;
- }
- }
-
Как видим, метод предельно прост, принимает на вход URL для запроса, данные (любой JS-объект), а так же, чтобы было удобнее использовать я сделал два параметра: один для обозначения селектора HTML-элемента, кой надлежит отключать на время запроса, и один… Ну, примерно то же самое, только для элемента, к которому будет подписываться надпись «Loading…». Полагаю самоочевидным, что надпись «Loading…» вы без труда можете заменить на что-нибудь свое. Возвращает этот метод jQuery-евский Promise, к которому вы можете дописать .then/.fail/.end и другие. Впрочем, я полагаю что целевая аудитория этой статьи и без меня в курсе как работать с промисами.:)
Шаг 2. Атрибуты
Так как через reflection невозможно определить что возвращают наши методы контроллера, мы сделаем атрибут, которым будем помечать все методы нашего JQueryController-а, для которых необходимо сгенерировать обертку. Выглядеть он будет, например, так:
-
- public class JQueryMethodAttribute : TsFunctionAttribute
- {
- public JQueryMethodAttribute(Type returnType)
- {
- StrongType = returnType;
- CodeGeneratorType = typeof (JQueryActionCallGenerator);
- }
- }
-
Здесь мы убиваем двух зайцев сразу и указываем CodeGeneratorType для этого метода. Не волнуйтесь что его пока нет — мы напишем его в следующем разделе. В остальном — имеющийся атрибут можно разместить над нашими методами контроллера. Заодно мы поставим [TsClass], не отходя от кассы:
-
- [TsClass]
- public class JQueryController : Controller
- {
- [JQueryMethod(typeof(int))]
- public ActionResult SimpleIntMethod()
- {
- // fold
- }
-
- [JQueryMethod(typeof(string))]
- public ActionResult MethodWithParameters(int num, string s, bool boolValue)
- {
- // fold
- }
-
- [JQueryMethod(typeof(SampleResponseModel))]
- public ActionResult ReturningObject()
- {
- // fold
- }
-
- [JQueryMethod(typeof(SampleResponseModel))]
- public ActionResult ReturningObjectWithParameters(string echo)
- {
- // fold
- }
-
- [JQueryMethod(typeof(void))]
- public ActionResult VoidMethodWithParameters(SampleResponseModel model)
- {
- // fold
- }
- }
-
Так же не забудем поставить [TsInterface] над моделькой. Иначе, как говаривал Амаяк Акопян, «никакого чуда не произойдет“.
-
- [TsInterface]
- public class SampleResponseModel
- {
- // fold
- }
-
Шаг 3. Кодогенератор
Теория сравнительно проста. RT собирает в кучу информацию о том, что из своих C#-типов вы хотите видеть в результирующем TypeScript-е и в каком виде, потом начинает это добро обходить в глубину, начиная с неймспейсов и спускаясь к членам класса и параметрам декларируемых методов. Встречая на своем пути ту или иную сущность, RT вызывает соответствующий кодогенератор (экземпляры кодогенераторов он инстанциирует лениво, с лайфтаймом «1 экземпляр на весь процесс экспорта»). На этот процесс можно повлиять, указав какой кодогенератор вы хотите использовать. Сделать это можно с помощью атрибутной конфигурации (поле CodeGeneratorType, которое есть в любом атрибуте RT) — тут нет «защиты от дурака», но предполагается что вы укажете typeof наследника интерфейса Reinforced.Typings.Generators.ITsCodeGenerator с правильным T, соответствующим сущности, над которой размещается атрибут. А еще можно использовать Fluent-вызов .WithCodeGenerator. Там уже есть «защита от дурака» и указать неподходящий кодогенератор у вас не получится.
Есть в наличии несколько встроенных кодогенераторов. Все они расположены в пространстве имен Reinforced.Typings.Generators. Имеются кодогенераторы для интерфейса, класса, конструктора, перечисления, поля, свойства, метода и параметра метода. Самый простой способ сделать свой кодогенератор — унаследоваться от существующего. Далее (по возрастанию сложности) — унаследоваться от TsCodeGeneratorBase<T1, T2>, где T1 — тип входной reflection-сущности, а T2 — тип результата в синтаксическом дереве (наследник RtNode). Ну и сложный вариант — реализовать интерейс ITsCodeGenerator самостоятельно, что в большинстве случаев делать не рекомендуется, ибо требует знания некоторых тонкостей и чтения исходнико, да и незачем.
Мы сделаем наш JQueryActionCallGenerator просто унаследовавшись от встроенного кодогенератора для методов. Итак, вот наш кодогенератор с детальными комментариями:
-
- using System;
- using System.Linq;
- using System.Reflection;
- using Reinforced.Typings.Ast;
- using Reinforced.Typings.Generators;
-
-
- /// <summary>
- /// Наш кодогенератор для оберток на вызовы метода контроллера.
- /// Он заточен на то, чтобы брать на вход MethodInfo и продуцировать на выходе
- /// экземпляр RtFunction, который представляет собой кусок синтаксического дерева
- /// с TypeScript-ом.
- /// Как уже было сказано выше, мы наследуемся от встроенного в RT MethodCodeGenerator,
- /// который в обычных условиях генерирует метод с сигнатурой и пустой реализацией
- /// </summary>
- public class JQueryActionCallGenerator : MethodCodeGenerator
- {
- /// <summary>
- /// Естественно, мы перегружаем метод GenerateNode. Собственно, это почти что единственный
- /// и основной метод в кодогенераторе.
- /// </summary>
- /// <param name="element">Метод, для которого будет сгенерирован код в виде MethodInfo</param>
- /// <param name="result">
- /// Результирующая AST-нода (RtFunction).
- /// Мы не создаем ноду "с нуля". И честно говоря, я забыл почему принял такое архитектурное решение :)
- /// Но мы все еще можем вернуть null, чтобы исключить ноду/метод из конечного TypeScript-файла
- /// </param>
- /// <param name="resolver">
- /// А это - экземпляр TypeResolver-а. Это специальный класс, который мы можем использовать
- /// для вывода типов в результирующем TypeScript-е, чтобы не делать ничего руками.
- /// </param>
- /// <returns>RtFunction (она же нода синтаксического дерева, она же AST-нода)</returns>
- public override RtFuncion GenerateNode(MethodInfo element, RtFuncion result, TypeResolver resolver)
- {
- // Для начала сгенерируем обертку метода "как обычно"
- // Далее мы будем её расширять и дополнять
- result = base.GenerateNode(element, result, resolver);
-
- // Если по каким-то причинам сгенерирована пустая нода - значит так надо
- // не будем вмешиваться в этот процесс
- if (result == null) return null;
-
- // Делаем наш метод статическим
- result.IsStatic = true;
-
- // А так же добавляем к методу пару лишних параметров для указания
- // элемента, который будет отключен, пока будет идти вызов серверного метода,
- // а так же элемента, в который надо будет добавлять индикатор загрузки
- result.Arguments.Add(
- new RtArgument()
- {
- Identifier = new RtIdentifier("loadingPlaceholderSelector"),
- Type = resolver.ResolveTypeName(typeof(string)),
- DefaultValue = "''"
- });
-
- result.Arguments.Add(
- new RtArgument()
- {
- Identifier = new RtIdentifier("disableElement"),
- Type = resolver.ResolveTypeName(typeof(string)),
- DefaultValue = "''"
- });
-
- // Сохраняем оригинальное возвращаемое значение метода
- // ибо дальше нам потребуется параметризовать им JQueryPromise
- var retType = result.ReturnType;
-
- // ... и если возвращаемый тип - void, мы просто оставим JQueryPromise<any>
- bool isVoid = (retType is RtSimpleTypeName) && (((RtSimpleTypeName) retType).TypeName == "void");
-
- // здесь я использую TypeResolver чтобы получить AST-ноду для имени типа "any"
- // просто чтобы продемонстрировать применение TypeResolver-а
- // (ну и еще потому что я достаточно ленив чтобы создавать any руками)
- if (isVoid) retType = resolver.ResolveTypeName(typeof (object));
-
- // Окей, переопределяем возвращаемое значение нашего метода, оборачивая его
- // в JQueryPromise
- // Мы используем класс RtSimpleType, передавая ему generic-аргументы,
- // чтобы не писать угловые скобки руками
- result.ReturnType = new RtSimpleTypeName("JQueryPromise", new[] { retType });
-
- // Теперь дело за параметрами. Достаем их через reflection.
- // Далее мы используем экстеншн .GetName() чтобы достать имя параметра
- // Этот экстеншн поставляется с Reinforced.Typings и возвращает имя параметра
- // метода со всеми потенциальными перегрузками через Fluent-конфигурацию или
- // атрибут [TsParameter]
- var p = element.GetParameters().Select(c => string.Format("'{0}': {0}", c.GetName()));
-
- // Сцепляем параметры для генерации кода тела метода
- var dataParameters = string.Join(", ", p);
-
- // Достаем путь к контроллеру
- // Здесь у нас простое решение, которое требует наличия MVC-маршрута на /{controller}/{action}
- // предполагается что он есть (хотя кого я обманываю, он есть в 90% приложений)
- string controller = element.DeclaringType.Name.Replace("Controller", String.Empty);
- string path = String.Format("/{0}/{1}", controller, element.Name);
-
- // Теперь лепим glue-код с вызовом QueryController, который мы определили в query.ts
- string code = String.Format(
- @"return QueryController.query<{2}>(
- '{0}',
- {{ {1} }},
- loadingPlaceholderSelector,
- disableElement
- );",
- path, dataParameters, retType);
-
- // Оборачиваем код в RtRaw и запихиваем в качестве тела в наш результат
- result.Body = new RtRaw(code);
-
- // Теперь давайте добавим немного JSDOC-а, чтобы результаты нашей работы были очевиднее
- result.Documentation = new RtJsdocNode(){Description = String.Format("Wrapper method for call {0} of {1}",element.Name,element.DeclaringType.Name)};
-
- // Собственно, на этом все. Возвращаем RtFunction
- // Согласно дефолтным настройкам конфига, это добро будет записано в project.ts
- return result;
- }
- }
-
Кодогенератор готов. Осталось только сделать нашему проекту Rebuild. Если у вас возникают какие-либо сложности с отладкой генераторов, то вы можете использовать свойство генератора Context типа ExportContext. У того наличествует свойство Warnings, представляющее собой коллекцию RtWarning-ов. Добавьте туда свой — он выведется в окошко Warnings в студии при билде (эта фича была добавлена буквально недавно, в версии 1.2.2).
Для тех, кому не терпится, привожу результат работы кодогенратора:
-
- module Reinforced.Typings.Samples.Difficult.CodeGenerators.Models {
- export interface ISampleResponseModel
- {
- Message: string;
- Success: boolean;
- CurrentTime: string;
- }
- }
- module Reinforced.Typings.Samples.Difficult.CodeGenerators.Controllers {
- export class JQueryController
- {
- /** Wrapper method for call SimpleIntMethod of JQueryController */
- public static SimpleIntMethod(loadingPlaceholderSelector: string = '', disableElement: string = '') : JQueryPromise<number>
- {
- return QueryController.query<number>(
- '/JQuery/SimpleIntMethod',
- { },
- loadingPlaceholderSelector,
- disableElement
- );
- }
- /** Wrapper method for call MethodWithParameters of JQueryController */
- public static MethodWithParameters(num: number, s: string, boolValue: boolean, loadingPlaceholderSelector: string = '', disableElement: string = '') : JQueryPromise<string>
- {
- return QueryController.query<string>(
- '/JQuery/MethodWithParameters',
- { 'num': num, 's': s, 'boolValue': boolValue },
- loadingPlaceholderSelector,
- disableElement
- );
- }
- /** Wrapper method for call ReturningObject of JQueryController */
- public static ReturningObject(loadingPlaceholderSelector: string = '', disableElement: string = '') : JQueryPromise<Reinforced.Typings.Samples.Difficult.CodeGenerators.Models.ISampleResponseModel>
- {
- return QueryController.query<Reinforced.Typings.Samples.Difficult.CodeGenerators.Models.ISampleResponseModel>(
- '/JQuery/ReturningObject',
- { },
- loadingPlaceholderSelector,
- disableElement
- );
- }
- /** Wrapper method for call ReturningObjectWithParameters of JQueryController */
- public static ReturningObjectWithParameters(echo: string, loadingPlaceholderSelector: string = '', disableElement: string = '') : JQueryPromise<Reinforced.Typings.Samples.Difficult.CodeGenerators.Models.ISampleResponseModel>
- {
- return QueryController.query<Reinforced.Typings.Samples.Difficult.CodeGenerators.Models.ISampleResponseModel>(
- '/JQuery/ReturningObjectWithParameters',
- { 'echo': echo },
- loadingPlaceholderSelector,
- disableElement
- );
- }
- /** Wrapper method for call VoidMethodWithParameters of JQueryController */
- public static VoidMethodWithParameters(model: Reinforced.Typings.Samples.Difficult.CodeGenerators.Models.ISampleResponseModel, loadingPlaceholderSelector: string = '', disableElement: string = '') : JQueryPromise<any>
- {
- return QueryController.query<any>(
- '/JQuery/VoidMethodWithParameters',
- { 'model': model },
- loadingPlaceholderSelector,
- disableElement
- );
- }
- }
- }
-
Так же важный момент — если ваш кодогенератор отработал неправильно и выдал ошибку, которая сделала ваши TypeScript-ы несобираемыми и теперь у вас не собирается солюшен, чтобы сгенерировать новые тайпинги — пойдите в Reinforced.Typings.settings.xml, что в корне вашего проекта и установите там RtBypassTypeScriptCompilation в true. Это сместит MSBuild-задачу сборки TypeScript-ов так, чтобы она вызывалась после сборки проекта (а не до, как происходит по умолчанию). Однако, не забудьте вернуть потом обратно, ибо как будучи активным в ходе выполнения задач паблишинга, этот параметр может привести к тому, что тайпскрипты не будут пересобираться перед публикацией. И это не особо весело.
Шаг 4. Использование
На этом моменте вы можете вернуться в ваш IndexPage.ts и использовать статические вызовы из сгенерированного нами класса JQueryController. В целом все достаточно прозаично и монотонно, поэтому я порекомендую вам самостоятельно этим заняться и испытать как работает IntelliSense. Код всего IndexPage.ts приведен ниже:
-
- module Reinforced.Typings.Samples.Difficult.CodeGenerators.Pages {
- import JQueryController = Reinforced.Typings.Samples.Difficult.CodeGenerators.Controllers.JQueryController;
-
- export class IndexPage {
- constructor() {
- $('#btnSimpleInt').click(this.btnSimpleIntClick.bind(this));
- $('#btnMethodWithParameters').click(this.btnMethodWithParametersClick.bind(this));
- $('#btnReturningObject').click(this.btnReturningObjectClick.bind(this));
- $('#btnReturningObjectWithParameters').click(this.btnReturningObjectWithParametersClick.bind(this));
- $('#btnVoidMethodWithParameters').click(this.btnVoidMethodWithParametersClick.bind(this));
- }
-
- private btnSimpleIntClick() {
- JQueryController.SimpleIntMethod('#loading', '#btnSimpleInt')
- .then(r => $('#result').html('Server tells us ' + r));
- }
-
- private btnMethodWithParametersClick() {
- JQueryController.MethodWithParameters(Math.random(), 'string' + Math.random(), Math.random() > 0.5, '#loading', '#btnMethodWithParameters')
- .then(r => {
- $('#result').html(r);
- });
- }
-
- private btnReturningObjectClick() {
- JQueryController.ReturningObject('#loading', '#btnReturningObject')
- .then(r => {
- var s = "<pre> { <br/>";
- for (var key in r) {
- s += ` ${key}: ${r[key]},\n`;
- }
- s += '} </pre>';
- $('#result').html(s);
- });
- }
-
- private btnReturningObjectWithParametersClick() {
- var str = 'Random number: ' + Math.random() * 100;
- JQueryController.ReturningObjectWithParameters(str, '#loading', '#btnReturningObjectWithParameters')
- .then(r => {
- var s = "<pre> { <br/>";
- for (var key in r) {
- s += ` ${key}: ${r[key]},\n`;
- }
- s += '} </pre>';
- $('#result').html(s);
- });
- }
-
- private btnVoidMethodWithParametersClick() {
- JQueryController.VoidMethodWithParameters({
- Message: 'Hello',
- Success: true,
- CurrentTime: null
- }, '#loading', '#btnVoidMethodWithParameters')
- .then(() => {
- $('#result').html('Void method executed but it does not return result');
- });
- }
- }
- }
-
Для любителей Angular
В принципе все то же самое. Однако, помимо генератора методов, нам понадобится генератор для всего класса, ибо как в лучших традициях ангуляра, неплохо будет экспортировать наш серверный контроллер в angular-сервис. Итак, поставьте DefinitelyTyped для angular.js, подключите сам angular через NuGet и давайте глянем на код генератора методов. Он немного отличается от jQuery-евского в силу того, что надо использовать другие промисы, а так же использовать $http.post вместо $.ajax:
-
- using System;
- using System.Linq;
- using System.Reflection;
- using Reinforced.Typings.Ast;
- using Reinforced.Typings.Generators;
-
- /// <summary>
- /// Генератор оберток методов для angular.js
- /// </summary>
- public class AngularActionCallGenerator : MethodCodeGenerator
- {
- public override RtFuncion GenerateNode(MethodInfo element, RtFuncion result, TypeResolver resolver)
- {
- result = base.GenerateNode(element, result, resolver);
- if (result == null) return null;
-
- // перегружаем тип метода под наш промис
- var retType = result.ReturnType;
- bool isVoid = (retType is RtSimpleTypeName) && (((RtSimpleTypeName)retType).TypeName == "void");
-
- // точно такой же трюк с void-методами
- if (isVoid) retType = resolver.ResolveTypeName(typeof(object));
-
- // используем angular.IPromise вместо JQueryPromise
- result.ReturnType = new RtSimpleTypeName(new[] { retType }, "angular", "IPromise");
-
- // параметры метода - по такому же принципу
- var p = element.GetParameters().Select(c => string.Format("'{0}': {0}", c.GetName()));
- var dataParameters = string.Join(", ", p);
-
- // Достаем путь к контроллеру
- string controller = element.DeclaringType.Name.Replace("Controller", String.Empty);
- string path = String.Format("/{0}/{1}", controller, element.Name);
-
- // Генерируем код через this.http.post
- const string code = @"var params = {{ {1} }};
- return this.http.post('{0}', params)
- .then((response) => {{ return response.data; }});";
-
- // так же оборачиваем в RtRaw
- RtRaw body = new RtRaw(String.Format(code, path, dataParameters));
- result.Body = body;
- return result;
- }
- }
-
Вот и вся магия. Не забудьте задать этот кодогенератор для всех angular-friendly методов контроллеров через атрибут или через Fluent-конфигурацию.
А теперь давайте сделаем кодогенератор для класса, который будет содержать эти методы. Специфика в том, что методы у нас отныне не статические, нам понадобится сделать приватное поле http, которое будет хранить сервис $http, конструктор, который будет инжектить этот сервис, а так же регистрацию нашего контроллер-сервиса в нашем приложении.
Здесь я предполагаю что у вас уже есть где-то создание модуля вашего приложения посредством вызова angular.module и сам модуль лежит в глобальной переменной app.
-
- using System;
- using Reinforced.Typings.Ast;
- using Reinforced.Typings.Generators;
-
- /// <summary>
- /// Генератор, оборачивающий наши методы в angular-сервис.
- /// наследуем от стандартного генератора классов
- /// </summary>
- public class AngularControllerGenerator : ClassCodeGenerator
- {
- public override RtClass GenerateNode(Type element, RtClass result, TypeResolver resolver)
- {
- // Опять же - начинаем со "стандартного" класса, сгенерированного для контроллера
- result = base.GenerateNode(element, result, resolver);
- if (result == null) return null;
-
- // Добавим немножко документации
- result.Documentation = new RtJsdocNode(){Description = "Result of AngularControllerGenerator activity"};
-
- // создаем имя типа angular.IHttpService
- var httpServiceType = new RtSimpleTypeName("IHttpService") { Namespace = "angular" };
-
- // Добавлем конструктор,...
- RtConstructor constructor = new RtConstructor();
- // ... принимающий $http: angular.IHttpService
- constructor.Arguments.Add(new RtArgument(){Type = httpServiceType,Identifier = new RtIdentifier("$http")});
- // его тело будет содержать единственную строчку -
- // занесение $http в локальное поле
- constructor.Body = new RtRaw("this.http = $http;");
-
- // Описываем поле, которое будет держать в себе $http
- RtField httpServiceField = new RtField()
- {
- Type = httpServiceType,
- Identifier = new RtIdentifier("http"),
- AccessModifier = AccessModifier.Private,
- Documentation = new RtJsdocNode() { Description = "Хранит экземпляр $http полученный при вызове конструктора"}
- };
-
- // Добавляем констркутор и поле в наш класс
- result.Members.Add(httpServiceField);
- result.Members.Add(constructor);
-
- // Код для строчки, которая будет регистрировать наш класс в модуле для последующей инъекции
- // регистрация будет идти под именем Api.%Something%Controller
- const string initializerFormat =
- "if (window['app']) window['app'].factory('Api.{0}', ['$http', ($http: angular.IHttpService) => new {1}($http)]);";
-
- // подставляем параметры
- RtRaw registration = new RtRaw(String.Format(initializerFormat,element.Name,result.Name));
-
- // Находим текущий модуль в контексте экспорта (свойство генератора Context), берем у него список юнитов компиляции
- // Для этого используем Context.Location.CurrentModule.CompilationUnits
- // Коллекция CompilationUnits собирает в себе RtNode, так что
- // в нее можно запихнуть все, вплоть до RtRaw
- // По сему, смело пихаем регистрацию прямо вот туда
- Context.Location.CurrentModule.CompilationUnits.Add(registration);
- return result;
- }
- }
-
Присоедините этот кодогенератор к вашим контроллерам любым способом (атрибутом или Fluent-конфигурацией). После чего, пересоберите проект и смело инжектите полученный сервис в свои angular-контроллеры.
Опять же, результат работы этого кодогенератора для нетерпеливых:
-
- module Reinforced.Typings.Samples.Difficult.CodeGenerators.Controllers {
- if (window['app']) window['app'].factory('Api.AngularController', ['$http', ($http: ng.IHttpService) => new AngularController($http)]);
- /** Result of AngularControllerGenerator activity */
- export class AngularController
- {
- constructor ($http: ng.IHttpService)
- {
- this.http = $http;
- }
- public SimpleIntMethod() : ng.IPromise<number>
- {
- var params = { };
- return this.http.post('/Angular/SimpleIntMethod', params)
- .then((response) => { response.data['requestParams'] = params; return response.data; });
- }
- public MethodWithParameters(num: number, s: string, boolValue: boolean) : ng.IPromise<string>
- {
- var params = { 'num': num, 's': s, 'boolValue': boolValue };
- return this.http.post('/Angular/MethodWithParameters', params)
- .then((response) => { response.data['requestParams'] = params; return response.data; });
- }
- public ReturningObject() : ng.IPromise<Reinforced.Typings.Samples.Difficult.CodeGenerators.Models.ISampleResponseModel>
- {
- var params = { };
- return this.http.post('/Angular/ReturningObject', params)
- .then((response) => { response.data['requestParams'] = params; return response.data; });
- }
- public ReturningObjectWithParameters(echo: string) : ng.IPromise<Reinforced.Typings.Samples.Difficult.CodeGenerators.Models.ISampleResponseModel>
- {
- var params = { 'echo': echo };
- return this.http.post('/Angular/ReturningObjectWithParameters', params)
- .then((response) => { response.data['requestParams'] = params; return response.data; });
- }
- public VoidMethodWithParameters(model: Reinforced.Typings.Samples.Difficult.CodeGenerators.Models.ISampleResponseModel) : ng.IPromise<any>
- {
- var params = { 'model': model };
- return this.http.post('/Angular/VoidMethodWithParameters', params)
- .then((response) => { response.data['requestParams'] = params; return response.data; });
- }
- /** Keeps $http instance received on construction */
- private http: ng.IHttpService;
- }
- }
-
Кстати, полная версия файла из примера вживую лежит вот тут.
Добавляем документацию
Так же в RT присутствует возможность автоматически импортировать в полученный TypeScript-код XMLDOC из методов контроллеров. Помимо дописывания RtJsdocNode напрямую руками, возможен следующий подход:
1) Пройдите в Reinforced.Typings.settings.xml и установите там параметр RtGenerateDocumentation в True
2) Включите экспорт XMLDOC-а для вашего проекта. Чтобы сделать это, щелкните правой кнопкой мыши на вашем .csproj-е, выберите Properties, пройдите на вкладку Build и поставьте галочку «XML Documentation File» в секции Output (см. картинку). После чего сохраните проект через Ctrl-S
3) Пересоберите чтобы увидеть изменения Здесь важно заметить две особенности: во-первых по умолчанию, в Release-конфигурации эта галочка уже стоит. Поэтому как вариант вы можете установить параметр RtGenerateDocumentation и переключить конфиг сборки проекта на Release.
Вторая особенность — если волею судеб вы экспортируете типы с fluent-конфигурацией из другой сборки, то RT надо явно указать какой файл с документацией надо подключить (помимо основного). Сделать это можно с помощью Fluent-вызова ConfigurationBuilder.TryLookupDocumentationForAssembly, которому надо передать сборку из которой идет экспорт и полный путь к XML-файлу с документацией.
Раскладываем по разным файлам
Для активации режима раскладки по разным файлам, вам понадобится включить еще одну настройку сборки. Итак
1) Пройдите в Reinforced.Typings.settings.xml, установите RtDivideTypesAmongFiles в true. Рядом же найдите параметр RtTargetDirectory — он указывает целевую папку, в которую свалятся сгенерированные typescript-файлы. Поправьте и этот параметр при необходимости
2) Определите какой код в какие файлы ляжет. Это можно сделать используя атрибут [TsFile], или же Fluent-вызов .ExportTo, доступный при использовании .ExportAsClass (es)/.ExportAsInterface (s)/.ExportAsEnum (s). Параметр принимает путь к файлу относительно директории, обозначенной в RtTargetDirectory.
3) Пересоберите, добавьте полученные файлы в проект
Казалось бы, ничего сложного, но тут всплывает сложный момент с директивой ///. Так вот — спешу обрадовать, никаких дополнительных настроек не требуется. RT достаточно умен, чтобы расставить эти директивы корректно без вашего участия. Однако, если вам необходимы референсы на какие-то другие файлы, то это легко решается путем атрибута [TsAddTypeReference] или Fluent-вызова .AddReference, который доступен примерно там же, где и .ExportTo. Эти методы позволяют добавить референс на файл по вашему экспортируемому CLR-типу, или же просто сырой строкой. Кстати, так же есть атрибут [assembly: TsReference], который добавит желаемый референс во все генерируемые файлы. То же самое делает fluent-вызов .AddReference, будучи примененным на ConfigurationBuilder-е.
Заключение
Изложенные примеры доступны в репозитории Reinforced.Typings. Я открыт к предложениям, вопросам и пожеланиям в комментариях к этой статье, а так же в Issues к репозиторию RT. Еще у проекта есть замечательная github-wiki на английском, которую я развиваю по мере наличия свободного времени. Так же был (и есть) проект с русской документацией на RTFD, но он сейчас в суспенде.
Что касается статей и планов на будущее, то я вижу так что это последняя статья про RT, так как сам по себе RT является лишь компонентом моего другого интересного проекта — Reinforced.Lattice, который я выложу несколько позже, ибо как сейчас он проходит стадию обкатки и тестирования на живых людях. Мне почему-то кажется, что он вам понравится. Так что, следующая статья от меня будет уже про Lattice и, вероятнее всего, не скоро.
Спасибо что дочитали до конца. Это помогает мне не забрасывать проект:)
Reinforced.Typings на Github
Reinforced.Typings в NuGet
______________________