Pull to refresh

Reinforced.Typings — Angular-сервисы на TypeScript прямо из ваших MVC-контроллеров

Reading time 20 min
Views 8K
Всем привет
И вот, еще одна (и последняя) статья-пример по моему фреймворку для генерации 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. Инфраструктура


Я обозначу здесь тестовый код, который мы имеем, чтобы было от чего отталкиваться.

Моделька:
  1.  
  2. public class SampleResponseModel
  3. {
  4.     public string Message { get; set; }
  5.     public bool Success { get; set; }
  6.     public string CurrentTime { get; set; }
  7. }
  8.  


Контроллер:

  1.  
  2. public class JQueryController : Controller
  3. {
  4.     public ActionResult SimpleIntMethod()
  5.     {
  6.         return Json(new Random().Next(100));
  7.     }
  8.  
  9.     public ActionResult MethodWithParameters(int num, string s, bool boolValue)
  10.     {
  11.         return Json(string.Format("{0}-{1}-{2}", num, s, boolValue));
  12.     }
  13.  
  14.     public ActionResult ReturningObject()
  15.     {
  16.         var result = new SampleResponseModel()
  17.         {
  18.             CurrentTime = DateTime.Now.ToLongTimeString(),
  19.             Message = "Hello!",
  20.             Success = true
  21.         };
  22.         return Json(result);
  23.     }
  24.  
  25.     public ActionResult ReturningObjectWithParameters(string echo)
  26.     {
  27.         var result = new SampleResponseModel()
  28.         {
  29.             CurrentTime = DateTime.Now.ToLongTimeString(),
  30.             Message = echo,
  31.             Success = true
  32.         };
  33.         return Json(result);
  34.     }
  35.  
  36.     public ActionResult VoidMethodWithParameters(SampleResponseModel model)
  37.     {
  38.         return null;
  39.     }
  40. }
  41.  


К тому же мы заведем TypeScript-файл (он у меня лежит в /Scripts/IndexPage.ts) для тестирования. Вот такой (да простят меня адепты MVVM, для вас будет материал ниже):

  1.  
  2. module Reinforced.Typings.Samples.Difficult.CodeGenerators.Pages{
  3.     export class IndexPage {
  4.         constructor() {
  5.             $('#btnSimpleInt').click(this.btnSimpleIntClick.bind(this));
  6.             $('#btnMethodWithParameters').click(this.btnMethodWithParametersClick.bind(this));
  7.             $('#btnReturningObject').click(this.btnReturningObjectClick.bind(this));
  8.         }
  9.  
  10.         private btnSimpleIntClick() {       }
  11.  
  12.         private btnMethodWithParametersClick() {        }
  13.  
  14.         private btnReturningObjectClick() {         }
  15.     }
  16. }
  17.  


Важно! Не забудьте поставить себе тайпинги для jQuery из NuGet от DefinitelyTyped. В противном случае с компиляцией TS будут проблемы.

Ну и грех не сделать тестовую въюху (предполагается что jQuery у вас уже подключен). Перепишите ваш Index.cshtml, например, так:

  1.  
  2. <span id="loading"></span>
  3. Результат: <strong id="result"></strong>
  4. <button class="btn btn-primary" id="btnSimpleInt">Тынц</button>
  5. <button class="btn btn-default" id="btnMethodWithParameters">Клац</button>
  6. <button class="btn btn-default" id="btnReturningObject">Бумс</button>
  7. <button class="btn btn-default" id="btnReturningObjectWithParameters">Бдыщ</button>
  8. <button class="btn btn-default" id="btnVoidMethodWithParameters">Кря!</button>
  9. @section Scripts
  10. {
  11.     <script type="text/javascript" src="/Scripts/IndexPage.js"></script>
  12.     <script type="text/javascript" src="/Scripts/query.js"></script>
  13.     <script type="text/javascript">
  14.         $(document).ready(function () {
  15.             var a = new Reinforced.Typings.Samples.Difficult.CodeGenerators.Pages.IndexPage();
  16.         })
  17.     </script>
  18. }
  19.  


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

Все, это была портянка тестовой инфраструктуры. Дальше будет легче.

