Nov 18

Written by: Michael Washington
11/18/2017 6:14 PM  RssIcon

Using the open source Typewriter plug-in with Visual Studio will allow you to automate much of the boilerplate code for your classes and services.

image

A modern .Net Core 2.0 Angular 4+ application is usually composed, on the Angular client-side, of Components, Services and Classes (written in TypeScript), and Controllers and Classes on the server-side.

image

Using Typewriter, the Angular classes and services will be auto-created. This will be accomplished by creating a template (a .tst file) that will read the server-side code and auto-create the corresponding Angular code.

 

Install and Use Typewriter

image

You can download and Install Typewriter at the following link.

It will install as a plug-in in Visual Studio.

Creating Classes

image

In Visual Studio, we will create an Angular project and then create a class using the following code:

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace TypeWriterTest.Models
{
    public class CarDTO
    {
        public int id { get; set; }
        public string carName { get; set; }
        public bool? carActive { get; set; }
        public string lastUpdate { get; set; }
    }
}

 

image

Next we create a classes folder in the Angular section of the application.

We right-click on the classes folder and select Add then New Item…

image

We select TypeScript Template File, name the file Classes.tst, and click Add.

image

We use the following code:

 

${
    // Enable extension methods by adding using Typewriter.Extensions.*
    using Typewriter.Extensions.Types;
    // This custom function will take the type currently being processed and 
    // append a ? next to any type that is nullable
   string TypeFormatted(Property property)
    {
        var type = property.Type;
        if (type.IsNullable)
        {
            return  $"?";
        }
        else
        {
            return  $"";
        }
    }
}
    // $Classes/Enums/Interfaces(filter)[template][separator]
    // filter (optional): Matches the name or full name of the current item. * = match any, 
    // wrap in [] to match attributes or prefix with : to match interfaces or base classes.
    // template: The template to repeat for each matched item
    // separator (optional): A separator template that is placed between all templates e.g. 
    // $Properties[public $name: $Type][, ]
    // More info: http://frhagn.github.io/Typewriter/
    $Classes(*DTO)[
    export interface $Name {$Properties[
        $name$TypeFormatted: $Type;]
    }
    ]

 

When we save the file, a new file, CarDTO.ts, will be created:

image

It will have the following code:

 

    // $Classes/Enums/Interfaces(filter)[template][separator]
    // filter (optional): Matches the name or full name of the current item. 
    // * = match any, wrap in [] to match attributes or prefix with : to match interfaces or base classes.
    // template: The template to repeat for each matched item
    // separator (optional): A separator template that is placed between all templates e.g. 
    // $Properties[public $name: $Type][,]
    // More info: http://frhagn.github.io/Typewriter/
    
    export interface CarDTO {
        id: number;
        carName: string;
        carActive?: boolean;
        lastUpdate: string;
    }

 

Note:

  • The CarDTO.ts file will be automatically updated whenever the corresponding CarDTO.cs file is updated
  • If we add additional server-side classes to the project, additional TypeScript Angular classes will be created for them
  • To understand how templates are created see the documentation on the Typewriter site.

 

Creating Services

image

