📜 ⬆️ ⬇️

Reinforced.Typings - TypeScript Angular services directly from your MVC controllers

Hello
And here, another (and last) article is an example of my framework for generating a TypeScript glue code: Reinforced.Typings (before that there was another one and more ). Today, we learn how to automatically generate TypeScript wrappers for calls to MVC controller methods, provide them with documentation and lay them out in different files. I hope you will appreciate how quickly and easily such problems are solved using RT. As part of my tutorial, we implement the generation of a helper class for calling controller methods using jQuery and promises, as well as a service for angular.js, which is ready for embedding into your application and bringing transparent calls to server methods. Next, we will include in the generated TypeScript documentation for this case (import from XMLDOC) and decompose it in files so that it does not mix. Anyone interested and willing to have such a thing in their projects, welcome under cat.

Prologue


In short, RT uses the code generator system to generate the source TypeScript code. Recently (version 1.2.0) I abandoned the practice of writing TypeScript code directly into an open text stream (which was inconvenient, but very quickly) and came to the practice of using a very simple AST (Abstract Syntax Tree) . In fact, this tree is very basic, far from complete, and it is focused on TypeScript declaration elements. Well, that is, without expressions, statements, switches, variable creatures, etc. Only classes / interfaces, methods, fields, modules. After the introduction of AST, I will not hide, it became easier. Including formatting the displayed code, as opposed to the old solution, when the user had to take care of formatting on his own. Yes, of course, in a certain sense, this is a compromise in usher of flexibility, but in terms of profitability and ease of use, but in general, it seems to me, the game is worth the candle. In addition, in the main places no one restricts you in the type of tree nodes. What does it mean? And this means that you can insert an abusive commentary (even multi-line) into the middle of a class through AST. Yes, yes, and he also correctly set the tab at the beginning of the line. Well, let's get started.

Right off the bat


Let me explain with a simple example - generating jQuery wrappers with promises for controller calls. Because the angular way may be alien to someone, but with jQuery, in general, everything is clear.

So, this is given: a simple ASP.NET MVC application, jQuery, a controller with methods that you want to pull from the client's TypeScript, several View models and your itchy hands.
Task: set the server API in the TypeScript class, so as not to be steamed with the constant $ .post / $. Load, or, well, $ .ajax.
Decision:

Step 0. Infrastructure


I will mark here the test code that we have, so that it is something to make a start from.
')
Modelka:
  1. public class SampleResponseModel { public string Message { get ; set ; } public bool Success { get ; set ; } public string CurrentTime { get ; set ; } }
  2. public class SampleResponseModel { public string Message { get ; set ; } public bool Success { get ; set ; } public string CurrentTime { get ; set ; } }
  3. public class SampleResponseModel { public string Message { get ; set ; } public bool Success { get ; set ; } public string CurrentTime { get ; set ; } }
  4. public class SampleResponseModel { public string Message { get ; set ; } public bool Success { get ; set ; } public string CurrentTime { get ; set ; } }
  5. public class SampleResponseModel { public string Message { get ; set ; } public bool Success { get ; set ; } public string CurrentTime { get ; set ; } }
  6. public class SampleResponseModel { public string Message { get ; set ; } public bool Success { get ; set ; } public string CurrentTime { get ; set ; } }


Controller:

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


In addition, we will get a TypeScript file (which I have in /Scripts/IndexPage.ts) for testing. Here is this (for example, MVVM followers forgive me, the material below will be for you):

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


Important! Don't forget to set yourself the jQuery taypings from NuGet from DefinitelyTyped. Otherwise, there will be problems with compiling TS.

Well and a sin not to make test (it is supposed that jQuery at you is already connected). Rewrite your Index.cshtml, for example, like this:

  1. < span id = "loading" > < / span >
  2. Result: < strong id = "result" > < / strong >
  3. < button class = "btn btn-primary" id = "btnSimpleInt" > Tynts < / button >
  4. < button class = "btn btn-default" id = "btnMethodWithParameters" > Klac < / button >
  5. < button class = "btn btn-default" id = "btnReturningObject" > Bums < / button >
  6. < button class = "btn btn-default" id = "btnReturningObjectWithParameters" > Bidd < / button >
  7. < button class = "btn btn-default" id = "btnVoidMethodWithParameters" > Quack! < / button >
  8. @section Scripts
  9. {
  10. < script type = "text / javascript" src = "/Scripts/IndexPage.js" > < / script >
  11. < script type = "text / javascript" src = "/Scripts/query.js" > < / script >
  12. < script type = "text / javascript" >
  13. $ (document) .ready (function () {
  14. var a = new Reinforced.Typings.Samples.Difficult.CodeGenerators.Pages.IndexPage ();
  15. })
  16. < / script >
  17. }