Шаг 1. Запросы


Первым делом давайте вынесем в отдельный TypeScript-класс всего один метод, который мы будем использовать для запросов к серверу. Просто jQuery в этом плане несколько громоздок и мне бы не хотелось повторять весь этот шаблонный код в каждом методе в угоду читабельности. Итак, маленький класс с маленьким методом для того, чтобы делать запросы к серверу:

/Scripts/query.ts

  1.  
  2. class QueryController {
  3.     public static query<T>(url: string, data: any, progressSelector: string, disableSelector:string = ''): JQueryPromise<T> {
  4.         var promise = jQuery.Deferred();
  5.         var query = {
  6.             data: JSON.stringify(data),
  7.             type: 'post',
  8.             dataType: 'json',
  9.             contentType:'application/json',
  10.             timeout: 9000000,
  11.             traditional: false,
  12.             complete: () => {
  13.                 if (progressSelector && progressSelector.length > 0) {
  14.                     $(progressSelector).find('span[data-role="progressContainer"]').remove();
  15.                 }
  16.                 if (disableSelector && disableSelector.length > 0) {
  17.                     $(disableSelector).prop('disabled', false);
  18.                 }
  19.             },
  20.             success: (response: T) => {
  21.                 promise.resolve(response);
  22.             },
  23.             error: (request, status, error) => {
  24.                 promise.reject({ Success: false, Message: error.toString(), Data: error });
  25.             }
  26.         };
  27.  
  28.         if (progressSelector && progressSelector.length > 0) {
  29.             $(progressSelector).append('<span data-role="progressContainer"> Loading ... </span>');
  30.         }
  31.  
  32.         if (disableSelector && disableSelector.length > 0) {
  33.             $(disableSelector).prop('disabled',true);
  34.         }
  35.  
  36.         $.ajax(url,<any>query);
  37.         return promise;
  38.     }
  39. }
  40.  


Как видим, метод предельно прост, принимает на вход URL для запроса, данные (любой JS-объект), а так же, чтобы было удобнее использовать я сделал два параметра: один для обозначения селектора HTML-элемента, кой надлежит отключать на время запроса, и один… Ну, примерно то же самое, только для элемента, к которому будет подписываться надпись «Loading…». Полагаю самоочевидным, что надпись «Loading…» вы без труда можете заменить на что-нибудь свое. Возвращает этот метод jQuery-евский Promise, к которому вы можете дописать .then/.fail/.end и другие. Впрочем, я полагаю что целевая аудитория этой статьи и без меня в курсе как работать с промисами.:)

Шаг 2. Атрибуты


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

  1.  
  2. public class JQueryMethodAttribute : TsFunctionAttribute
  3. {
  4.     public JQueryMethodAttribute(Type returnType)
  5.     {
  6.         StrongType = returnType;
  7.         CodeGeneratorType = typeof (JQueryActionCallGenerator);
  8.     }
  9. }
  10.  


Здесь мы убиваем двух зайцев сразу и указываем CodeGeneratorType для этого метода. Не волнуйтесь что его пока нет — мы напишем его в следующем разделе. В остальном — имеющийся атрибут можно разместить над нашими методами контроллера. Заодно мы поставим [TsClass], не отходя от кассы:

  1.  
  2. [TsClass]
  3. public class JQueryController : Controller
  4. {
  5.     [JQueryMethod(typeof(int))]
  6.     public ActionResult SimpleIntMethod()
  7.     {
  8.         // fold
  9.     }
  10.  
  11.     [JQueryMethod(typeof(string))]
  12.     public ActionResult MethodWithParameters(int num, string s, bool boolValue)
  13.     {
  14.         // fold
  15.     }
  16.  
  17.     [JQueryMethod(typeof(SampleResponseModel))]
  18.     public ActionResult ReturningObject()
  19.     {
  20.         // fold
  21.     }
  22.    
  23.     [JQueryMethod(typeof(SampleResponseModel))]
  24.     public ActionResult ReturningObjectWithParameters(string echo)
  25.     {
  26.         // fold
  27.     }
  28.  
  29.     [JQueryMethod(typeof(void))]
  30.     public ActionResult VoidMethodWithParameters(SampleResponseModel model)
  31.     {
  32.         // fold
  33.     }
  34. }
  35.  