Create a controller called CarController.cs in the server-side code section of the project using the following code:

 

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using TypeWriterTest.Models;
namespace TypeWriterTest.Controllers
{
    [Route("api/[controller]")]
    public class CarApiController : Controller
    {
        string strCurrentDateTime = 
            $"{DateTime.Now.ToShortDateString()} {DateTime.Now.ToShortTimeString()}";
        // GET: api/CarApi/GetCars
        [AllowAnonymous]
        [HttpGet("[action]")]
        #region public List<CarDTO> GetCars()
        public List<CarDTO> GetCars()
        {            
            // Collection to hold Cars
            List<CarDTO> colCarDTOs = new List<CarDTO>();
            colCarDTOs.Add(new CarDTO() { id = 1, carName = "Car One", carActive = true, lastUpdate = "" });
            colCarDTOs.Add(new CarDTO() { id = 2, carName = "Car Two", carActive = false, lastUpdate = "" });
            colCarDTOs.Add(new CarDTO() { id = 3, carName = "Car Three", carActive = true, lastUpdate = "" });
            return colCarDTOs;
        }
        #endregion
        // PUT: api/CarApi/1
        [HttpPut("{id}")]
        #region public IActionResult Put([FromRoute] int id, [FromBody] CarDTO CarDTO)
        public IActionResult Put([FromRoute] int id, [FromBody] CarDTO CarDTO)
        {
            CarDTO.lastUpdate = strCurrentDateTime;
            return Ok(CarDTO);
        }
        #endregion
        // POST: api/CarApi
        [HttpPost]
        #region public IActionResult Post([FromBody] CarDTO CarDTO)
        public IActionResult Post([FromBody] CarDTO CarDTO)
        {
            CarDTO.lastUpdate = strCurrentDateTime;
            return Ok(CarDTO);
        }
        #endregion
        // DELETE: api/CarApi/1
        [HttpDelete("{id}")]
        #region public IActionResult Delete([FromRoute] int id)
        public IActionResult Delete([FromRoute] int id)
        {
            return NoContent();
        }
        #endregion
    }
}

 

image

Next we create a services folder in the Angular section of the application.

We create a TypeScript Template File, named Services.tst using the following code:

 