You must have noticed the inclusion of query.js in the document. Do not worry - it is necessary, in the next section I will explain what's what.

Everything, it was a footcloth test infrastructure. It will be easier later.

Step 1. Requests


First of all, let's move only one method into a separate TypeScript class, which we will use to query the server. Just jQuery in this regard is somewhat cumbersome and I would not like to repeat all this sample code in each method for the sake of readability. So, a small class with a small method to make requests to the server:

/Scripts/query.ts

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


As you can see, the method is extremely simple; it takes as input the URL for the request, the data (any JS object), and also to make it easier to use, I made two parameters: one to designate the selector of the HTML element that should be disabled at the time of the request, and one ... Well, about the same, only for the element to which the inscription "Loading ..." will be subscribed. I suppose it is self-evident that the inscription “Loading ...” you can easily replace with your own. Returns this jQuery-yevsky Promise method to which you can add .then / .fail / .end and others. However, I believe that the target audience of this article and without me know how to work with promises. :)

Step 2. Attributes


Since through reflection it is impossible to determine what our controller methods return, we will make an attribute that will mark all methods of our JQueryController, for which we need to generate a wrapper. It will look, for example, like this:

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


Here we kill two birds with one stone and specify the CodeGeneratorType for this method. Do not worry that it is not there yet - we will write it in the next section. As for the rest, the existing attribute can be placed above our controller methods. At the same time we will put [TsClass], without departing from the cash register:

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


Just do not forget to put [TsInterface] on the model. Otherwise, as Hmayak Hakobyan used to say, “no miracle will happen”.

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


Step 3. Code generator