Так же не забудем поставить [TsInterface] над моделькой. Иначе, как говаривал Амаяк Акопян, «никакого чуда не произойдет“.

  1.  
  2. [TsInterface]
  3. public class SampleResponseModel
  4. {
  5.     // fold
  6. }
  7.  


Шаг 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 просто унаследовавшись от встроенного кодогенератора для методов. Итак, вот наш кодогенератор с детальными комментариями:

  1.  
  2. using System;
  3. using System.Linq;
  4. using System.Reflection;
  5. using Reinforced.Typings.Ast;
  6. using Reinforced.Typings.Generators;
  7.  
  8.  
  9. /// <summary>
  10. /// Наш кодогенератор для оберток на вызовы метода контроллера.
  11. /// Он заточен на то, чтобы брать на вход MethodInfo и продуцировать на выходе
  12. /// экземпляр RtFunction, который представляет собой кусок синтаксического дерева
  13. /// с TypeScript-ом.
  14. /// Как уже было сказано выше, мы наследуемся от встроенного в RT MethodCodeGenerator,
  15. /// который в обычных условиях генерирует метод с сигнатурой и пустой реализацией
  16. /// </summary>
  17. public class JQueryActionCallGenerator : MethodCodeGenerator
  18. {
  19.     /// <summary>
  20.     /// Естественно, мы перегружаем метод GenerateNode. Собственно, это почти что единственный
  21.     /// и основной метод в кодогенераторе.
  22.     /// </summary>
  23.     /// <param name="element">Метод, для которого будет сгенерирован код в виде MethodInfo</param>
  24.     /// <param name="result">
  25.     /// Результирующая AST-нода (RtFunction).
  26.     /// Мы не создаем ноду "с нуля". И честно говоря, я забыл почему принял такое архитектурное решение :)
  27.     /// Но мы все еще можем вернуть null, чтобы исключить ноду/метод из конечного TypeScript-файла
  28.     /// </param>
  29.     /// <param name="resolver">
  30.     /// А это - экземпляр TypeResolver-а. Это специальный класс, который мы можем использовать
  31.     /// для вывода типов в результирующем TypeScript-е, чтобы не делать ничего руками.    
  32.     /// </param>
  33.     /// <returns>RtFunction (она же нода синтаксического дерева, она же AST-нода)</returns>
  34.     public override RtFuncion GenerateNode(MethodInfo element, RtFuncion result, TypeResolver resolver)
  35.     {
  36.         // Для начала сгенерируем обертку метода "как обычно"
  37.         // Далее мы будем её расширять и дополнять
  38.         result =  base.GenerateNode(element, result, resolver);
  39.        
  40.         // Если по каким-то причинам сгенерирована пустая нода - значит так надо
  41.         // не будем вмешиваться в этот процесс
  42.         if (result == null) return null;
  43.  
  44.         // Делаем наш метод статическим
  45.         result.IsStatic = true;
  46.  
  47.         // А так же добавляем к методу пару лишних параметров для указания
  48.         // элемента, который будет отключен, пока будет идти вызов серверного метода,
  49.         // а так же элемента, в который надо будет добавлять индикатор загрузки
  50.         result.Arguments.Add(
  51.             new RtArgument()
  52.             {
  53.                 Identifier = new RtIdentifier("loadingPlaceholderSelector"),
  54.                 Type = resolver.ResolveTypeName(typeof(string)),
  55.                 DefaultValue = "''"
  56.             });
  57.  
  58.         result.Arguments.Add(
  59.            new RtArgument()
  60.            {
  61.                Identifier = new RtIdentifier("disableElement"),
  62.                Type = resolver.ResolveTypeName(typeof(string)),
  63.                DefaultValue = "''"
  64.            });
  65.  
  66.         // Сохраняем оригинальное возвращаемое значение метода
  67.         // ибо дальше нам потребуется параметризовать им JQueryPromise
  68.         var retType = result.ReturnType;
  69.        
  70.         // ... и если возвращаемый тип - void, мы просто оставим JQueryPromise<any>
  71.         bool isVoid = (retType is RtSimpleTypeName) && (((RtSimpleTypeName) retType).TypeName == "void");
  72.  
  73.         // здесь я использую TypeResolver чтобы получить AST-ноду для имени типа "any"
  74.         // просто чтобы продемонстрировать применение TypeResolver-а
  75.         // (ну и еще потому что я достаточно ленив чтобы создавать any руками)
  76.         if (isVoid) retType = resolver.ResolveTypeName(typeof (object));
  77.  
  78.         // Окей, переопределяем возвращаемое значение нашего метода, оборачивая его
  79.         // в JQueryPromise
  80.         // Мы используем класс RtSimpleType, передавая ему generic-аргументы,
  81.         // чтобы не писать угловые скобки руками
  82.         result.ReturnType = new RtSimpleTypeName("JQueryPromise", new[] { retType });
  83.  
  84.         // Теперь дело за параметрами. Достаем их через reflection.
  85.         // Далее мы используем экстеншн .GetName() чтобы достать имя параметра
  86.         // Этот экстеншн поставляется с Reinforced.Typings и возвращает имя параметра
  87.         // метода со всеми потенциальными перегрузками через Fluent-конфигурацию или
  88.         // атрибут [TsParameter]
  89.         var p = element.GetParameters().Select(c => string.Format("'{0}': {0}", c.GetName()));
  90.  
  91.         // Сцепляем параметры для генерации кода тела метода
  92.         var dataParameters = string.Join(", ", p);
  93.  
  94.         // Достаем путь к контроллеру
  95.         // Здесь у нас простое решение, которое требует наличия MVC-маршрута на /{controller}/{action}
  96.         // предполагается что он есть (хотя кого я обманываю, он есть в 90% приложений)
  97.         string controller = element.DeclaringType.Name.Replace("Controller", String.Empty);
  98.         string path = String.Format("/{0}/{1}", controller, element.Name);
  99.  
  100.         // Теперь лепим glue-код с вызовом QueryController, который мы определили в query.ts
  101.         string code = String.Format(
  102. @"return QueryController.query<{2}>(
  103.    '{0}',
  104.    {{ {1} }},
  105.    loadingPlaceholderSelector,
  106.    disableElement
  107. );",
  108.    path, dataParameters, retType);
  109.  
  110.         // Оборачиваем код в RtRaw и запихиваем в качестве тела в наш результат
  111.         result.Body = new RtRaw(code);
  112.  
  113.         // Теперь давайте добавим немного JSDOC-а, чтобы результаты нашей работы были очевиднее
  114.         result.Documentation = new RtJsdocNode(){Description = String.Format("Wrapper method for call {0} of {1}",element.Name,element.DeclaringType.Name)};
  115.  
  116.         // Собственно, на этом все. Возвращаем RtFunction
  117.         // Согласно дефолтным настройкам конфига, это добро будет записано в project.ts
  118.         return result;
  119.     }
  120. }
  121.  