${
    using Typewriter.Extensions.WebApi;
    Template(Settings settings)
    {
        settings.OutputFilenameFactory = file => 
        {
            var FinalFileName = file.Name.Replace("Controller", "");
            FinalFileName = FinalFileName.Replace(".cs", "");
            return $"{FinalFileName}.service.ts";
        };
    }
    // Change ApiController to Service
    string ServiceName(Class c) => c.Name.Replace("ApiController", "Service");
    // Turn IActionResult into void
    string ReturnType(Method objMethod) 
    {
        if(objMethod.Type.Name == "IActionResult")
        {
                if((objMethod.Parameters.Where(x => !x.Type.IsPrimitive).FirstOrDefault() != null))
                {
                    return objMethod.Parameters.Where(x => !x.Type.IsPrimitive).FirstOrDefault().Name;
                }
                else
                {
                    return "void";
                }
        } 
        else
        {
                return objMethod.Type.Name;
        }
    }
    // Get the non primitive paramaters so we can create the Imports at the
    // top of the service
    string ImportsList(Class objClass)
    {
        var ImportsOutput = "";
        // Get the methods in the Class
        var objMethods = objClass.Methods;
        // Loop through the Methdos in the Class
        foreach(Method objMethod in objMethods)
        {
            // Loop through each Parameter in each method
            foreach(Parameter objParameter in objMethod.Parameters)
            {
                // If the Paramater is not prmitive we need to add this to the Imports
                if(!objParameter.Type.IsPrimitive){
                    ImportsOutput = objParameter.Name;
                }
            }
        }
        // Notice: As of now this will only return one import
        return  $"import {{ { ImportsOutput } }} from '../classes/{ImportsOutput}';";
    }
    // Format the method based on the return type
    string MethodFormat(Method objMethod)
    {
        if(objMethod.HttpMethod() == "get"){
            return  $"<{objMethod.Type.Name}>(_Url)";
        } 
        
        if(objMethod.HttpMethod() == "post"){
            return  $"(_Url, {objMethod.Parameters[0].name})";
        }
        if(objMethod.HttpMethod() == "put"){
            return  $"(_Url, {objMethod.Parameters[1].name})";
        }
        if(objMethod.HttpMethod() == "delete"){
            return  $"(_Url)";
        }
        
        return  $"";
    }
}
${
//The do not modify block below is intended for the outputed typescript files... }
//*************************DO NOT MODIFY**************************
//
//THESE FILES ARE AUTOGENERATED WITH TYPEWRITER AND ANY MODIFICATIONS MADE HERE WILL BE LOST
//PLEASE VISIT http://frhagn.github.io/Typewriter/ TO LEARN MORE ABOUT THIS VISUAL STUDIO EXTENSION
//
//*************************DO NOT MODIFY**************************
import { Injectable } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http'; 
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/map';
$Classes(*ApiController)[$ImportsList
@Injectable()
export class $ServiceName {
    constructor(private _httpClient: HttpClient) { }        
    $Methods[
    // $HttpMethod: $Url      
    $name($Parameters[$name: $Type][, ]): Observable<$ReturnType> {
        var _Url = `$Url`;
            return this._httpClient.$HttpMethod$MethodFormat
                .catch(this.handleError);
    }
    ]
    // Utility
    private handleError(error: HttpErrorResponse) {
        console.error(error);
        let customError: string = "";
        if (error.error) {
            customError = error.status === 400 ? error.error : error.statusText
        }
        return Observable.throw(customError || 'Server error');
    }
}]

 

image

When we save the template, it will create service file with the following code:

 

//*************************DO NOT MODIFY**************************
//
//THESE FILES ARE AUTOGENERATED WITH TYPEWRITER AND ANY MODIFICATIONS MADE HERE WILL BE LOST
//PLEASE VISIT http://frhagn.github.io/Typewriter/ TO LEARN MORE ABOUT THIS VISUAL STUDIO EXTENSION
//
//*************************DO NOT MODIFY**************************
import { Injectable } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http'; 
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/map';
import { CarDTO } from '../classes/CarDTO';
@Injectable()
export class CarService {
    constructor(private _httpClient: HttpClient) { }        
    
    // get: api/CarApi/getCars      
    getCars(): Observable<CarDTO[]> {
        var _Url = `api/CarApi/getCars`;
            return this._httpClient.get<CarDTO[]>(_Url)
                .catch(this.handleError);
    }
    
    // put: api/CarApi/${id}      
    put(id: number, carDTO: CarDTO): Observable<CarDTO> {
        var _Url = `api/CarApi/${id}`;
            return this._httpClient.put(_Url, carDTO)
                .catch(this.handleError);
    }
    
    // post: api/CarApi      
    post(carDTO: CarDTO): Observable<CarDTO> {
        var _Url = `api/CarApi`;
            return this._httpClient.post(_Url, carDTO)
                .catch(this.handleError);
    }
    
    // delete: api/CarApi/${id}      
    delete(id: number): Observable<void> {
        var _Url = `api/CarApi/${id}`;
            return this._httpClient.delete(_Url)
                .catch(this.handleError);
    }
    
    // Utility
    private handleError(error: HttpErrorResponse) {
        console.error(error);
        let customError: string = "";
        if (error.error) {
            customError = error.status === 400 ? error.error : error.statusText
        }
        return Observable.throw(customError || 'Server error');
    }
}

 

Complete The Application

image

To demonstrate how the code is consumed, we will create a car folder and add the following code:

car.component.html

 

<h1>Car Sample</h1>
<p *ngIf="!cars"><em>Loading...</em></p>
<table class='table' *ngIf="cars">
    <thead>
        <tr>
            <th> </th>
            <th>Car Name</th>
            <th>Car Active</th>
            <th>Last Update</th>
        </tr>
    </thead>
    <tbody>
        <tr *ngFor="let car of cars">
            <td width="20%">
                <button type="button" (click)="updateCar(car)">Update</button>
                <button type="button" (click)="deleteCar(car)">Delete</button>
            </td>
            <td width="30%"><input type="text" [(ngModel)]="car.carName" name="carName"></td>
            <td width="20%">
                <select [(ngModel)]="car.carActive">
                    <option *ngFor="let stat of status" value={{stat}}>
                        {{stat}}
                    </option>
                </select>
            </td>
            <td width="30%">{{ car.lastUpdate }}</td>
        </tr>
    </tbody>
</table>
<br />
<h4>Add Car:</h4>
<form [formGroup]="carForm" (ngSubmit)="AddCar($event)"
      style="background-color:cornsilk; padding: 12px 20px; width:300px; border:double 1px;">
    <span>Car Name: </span>
    <input formControlName="carName" size="20" type="text">
    <br /><br />
    <span>Car Active: </span>
    <select formControlName="carActive">
        <option *ngFor="let stat of status" value={{stat}}>
            {{stat}}
        </option>
    </select>
    <br /><br />
    <button type="submit">Add Car</button>
</form>

 

car.component.ts

 

import { Component, OnInit } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';
import { CarService } from '../services/Car.service';
import { CarDTO } from '../classes/CarDTO';
@Component({
    selector: 'car',
    templateUrl: './car.component.html'
})
export class CarComponent {
    public cars: CarDTO[] = [];
    public errorMessage: string;
    public status = [
        true,
        false
    ];
    public carForm = this.fb.group({
        carName: ["", Validators.required],
        carActive: ["", Validators.required]
    });
    constructor(
        private _CarService: CarService,
        public fb: FormBuilder) { }
    ngOnInit() {
        this._CarService.getCars().subscribe(
            cars => {
                this.cars = cars;
            },
            error => {
                this.errorMessage = <any>error;
                alert(this.errorMessage);
            });
    }
    AddCar() {
        // Get the form values
        let formData = this.carForm.value;
        let paramCarName = formData.carName;
        let paramCarActive = formData.carActive;
        let NewCar: CarDTO = {
            id: (this.cars.length + 1),
            carName: paramCarName,
            carActive: paramCarActive,
            lastUpdate: ""
        };
        this._CarService.post(NewCar).subscribe(
            returnedCar => {
                // Add the Car to the grid
                this.cars.push(returnedCar);
            },
            error => {
                this.errorMessage = <any>error;
                alert(this.errorMessage);
            });
    }
    updateCar(car: CarDTO) {
        this._CarService.put(car.id,car).subscribe(
            returnedCar => {
                // Get the index of the Car in the grid
                var intCarIndex: number =
                    this.cars.findIndex(x => x.id == returnedCar.id);
                // Update the Car
                if (intCarIndex > -1) {
                    this.cars[intCarIndex].carName = returnedCar.carName;
                    this.cars[intCarIndex].carActive = returnedCar.carActive;
                    this.cars[intCarIndex].lastUpdate = returnedCar.lastUpdate;
                }
            },
            error => {
                this.errorMessage = <any>error;
                alert(this.errorMessage);
            });
    }
    deleteCar(car: CarDTO) {
        this._CarService.delete(car.id).subscribe(
            () => {
                // Get the index of the Car in the grid
                var intCarIndex: number =
                    this.cars.findIndex(x => x.id == car.id);
                // Remove the Car from the grid
                this.cars.splice(intCarIndex,1);
            },
            error => {
                this.errorMessage = <any>error;
                alert(this.errorMessage);
            });
    }
}

 

When we run the application it allows us to perform Create, Read, Update, and Delete functionality.

 

Links

Typewriter

Visual Studio 2017

TypeScript

Keeping your C# and TypeScript models in sync (Angular 4)

Using Typewriter to Strongly-Type Your Client-Side Models and Services (Knockout)

TypeWriter, TypeScript, WebAPI! Oh my!  (AngularJs)

TypeScript codegeneration with Typewriter  (AngularJs)

Download

The project is available at http://lightswitchhelpwebsite.com/Downloads.aspx

You must have Visual Studio 2017 (or higher) installed to run the code.


Your name:
Gravatar Preview
Your email:
(Optional) Email used only to show Gravatar.
Your website:
Title:
Comment:
Security Code
CAPTCHA image
Enter the code shown above in the box below
Add Comment   Cancel 
Microsoft Visual Studio is a registered trademark of Microsoft Corporation / LightSwitch is a registered trademark of Microsoft Corporation