The theory is relatively simple. RT collects in a heap information about what you want to see from your C # types in the resulting TypeScript and in what form, then begins to go round this good in depth, starting with namespaces and going down to the members of the class and the parameters of the declared methods. Upon encountering this or that entity, RT calls the corresponding code generator (it instantiates instances of code generators lazily, with “1 instance for the whole export process” with flytime). This process can be influenced by specifying which code generator you want to use. This can be done using the attribute configuration (CodeGeneratorType field, which is in any RT attribute) - there is no “fool protection”, but it is assumed that you specify the typeof of the heir from the Reinforced.Typings.Generators.ITsCodeGenerator interface with the correct T corresponding to the entity above which is the attribute. You can also use the Fluent call .WithCodeGenerator. There is already a "foolproof" and you will not be able to specify an unsuitable code generator.
There are several built-in code generators available. They are all located in the Reinforced.Typings.Generators namespace. There are code generators for the interface, class, constructor, enumeration, field, property, method, and method parameter. The easiest way to make your code generator is to inherit from the existing one. Further (by increasing complexity) - inherit from TsCodeGeneratorBase <T1, T2>, where T1 is the type of input reflection-entity, and T2 is the type of result in the syntax tree (heir to RtNode). Well, the difficult option is to implement the ITsCodeGenerator on-line independently, which in most cases is not recommended, because it requires knowledge of some subtleties and reading the source, and there is no need.
We will make our JQueryActionCallGenerator simply inheriting from the built-in code generator for methods. So, here is our code generator with detailed comments:

  1. using System ;
  2. using System.Linq ;
  3. using System.Reflection ;
  4. using Reinforced.Typings.Ast ;
  5. using Reinforced.Typings.Generators ;
  6. /// <summary>
  7. /// Our code generator for wrappers on calls to the controller method.
  8. /// It is sharpened to take MethodInfo as input and produce as output
  9. /// instance of RtFunction, which is a piece of the syntax tree
  10. /// with TypeScript.
  11. /// As mentioned above, we inherit from the built-in RT MethodCodeGenerator,
  12. /// which normally generates a method with a signature and an empty implementation
  13. /// </ summary>
  14. public class JQueryActionCallGenerator : MethodCodeGenerator
  15. {
  16. /// <summary>
  17. /// Naturally, we overload the GenerateNode method. Actually, it is almost the only
  18. /// and the main method in the code generator.
  19. /// </ summary>
  20. /// <param name = "element"> Method for which code will be generated as MethodInfo </ param>
  21. /// <param name = "result">
  22. /// Result AST-node (RtFunction).
  23. /// We do not create a node from scratch. And to be honest, I forgot why I made such an architectural decision :)
  24. /// But we can still return null to exclude the node / method from the final TypeScript file
  25. /// </ param>
  26. /// <param name = "resolver">
  27. /// And this is an instance of TypeResolver. This is a special class that we can use.
  28. /// to display types in the resulting TypeScript, so as not to do anything by hand.
  29. /// </ param>
  30. /// <returns> RtFunction (also known as the syntax tree node, also known as the AST node) </ returns>
  31. public override RtFuncion GenerateNode ( MethodInfo element, RtFuncion result, TypeResolver resolver )
  32. {
  33. // First, generate the "as usual" method wrapper
  34. // Next, we will expand and complement it
  35. result = base . GenerateNode ( element, result, resolver ) ;
  36. // If for some reason an empty node is generated, then it should be
  37. // we will not interfere in this process
  38. if ( result == null ) return null ;
  39. // Make our method static
  40. result . IsStatic = true ;
  41. // And also add a couple of extra parameters to the method to specify
  42. // element that will be disabled while the server method is being called,
  43. // as well as the element to which the loading indicator should be added
  44. result . Arguments . Add (
  45. new RtArgument ( )
  46. {
  47. Identifier = new RtIdentifier ( "loadingPlaceholderSelector" ) ,
  48. Type = resolver . ResolveTypeName ( typeof ( string ) ) ,
  49. DefaultValue = "''"
  50. } ) ;
  51. result . Arguments . Add (
  52. new RtArgument ( )
  53. {
  54. Identifier = new RtIdentifier ( "disableElement" ) ,
  55. Type = resolver . ResolveTypeName ( typeof ( string ) ) ,
  56. DefaultValue = "''"
  57. } ) ;
  58. // Store the original method return value
  59. // for further we need to parameterize it with jQueryPromise
  60. var retType = result . ReturnType ;
  61. // ... and if the return type is void, we just leave JQueryPromise <any>
  62. bool isVoid = ( retType is RtSimpleTypeName ) && ( ( ( RtSimpleTypeName ) retType ) . TypeName == "void" ) ;
  63. // here I use TypeResolver to get an AST node for the name of the type "any"
  64. // just to demonstrate the use of TypeResolver
  65. // (well, also because I'm lazy enough to create with any hands)
  66. if ( isVoid ) retType = resolver . ResolveTypeName ( typeof ( object ) ) ;
  67. // OK, redefine the return value of our method, wrapping it
  68. // in jQueryPromise
  69. // We use the RtSimpleType class, passing generic arguments to it,
  70. // so as not to write angle brackets
  71. result . ReturnType = new RtSimpleTypeName ( "JQueryPromise" , new [ ] { retType } ) ;
  72. // Now it's up to the parameters. We get them through reflection.
  73. // Next, we use the extension. GetName () to get the name of the parameter
  74. // This extension comes with Reinforced.Typings and returns the name of the parameter.
  75. // method with all potential overloads via Fluent configuration or
  76. // attribute [TsParameter]
  77. var p = element . GetParameters ( ) . Select ( c => string . Format ( "'{0}': {0}" , c . GetName ( ) ) ) ;
  78. // Link parameters to generate method body code
  79. var dataParameters = string . Join ( "," , p ) ;
  80. // Get the path to the controller
  81. // Here we have a simple solution that requires having a MVC route on / {controller} / {action}
  82. // it is assumed that he is (although whom I cheat, he is in 90% of applications)
  83. string controller = element . DeclaringType . Name . Replace ( "Controller" , String . Empty ) ;
  84. string path = String . Format ( "/ {0} / {1}" , controller, element . Name ) ;
  85. // Now let's create a glue-code with a call to QueryController, which we defined in query.ts
  86. string code = String . Format (
  87. @ "return QueryController.query <{2}> (
  88. '{0}',
  89. {{ {one} }},
  90. loadingPlaceholderSelector,
  91. disableElement
  92. ); " ,
  93. path, dataParameters, retType ) ;
  94. // Wrap the code in RtRaw and stuff as the body into our result
  95. result . Body = new RtRaw ( code ) ;
  96. // Now let's add some JSDOC to make the results of our work more obvious.
  97. result . Documentation = new RtJsdocNode ( ) { Description = String . Format ( "Wrapper method for call {0} of {1}" , element . Name , element . DeclaringType . Name ) } ;
  98. // Actually, that's all. Return RtFunction
  99. // According to the default settings of the config, this good will be recorded in the project.ts
  100. return result ;
  101. }
  102. }