Кодогенератор готов. Осталось только сделать нашему проекту Rebuild. Если у вас возникают какие-либо сложности с отладкой генераторов, то вы можете использовать свойство генератора Context типа ExportContext. У того наличествует свойство Warnings, представляющее собой коллекцию RtWarning-ов. Добавьте туда свой — он выведется в окошко Warnings в студии при билде (эта фича была добавлена буквально недавно, в версии 1.2.2).

Для тех, кому не терпится, привожу результат работы кодогенратора:
  1.  
  2. module Reinforced.Typings.Samples.Difficult.CodeGenerators.Models {
  3.         export interface ISampleResponseModel
  4.         {
  5.                 Message: string;
  6.                 Success: boolean;
  7.                 CurrentTime: string;
  8.         }
  9. }
  10. module Reinforced.Typings.Samples.Difficult.CodeGenerators.Controllers {
  11.         export class JQueryController
  12.         {
  13.                 /** Wrapper method for call SimpleIntMethod of JQueryController */
  14.                 public static SimpleIntMethod(loadingPlaceholderSelector: string = '', disableElement: string = '') : JQueryPromise<number>
  15.                 {
  16.                         return QueryController.query<number>(
  17.                                 '/JQuery/SimpleIntMethod',
  18.                                 {  },
  19.                                 loadingPlaceholderSelector,
  20.                                 disableElement
  21.                             );
  22.                 }
  23.                 /** Wrapper method for call MethodWithParameters of JQueryController */
  24.                 public static MethodWithParameters(num: number, s: string, boolValue: boolean, loadingPlaceholderSelector: string = '', disableElement: string = '') : JQueryPromise<string>
  25.                 {
  26.                         return QueryController.query<string>(
  27.                                 '/JQuery/MethodWithParameters',
  28.                                 { 'num': num, 's': s, 'boolValue': boolValue },
  29.                                 loadingPlaceholderSelector,
  30.                                 disableElement
  31.                             );
  32.                 }
  33.                 /** Wrapper method for call ReturningObject of JQueryController */
  34.                 public static ReturningObject(loadingPlaceholderSelector: string = '', disableElement: string = '') : JQueryPromise<Reinforced.Typings.Samples.Difficult.CodeGenerators.Models.ISampleResponseModel>
  35.                 {
  36.                         return QueryController.query<Reinforced.Typings.Samples.Difficult.CodeGenerators.Models.ISampleResponseModel>(
  37.                                 '/JQuery/ReturningObject',
  38.                                 {  },
  39.                                 loadingPlaceholderSelector,
  40.                                 disableElement
  41.                             );
  42.                 }
  43.                 /** Wrapper method for call ReturningObjectWithParameters of JQueryController */
  44.                 public static ReturningObjectWithParameters(echo: string, loadingPlaceholderSelector: string = '', disableElement: string = '') : JQueryPromise<Reinforced.Typings.Samples.Difficult.CodeGenerators.Models.ISampleResponseModel>
  45.                 {
  46.                         return QueryController.query<Reinforced.Typings.Samples.Difficult.CodeGenerators.Models.ISampleResponseModel>(
  47.                                 '/JQuery/ReturningObjectWithParameters',
  48.                                 { 'echo': echo },
  49.                                 loadingPlaceholderSelector,
  50.                                 disableElement
  51.                             );
  52.                 }
  53.                 /** Wrapper method for call VoidMethodWithParameters of JQueryController */
  54.                 public static VoidMethodWithParameters(model: Reinforced.Typings.Samples.Difficult.CodeGenerators.Models.ISampleResponseModel, loadingPlaceholderSelector: string = '', disableElement: string = '') : JQueryPromise<any>
  55.                 {
  56.                         return QueryController.query<any>(
  57.                                 '/JQuery/VoidMethodWithParameters',
  58.                                 { 'model': model },
  59.                                 loadingPlaceholderSelector,
  60.                                 disableElement
  61.                             );
  62.                 }
  63.         }
  64. }
  65.  


