Nov
18
Written by:
Michael Washington
11/18/2017 6:14 PM
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.
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.
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
You can download and Install Typewriter at the following link.
It will install as a plug-in in Visual Studio.
Creating Classes
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; }
}
}
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…
We select TypeScript Template File, name the file Classes.tst, and click Add.
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:
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
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
}
}
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');
}
}]
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
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.