Kodogenerator ready. It remains only to make our project Rebuild. If you have any difficulties with debugging generators, then you can use the Context generator property of type ExportContext. In addition, there is a property Warnings, which is a collection of RtWarning-s. Add there your own - it will appear in the Warnings window in the studio during the build (this feature was added just recently, in version 1.2.2).

For those who can not wait, here is the result of the work of the code generator:
  1. module Reinforced. Typings . Samples . Difficult . CodeGenerators . Models {
  2. export interface ISampleResponseModel
  3. {
  4. Message : string ;
  5. Success : boolean ;
  6. CurrentTime : string ;
  7. }
  8. }
  9. module Reinforced. Typings . Samples . Difficult . CodeGenerators . Controllers {
  10. export class jQueryController
  11. {
  12. / ** Wrapper method for call SimpleIntMethod of JQueryController * /
  13. public static SimpleIntMethod ( loadingPlaceholderSelector : string = '' , disableElement : string = '' ) : JQueryPromise < number >
  14. {
  15. return QueryController. query < number > (
  16. '/ JQuery / SimpleIntMethod' ,
  17. { }
  18. loadingPlaceholderSelector ,
  19. disableElement
  20. ) ;
  21. }
  22. / ** Wrapper method for call MethodWithParameters of JQueryController * /
  23. public static MethodWithParameters ( num : number , s : string , boolValue : boolean , loadingPlaceholder : string : string = '' , disableElement : string = '' ) : JQueryPromise < string >
  24. {
  25. return QueryController. query < string > (
  26. '/ JQuery / MethodWithParameters' ,
  27. { 'num' : num , 's' : s , 'boolValue' : boolValue } ,
  28. loadingPlaceholderSelector ,
  29. disableElement
  30. ) ;
  31. }
  32. / ** Wrapper method for call ReturningObject of JQueryController * /
  33. public static ReturningObject ( loadingPlaceholderSelector : string = '' , disableElement : string = '' ) : JQueryPromise < Reinforced. Typings . Samples . Difficult . CodeGenerators . Models . ISampleResponseModel >
  34. {
  35. return QueryController. query < Reinforced. Typings . Samples . Difficult . CodeGenerators . Models . ISampleResponseModel > (
  36. '/ JQuery / ReturningObject' ,
  37. { }
  38. loadingPlaceholderSelector ,
  39. disableElement
  40. ) ;
  41. }
  42. / ** Wrapper method for call ReturningObjectWithParameters of JQueryController * /
  43. public static ReturningObjectWithParameters ( echo : string , loadingPlaceholderSelector : string = '' , disableElement : string = '' ) : JQueryPromise < Reinforced. Typings . Samples . Difficult . CodeGenerators . Models . ISampleResponseModel >
  44. {
  45. return QueryController. query < Reinforced. Typings . Samples . Difficult . CodeGenerators . Models . ISampleResponseModel > (
  46. '/ JQuery / ReturningObjectWithParameters' ,
  47. { 'echo' : echo } ,
  48. loadingPlaceholderSelector ,
  49. disableElement
  50. ) ;
  51. }
  52. / ** Wrapper method for call VoidMethodWithParameters of JQueryController * /
  53. public static VoidMethodWithParameters ( model : Reinforced. Typings . Samples . Difficult . CodeGenerators . Models . ISampleResponseModel , loadingPlaceholderSelector : string = '' , disableElement : string = '' ) : JQueryPromise < any >
  54. {
  55. return QueryController. query < any > (
  56. '/ JQuery / VoidMethodWithParameters' ,
  57. { 'model' : model } ,
  58. loadingPlaceholderSelector ,
  59. disableElement
  60. ) ;
  61. }
  62. }
  63. }


It is also an important point - if your code generator worked incorrectly and produced an error that made your TypeScript uncollectible and now you have no solution to generate new taipings, go to Reinforced.Typings.settings.xml, which is at the root of your project and set there RtBypassTypeScriptCompilation to true. This will shift the MSBuild task of building TypeScripts so that it is called after the project is built (and not before, as happens by default). However, do not forget to return it later, because as being active during the execution of publishing tasks, this parameter can lead to the fact that the typing scripts will not be rebuilt before publishing. And this is not really fun.

Step 4. Use


At this point, you can return to your IndexPage.ts and use static calls from the JQueryController class that we generated. In general, everything is quite prosaic and monotonous, so I will recommend that you do it yourself and experience how IntelliSense works. The code for the entire IndexPage.ts is shown below:

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