Так же важный момент — если ваш кодогенератор отработал неправильно и выдал ошибку, которая сделала ваши TypeScript-ы несобираемыми и теперь у вас не собирается солюшен, чтобы сгенерировать новые тайпинги — пойдите в Reinforced.Typings.settings.xml, что в корне вашего проекта и установите там RtBypassTypeScriptCompilation в true. Это сместит MSBuild-задачу сборки TypeScript-ов так, чтобы она вызывалась после сборки проекта (а не до, как происходит по умолчанию). Однако, не забудьте вернуть потом обратно, ибо как будучи активным в ходе выполнения задач паблишинга, этот параметр может привести к тому, что тайпскрипты не будут пересобираться перед публикацией. И это не особо весело.

Шаг 4. Использование


На этом моменте вы можете вернуться в ваш IndexPage.ts и использовать статические вызовы из сгенерированного нами класса JQueryController. В целом все достаточно прозаично и монотонно, поэтому я порекомендую вам самостоятельно этим заняться и испытать как работает IntelliSense. Код всего IndexPage.ts приведен ниже:

  1.  
  2. module Reinforced.Typings.Samples.Difficult.CodeGenerators.Pages {
  3.     import JQueryController = Reinforced.Typings.Samples.Difficult.CodeGenerators.Controllers.JQueryController;
  4.  
  5.     export class IndexPage {
  6.         constructor() {
  7.             $('#btnSimpleInt').click(this.btnSimpleIntClick.bind(this));
  8.             $('#btnMethodWithParameters').click(this.btnMethodWithParametersClick.bind(this));
  9.             $('#btnReturningObject').click(this.btnReturningObjectClick.bind(this));
  10.             $('#btnReturningObjectWithParameters').click(this.btnReturningObjectWithParametersClick.bind(this));
  11.             $('#btnVoidMethodWithParameters').click(this.btnVoidMethodWithParametersClick.bind(this));
  12.         }
  13.  
  14.         private btnSimpleIntClick() {
  15.             JQueryController.SimpleIntMethod('#loading', '#btnSimpleInt')
  16.                 .then(r => $('#result').html('Server tells us ' + r));
  17.         }
  18.  
  19.         private btnMethodWithParametersClick() {
  20.             JQueryController.MethodWithParameters(Math.random(), 'string' + Math.random(), Math.random() > 0.5, '#loading', '#btnMethodWithParameters')
  21.                 .then(r => {
  22.                 $('#result').html(r);
  23.             });
  24.         }
  25.  
  26.         private btnReturningObjectClick() {
  27.             JQueryController.ReturningObject('#loading', '#btnReturningObject')
  28.                 .then(r => {
  29.                 var s = "<pre> { <br/>";
  30.                 for (var key in r) {
  31.                     s += `  ${key}: ${r[key]},\n`;
  32.                 }
  33.                 s += '} </pre>';
  34.                 $('#result').html(s);
  35.             });
  36.         }
  37.  
  38.         private btnReturningObjectWithParametersClick() {
  39.             var str = 'Random number: ' + Math.random() * 100;
  40.             JQueryController.ReturningObjectWithParameters(str, '#loading', '#btnReturningObjectWithParameters')
  41.                 .then(r => {
  42.                 var s = "<pre> { <br/>";
  43.                 for (var key in r) {
  44.                     s += `  ${key}: ${r[key]},\n`;
  45.                 }
  46.                 s += '} </pre>';
  47.                 $('#result').html(s);
  48.             });
  49.         }
  50.  
  51.         private btnVoidMethodWithParametersClick() {
  52.             JQueryController.VoidMethodWithParameters({
  53.                 Message: 'Hello',
  54.                 Success: true,
  55.                 CurrentTime: null
  56.             }, '#loading', '#btnVoidMethodWithParameters')
  57.                 .then(() => {
  58.                 $('#result').html('Void method executed but it does not return result');
  59.             });
  60.         }
  61.     }
  62. }
  63.  


Для любителей Angular


В принципе все то же самое. Однако, помимо генератора методов, нам понадобится генератор для всего класса, ибо как в лучших традициях ангуляра, неплохо будет экспортировать наш серверный контроллер в angular-сервис. Итак, поставьте DefinitelyTyped для angular.js, подключите сам angular через NuGet и давайте глянем на код генератора методов. Он немного отличается от jQuery-евского в силу того, что надо использовать другие промисы, а так же использовать $http.post вместо $.ajax:

  1.  
  2. using System;
  3. using System.Linq;
  4. using System.Reflection;
  5. using Reinforced.Typings.Ast;
  6. using Reinforced.Typings.Generators;
  7.  
  8. /// <summary>
  9. /// Генератор оберток методов для angular.js
  10. /// </summary>
  11. public class AngularActionCallGenerator : MethodCodeGenerator
  12. {
  13.     public override RtFuncion GenerateNode(MethodInfo element, RtFuncion result, TypeResolver resolver)
  14.     {
  15.         result = base.GenerateNode(element, result, resolver);
  16.         if (result == null) return null;
  17.  
  18.         // перегружаем тип метода под наш промис
  19.         var retType = result.ReturnType;
  20.         bool isVoid = (retType is RtSimpleTypeName) && (((RtSimpleTypeName)retType).TypeName == "void");
  21.  
  22.         // точно такой же трюк с void-методами
  23.         if (isVoid) retType = resolver.ResolveTypeName(typeof(object));
  24.  
  25.         // используем angular.IPromise вместо JQueryPromise
  26.         result.ReturnType = new RtSimpleTypeName(new[] { retType }, "angular", "IPromise");
  27.  
  28.         // параметры метода - по такому же принципу
  29.         var p = element.GetParameters().Select(c => string.Format("'{0}': {0}", c.GetName()));
  30.         var dataParameters = string.Join(", ", p);
  31.  
  32.         // Достаем путь к контроллеру
  33.         string controller = element.DeclaringType.Name.Replace("Controller", String.Empty);
  34.         string path = String.Format("/{0}/{1}", controller, element.Name);
  35.  
  36.         // Генерируем код через this.http.post
  37.         const string code = @"var params = {{ {1} }};
  38. return this.http.post('{0}', params)
  39. .then((response) => {{ return response.data; }});";
  40.  
  41.         // так же оборачиваем в RtRaw
  42.         RtRaw body = new RtRaw(String.Format(code, path, dataParameters));
  43.         result.Body = body;
  44.         return result;
  45.     }
  46. }
  47.  