For lovers of Angular


In principle, all the same. However, in addition to the method generator, we will need a generator for the entire class, because, as in the best traditions of Angulyar, it would be nice to export our server controller to an angular service. So, set DefinitelyTyped for angular.js, connect the angular itself via NuGet and let's look at the code of the method generator. It is slightly different from jQuery-evskogo due to the fact that you need to use other promises, as well as use $ http.post instead of $ .ajax:

  1. using System ;
  2. using System.Linq ;
  3. using System.Reflection ;
  4. using Reinforced.Typings.Ast ;
  5. using Reinforced.Typings.Generators ;
  6. /// <summary>
  7. /// Generator wrapper methods for angular.js
  8. /// </ summary>
  9. public class AngularActionCallGenerator : MethodCodeGenerator
  10. {
  11. public override RtFuncion GenerateNode ( MethodInfo element, RtFuncion result, TypeResolver resolver )
  12. {
  13. result = base . GenerateNode ( element, result, resolver ) ;
  14. if ( result == null ) return null ;
  15. // overload the type of method under our promise
  16. var retType = result . ReturnType ;
  17. bool isVoid = ( retType is RtSimpleTypeName ) && ( ( ( RtSimpleTypeName ) retType ) . TypeName == "void" ) ;
  18. // exactly the same trick with void methods
  19. if ( isVoid ) retType = resolver . ResolveTypeName ( typeof ( object ) ) ;
  20. // use angular.IPromise instead of jQueryPromise
  21. result . ReturnType = new RtSimpleTypeName ( new [ ] { retType } , "angular" , "IPromise" ) ;
  22. // method parameters - by the same principle
  23. var p = element . GetParameters ( ) . Select ( c => string . Format ( "'{0}': {0}" , c . GetName ( ) ) ) ;
  24. var dataParameters = string . Join ( "," , p ) ;
  25. // Get the path to the controller
  26. string controller = element . DeclaringType . Name . Replace ( "Controller" , String . Empty ) ;
  27. string path = String . Format ( "/ {0} / {1}" , controller, element . Name ) ;
  28. // Generate code via this.http.post
  29. const string code = @ "var params = {{{1}}};
  30. return this.http.post ('{0}', params)
  31. .then ((response) => {{return response.data;}}); " ;
  32. // also wrapped in RtRaw
  33. RtRaw body = new RtRaw ( String . Format ( code, path, dataParameters ) ) ;
  34. result . Body = body ;
  35. return result ;
  36. }
  37. }


That's all the magic. Do not forget to set this code generator for all angular-friendly controller methods via an attribute or through a Fluent configuration.

And now let's make a code generator for the class that will contain these methods. The specificity is that the methods are no longer static, we will need to make a private http field that will store the $ http service, a constructor that will inject this service, and also register our controller service in our application.
Here I assume that you already have somewhere creating a module of your application by calling angular.module and the module itself is in the global variable app.

  1. using System ;
  2. using Reinforced.Typings.Ast ;
  3. using Reinforced.Typings.Generators ;
  4. /// <summary>
  5. /// A generator that wraps our methods in an angular service.
  6. /// inherit from the standard class generator
  7. /// </ summary>
  8. public class AngularControllerGenerator : ClassCodeGenerator
  9. {
  10. public override RtClass GenerateNode ( Type element, RtClass result, TypeResolver resolver )
  11. {
  12. // Again, start with the "standard" class generated for the controller
  13. result = base . GenerateNode ( element, result, resolver ) ;
  14. if ( result == null ) return null ;
  15. // Add some documentation
  16. result . Documentation = new RtJsdocNode ( ) { Description = "Result of AngularControllerGenerator activity" } ;
  17. // create the type name angular.IHttpService
  18. var httpServiceType = new RtSimpleTypeName ( "IHttpService" ) { Namespace = "angular" } ;
  19. // Add a constructor, ...
  20. RtConstructor constructor = new RtConstructor ( ) ;
  21. // ... host $ http: angular.IHttpService
  22. constructor . Arguments . Add ( new RtArgument ( ) { Type = httpServiceType, Identifier = new RtIdentifier ( "$ http" ) } ) ;
  23. // his body will contain a single line -
  24. // $http
  25. constructor . Body = new RtRaw ( "this.http = $http;" ) ;
  26. // , $http
  27. RtField httpServiceField = new RtField ( )
  28. {
  29. Type = httpServiceType,
  30. Identifier = new RtIdentifier ( "http" ) ,
  31. AccessModifier = AccessModifier . Private ,
  32. Documentation = new RtJsdocNode ( ) { Description = " $http " }
  33. } ;
  34. //
  35. result . Members . Add ( httpServiceField ) ;
  36. result . Members . Add ( constructor ) ;
  37. // ,
  38. // Api.%Something%Controller
  39. const string initializerFormat =
  40. "if (window['app']) window['app'].factory('Api.{0}', ['$http', ($http: angular.IHttpService) => new {1}($http)]);" ;
  41. //
  42. RtRaw registration = new RtRaw ( String . Format ( initializerFormat,element . Name ,result . Name ) ) ;
  43. // ( Context),
  44. // Context.Location.CurrentModule.CompilationUnits
  45. // CompilationUnits RtNode,
  46. // , RtRaw
  47. // ,
  48. Context . The Location . CurrentModule . CompilationUnits . Add ( registration ) ;
  49. return result ;
  50. }
  51. }