Вот и вся магия. Не забудьте задать этот кодогенератор для всех angular-friendly методов контроллеров через атрибут или через Fluent-конфигурацию.

А теперь давайте сделаем кодогенератор для класса, который будет содержать эти методы. Специфика в том, что методы у нас отныне не статические, нам понадобится сделать приватное поле http, которое будет хранить сервис $http, конструктор, который будет инжектить этот сервис, а так же регистрацию нашего контроллер-сервиса в нашем приложении.
Здесь я предполагаю что у вас уже есть где-то создание модуля вашего приложения посредством вызова angular.module и сам модуль лежит в глобальной переменной app.

  1.  
  2. using System;
  3. using Reinforced.Typings.Ast;
  4. using Reinforced.Typings.Generators;
  5.  
  6. /// <summary>
  7. /// Генератор, оборачивающий наши методы в angular-сервис.
  8. /// наследуем от стандартного генератора классов
  9. /// </summary>
  10. public class AngularControllerGenerator : ClassCodeGenerator
  11. {
  12.     public override RtClass GenerateNode(Type element, RtClass result, TypeResolver resolver)
  13.     {
  14.         // Опять же - начинаем со "стандартного" класса, сгенерированного для контроллера
  15.         result = base.GenerateNode(element, result, resolver);
  16.         if (result == null) return null;
  17.  
  18.         // Добавим немножко документации
  19.         result.Documentation = new RtJsdocNode(){Description = "Result of AngularControllerGenerator activity"};
  20.  
  21.         // создаем имя типа angular.IHttpService
  22.         var httpServiceType = new RtSimpleTypeName("IHttpService") { Namespace = "angular" };
  23.  
  24.         // Добавлем конструктор,...        
  25.         RtConstructor constructor = new RtConstructor();
  26.         // ... принимающий $http: angular.IHttpService
  27.         constructor.Arguments.Add(new RtArgument(){Type = httpServiceType,Identifier = new RtIdentifier("$http")});
  28.         // его тело будет содержать единственную строчку -
  29.         // занесение $http в локальное поле
  30.         constructor.Body = new RtRaw("this.http = $http;");
  31.  
  32.         // Описываем поле, которое будет держать в себе $http
  33.         RtField httpServiceField = new RtField()
  34.         {
  35.             Type = httpServiceType,
  36.             Identifier = new RtIdentifier("http"),
  37.             AccessModifier = AccessModifier.Private,
  38.             Documentation = new RtJsdocNode() { Description = "Хранит экземпляр $http полученный при вызове конструктора"}
  39.         };
  40.  
  41.         // Добавляем констркутор и поле в наш класс
  42.         result.Members.Add(httpServiceField);
  43.         result.Members.Add(constructor);
  44.  
  45.         // Код для строчки, которая будет регистрировать наш класс в модуле для последующей инъекции
  46.         // регистрация будет идти под именем Api.%Something%Controller
  47.         const string initializerFormat =
  48.             "if (window['app']) window['app'].factory('Api.{0}', ['$http', ($http: angular.IHttpService) => new {1}($http)]);";
  49.  
  50.         // подставляем параметры
  51.         RtRaw registration = new RtRaw(String.Format(initializerFormat,element.Name,result.Name));
  52.        
  53.         // Находим текущий модуль в контексте экспорта (свойство генератора Context), берем у него список юнитов компиляции
  54.         // Для этого используем Context.Location.CurrentModule.CompilationUnits
  55.         // Коллекция CompilationUnits собирает в себе RtNode, так что
  56.         // в нее можно запихнуть все, вплоть до RtRaw
  57.         // По сему, смело пихаем регистрацию прямо вот туда        
  58.         Context.Location.CurrentModule.CompilationUnits.Add(registration);        
  59.         return result;
  60.     }
  61. }
  62.  


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

  1.  
  2. module Reinforced.Typings.Samples.Difficult.CodeGenerators.Controllers {
  3.         if (window['app']) window['app'].factory('Api.AngularController', ['$http', ($http: ng.IHttpService) => new AngularController($http)]);
  4.         /** Result of AngularControllerGenerator activity */
  5.         export class AngularController
  6.         {
  7.                 constructor ($http: ng.IHttpService)
  8.                 {
  9.                         this.http = $http;
  10.                 }
  11.                 public SimpleIntMethod() : ng.IPromise<number>
  12.                 {
  13.                         var params = {  };
  14.                         return this.http.post('/Angular/SimpleIntMethod', params)
  15.                             .then((response) => { response.data['requestParams'] = params; return response.data; });
  16.                 }
  17.                 public MethodWithParameters(num: number, s: string, boolValue: boolean) : ng.IPromise<string>
  18.                 {
  19.                         var params = { 'num': num, 's': s, 'boolValue': boolValue };
  20.                         return this.http.post('/Angular/MethodWithParameters', params)
  21.                             .then((response) => { response.data['requestParams'] = params; return response.data; });
  22.                 }
  23.                 public ReturningObject() : ng.IPromise<Reinforced.Typings.Samples.Difficult.CodeGenerators.Models.ISampleResponseModel>
  24.                 {
  25.                         var params = {  };
  26.                         return this.http.post('/Angular/ReturningObject', params)
  27.                             .then((response) => { response.data['requestParams'] = params; return response.data; });
  28.                 }
  29.                 public ReturningObjectWithParameters(echo: string) : ng.IPromise<Reinforced.Typings.Samples.Difficult.CodeGenerators.Models.ISampleResponseModel>
  30.                 {
  31.                         var params = { 'echo': echo };
  32.                         return this.http.post('/Angular/ReturningObjectWithParameters', params)
  33.                             .then((response) => { response.data['requestParams'] = params; return response.data; });
  34.                 }
  35.                 public VoidMethodWithParameters(model: Reinforced.Typings.Samples.Difficult.CodeGenerators.Models.ISampleResponseModel) : ng.IPromise<any>
  36.                 {
  37.                         var params = { 'model': model };
  38.                         return this.http.post('/Angular/VoidMethodWithParameters', params)
  39.                             .then((response) => { response.data['requestParams'] = params; return response.data; });
  40.                 }
  41.                 /** Keeps $http instance received on construction */
  42.                 private http: ng.IHttpService;
  43.         }
  44. }
  45.  


Кстати, полная версия файла из примера вживую лежит вот тут.

Добавляем документацию


Так же в RT присутствует возможность автоматически импортировать в полученный TypeScript-код XMLDOC из методов контроллеров. Помимо дописывания RtJsdocNode напрямую руками, возможен следующий подход:
1) Пройдите в Reinforced.Typings.settings.xml и установите там параметр RtGenerateDocumentation в True
2) Включите экспорт XMLDOC-а для вашего проекта. Чтобы сделать это, щелкните правой кнопкой мыши на вашем .csproj-е, выберите Properties, пройдите на вкладку Build и поставьте галочку «XML Documentation File» в секции Output (см. картинку). После чего сохраните проект через Ctrl-S
галочка Xmldoc
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

______________________
Текст подготовлен в Редакторе Блогов от SoftCoder.ru
Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
+7
Comments 2
Comments Comments 2

Articles