Attach this code generator to your controllers in any way (attribute or Fluent configuration). After that, rebuild the project and safely inject the resulting service into your angular controllers.
Again, the result of this code generator for impatient:

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


By the way, the full version of the file from the example live is here .

Add documentation


Also in RT there is the ability to automatically import XMLDOC from the controller methods into the resulting TypeScript code. In addition to writing RtJsdocNode directly with your hands, the following approach is possible:
1) Go to Reinforced.Typings.settings.xml and set the RtGenerateDocumentation parameter to True
2) Turn on the export of XMLDOC for your project. To do this, right-click on your .csproj-e, select Properties, go to the Build tab and tick the “XML Documentation File” in the Output section (see picture). Then save the project via Ctrl-S
tick xmldoc
3) Reassemble to see the changes. Here it is important to notice two features: firstly, by default, this tick is already in the Release-configuration. Therefore, as an option, you can set the RtGenerateDocumentation parameter and switch the project build configuration to Release.
The second feature is that if you, by fate, export types with a fluent configuration from another assembly, then RT you need to explicitly indicate which documentation file to include (besides the main one). This can be done with the help of the Fluent-call ConfigurationBuilder.TryLookupDocumentationForAssembly, to which the assembly from which the export and the full path to the XML-file with the documentation are transferred.

We decompose by different files.


To activate the layout mode for different files, you will need to enable another build setting. so
1) Go to Reinforced.Typings.settings.xml, set RtDivideTypesAmongFiles to true. Next, find the RtTargetDirectory parameter — it specifies the target folder into which the generated typescript files are dumped. Correct this parameter if necessary
2) Determine which code will fall into which files. This can be done using the [TsFile] attribute, or the Fluent call .ExportTo, available when using .ExportAsClass (es) /. ExportAsInterface (s) /. ExportAsEnum (s). The parameter takes the path to the file relative to the directory indicated in RtTargetDirectory.
3) Rebuild, add the resulting files to the project

It would seem nothing complicated, but a difficult moment with the directive /// comes up. So - I hasten to please, no additional settings are required. RT is smart enough to place these directives correctly without your participation. However, if you need references to any other files, this is easily solved by the [TsAddTypeReference] attribute or the Fluent call .AdReReference, which is available approximately in the same place as .ExportTo. These methods allow you to add a reference to a file by your exported CLR type, or simply by a raw string. By the way, there is also the [assembly: TsReference] attribute, which will add the desired reference to all generated files. The fluent call to .AddReference does the same when applied to ConfigurationBuilder.

Conclusion


The examples presented are available in the Reinforced.Typings repository . I am open to suggestions, questions and suggestions in the comments to this article, as well as in Issues to the RT repository. The project also has a great github-wiki in English, which I develop as there is free time. It was also (and is) a project with Russian documentation on RTFD, but it is now in suspension.
As for the articles and plans for the future, I see that this is the last article about RT, since RT itself is only a component of my other interesting project - Reinforced.Lattice, which I will post some time later, because as it is now passing through the run-in stage and testing on living people. For some reason it seems to me that you will like it. So, the next article from me will be about Lattice and, most likely, not soon.

Thank you for reading to the end.It helps me not to throw a project :)

Reinforced.Typings on Github
Reinforced.Typings in NuGet

______________________
The text was prepared in the Blog Editor from SoftCoder.ru

Source: https://habr.com/ru/post/278961/


All Articles