Interakcje2 W poprzednim wpisie omówiliśmy szablon, a teraz zaczniemy go przerabiać dodając nową usługę REST API w ASP.NET CORE i nowe widoki w Angular. Zanim jednak do tego przejdziemy musimy omówić pewną pułapkę związaną z przechowywaniem plików statycznych w cache. Powiemy też o konfiguracji w ASP.NET CORE.

O co chodzi?

Plik statyczny HTML w wwwroot

Jak pamiętasz utworzyliśmy prosty plik HTML w folderze wwwroot. Uruchamiając serwer pod adresem : https://localhost:44382/hello.html zobaczysz następujący widok

Obsługa tego pliku statycznego HTML

Teraz co się stanie, gdy zmienimy ten plik HTML.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title></title>
</head>
<body>
    JESTEM PRZED MVC I SPA
    <p>To jest test app.UseStaticFiles();</p>
    <p>A co z Cache?</p>
</body>
</html>

Tutaj możesz zobaczyć różne rezultat. Wszystko zależy, od jakiej przeglądarki korzystasz i jakie ona ma ustawienia statycznego cachowania plików oraz tego, czy Visual Studio w tle nie przebudowuje plików.

Na chwilę obecnego żadnego cache nie zadeklarowaliśmy w kodzie w C# więc każda zmiana kodu w statyczny pliku powinna być widoczna. Powiem Ci jednak, że programując w ASP.NET CORE miałem za dużo przygód z cachowaniem statycznych plików i miałem czasem wrażenie, że te zachowanie jest losowe.

Później jednak się okazało, że jeden z middleware robił Cache bez mojej wiedzy i potem się dziwiłem,że dopiero po twardy odświeżeniu strony widzę swoje zmiany.

Caching statycznych plików jest czymś wspaniałym dla serwerów produkcyjnych. Jednakże nie chcesz tego mieć, gdy budujesz aplikację. Nie chcesz marnować 2-3 minut nad myśleniem czy nowy plik styli CSS jest już załadowany, czy w cache siedzi stary. Jest to równie ważne, gdy mówimy tutaj o Angularze, a on przecież operuje na statycznych plikach.

Właśnie, a jak to jest z Angularem? Z każdym razem, gdy wprowadzisz zmianę do plików .ts serwer w pamięci AngularCliServer automatycznie przebuduje pliki za Ciebie. Sprawa jednak nadal się komplikuje z innymi statycznymi plikami jak HTML, favicons, obrazkami , stylami CSS

Dlatego warto to ustawić by mieć pewność, że mamy pełną kontrolę nad tym zachowaniem

W czasach ASP.NET 4 wyłączenie cache w pliku web.config wygląda tak :

<caching enabled="false" />
<staticContent>
    <clientCache cacheControlMode="DisableCache" />
</staticContent>
<httpProtocol>
    <customHeaders>
    <add name="Cache-Control" value="no-cache, no-store" />
    <add name="Pragma" value="no-cache" />
    <add name="Expires" value="-1" />
    </customHeaders>
</httpProtocol>

Później dzieliłeś plik na web.config i na web.debug.config.

To były jednak stare czasy. Jak kto zrobić ASP.NET CORE.  Mamy dwa pliki konfiguracyjne i w zależności od środowiska inny plik się załaduje. Nie chcemy umieszczać statycznej konfiguracji wewnątrz kodu.

appsettings.json w Visual Studio

Najpierw dodajmy parametry do pliku appsettings.json

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "StaticFiles": {
    "Headers": {
      "CacheControl": "max-age=3600",
      "Pragma": "cache",
      "Expires": null
    }
  }
}

Na produkcji warto ustawić cache. Natomiast na środowisku pracy chcemy włączyć cache całkowicie więc dodajemy takie parametry do pliku appsettings.Development.json

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "StaticFiles": {
    "Headers": {
      "CacheControl": "no-cache, no-store",
      "Pragma": "no-cache",
      "Expires": "-1"
    }
  }
}

Później w pliku Startup.cs w metodzie Configure i deklaracji UseStaticFile ustawiamy cache w taki sposób.  

Teraz przy każdym zapytaniu o pliku statycznym przeglądarki otrzymają odpowiednie nagłówki z informacją jak mają przechowywać pliki.

app.UseStaticFiles(new StaticFileOptions()
{
    OnPrepareResponse = (context) =>
    {
        // Retrieve cache configuration from appsettings.json
        context.Context.Response.Headers["Cache-Control"] =
        Configuration["StaticFiles:Headers:Cache-Control"];
        context.Context.Response.Headers["Pragma"] =
        Configuration["StaticFiles:Headers:Pragma"];
        context.Context.Response.Headers["Expires"] =
        Configuration["StaticFiles:Headers:Expires"];
    }
});

Co możemy zrobić tutaj lepiej.  Za każdym razem, gdy chcemy wyjmować parametry konfiguracyjne korzystamy z obiektu IConfiguration i wpisujemy skomplikowany napis adresujący się do specyficznej wartości. 

Jeśli piszesz prostą aplikację to moim zdaniem tyle wystarczy. W prawdziwych aplikacjach biznesowych na podstawie moich doświadczeń w firmie ubezpieczeniowej mogę Ci powiedzieć, że taki parametrów konfiguracyjnych może być co najmniej 300.

Możesz sobie wyobrazić bałagan, który może powstać przy takiej ilości parametrów. Jak więc można to napisać lepiej. Przydałby by się tutaj klasy reprezentujące tą konfigurację.

Są na to, aż 3 sposoby. Można skorzystać z interfejsu IOptions<T>. Można stworzyć swoją własną klasę i uniknąć IOptions<T>. Możesz też stworzyć singleton w trakcie konfiguracji wstrzykiwania zależności. 

Rozwiązanie ma być jednak przejrzyste i przyjęło się, że rozwiązanie IOptions jest najbardziej popularne.

Do projektu dodałem następującą klasę. Ma ona trzy właściwości : 

public class StaticFilesConfiguration
{
    public string CacheControl { get; set; }

    public string Pragma { get; set; }

    public string Expires { get; set; }
}

Teraz w metodzie ConfigureServices  deklaruje mapowanie tej klasy na odpowiednią sekcję w konfiguracji. 

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<StaticFilesConfiguration>
(Configuration.GetSection("StaticFiles:Headers"));

Teraz będę tej konfiguracji potrzebował w metodzie Configure. Dodaje więc typ generyczny IOptions jako parametry do metody Configure. ASP.NET CORE za mnie wstrzyknie definicję z mapowanej klasy. 

public void Configure(IApplicationBuilder app, IWebHostEnvironment env,
    IOptions<StaticFilesConfiguration> options)
{

Teraz deklaruje użyje tej klasy w ustawieniach middleware UseStaticFiles.

app.UseStaticFiles(new StaticFileOptions()
{
    OnPrepareResponse = (context) =>
    {
        StaticFilesConfiguration headers = options.Value;

        context.Context.Response.Headers["Cache-Control"] =
        headers.CacheControl;
        context.Context.Response.Headers["Pragma"] =
        headers.Pragma;
        context.Context.Response.Headers["Expires"] =
        headers.Expires;
    }
});

Jestem na środowisku pracowniczym więc otrzymam parametry z pliku appsetting.Development.json

Cache Control rezultat działanie kodu ASP.NET CORE

Teraz możesz przetestować jak cache działa. Możesz wymusić inne środowisko pracy taki kodem.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env,
IOptions<StaticFilesConfiguration> options)
{
    env.EnvironmentName = "Production";

Sam parametr możesz także zmienić w ustawieniach projektu.

Zmienna środowiskowa w ASP.NET CORE

Gdy będziesz publikować aplikację na produkcję ten parametr automatycznie się zmieni.

Bierzemy się za Angulara

Zacznijmy od skasowania Compomentów Angulara, które nie będą nam potrzebne. Myślę, że to będzie dobry trening dla Ciebie.

Kasowanie folderów w Visual Studio

Kasujemy folder counter i folder fetch-data. Visual Studio, jeżeli nie ma czkawki to powinno Ci zasygnalizować błędy w TypeScript. W końcu odwołujemy się, do których plików już nie ma.

Jeżeli nie masz błędów nie panikuj. Visual Studio może mieć problem z komunikacją z elementami Angulara.

Błędy w Visual Studio po skasowaniu plików

Wszystkie te błędy znajdują się w pliku app.module.ts. Tam są referencję do wszystkich plików TypeScript używanych przez Angulara. Błędy powinny być podkreślone na czerwono.

błąd w Visual Studio nie ma plików TypeScript

Usuwamy więc te linijki kodu. Najpierw z sekcji import usuwamy deklarację tych Komponentów. Później usuwamy deklarację w @NgModule. Usuwamy także z RouterMoudle ścieżki do tych Componentów.

Oto kod przed:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { RouterModule } from '@angular/router';

import { AppComponent } from './app.component';
import { NavMenuComponent } from './nav-menu/nav-menu.component';
import { HomeComponent } from './home/home.component';
import { CounterComponent } from './counter/counter.component';
import { FetchDataComponent } from './fetch-data/fetch-data.component';

@NgModule({
  declarations: [
    AppComponent,
    NavMenuComponent,
    HomeComponent,
    CounterComponent,
    FetchDataComponent
  ],
  imports: [
    BrowserModule.withServerTransition({ appId: 'ng-cli-universal' }),
    HttpClientModule,
    FormsModule,
    RouterModule.forRoot([
      { path: '', component: HomeComponent, pathMatch: 'full' },
      { path: 'counter', component: CounterComponent },
      { path: 'fetch-data', component: FetchDataComponent },
    ])
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

i po:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { RouterModule } from '@angular/router';

import { AppComponent } from './app.component';
import { NavMenuComponent } from './nav-menu/nav-menu.component';
import { HomeComponent } from './home/home.component';

@NgModule({
  declarations: [
    AppComponent,
    NavMenuComponent,
    HomeComponent,
  ],
  imports: [
    BrowserModule.withServerTransition({ appId: 'ng-cli-universal' }),
    HttpClientModule,
    FormsModule,
    RouterModule.forRoot([
      { path: '', component: HomeComponent, pathMatch: 'full' },
    ])
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Przy okazji takie czyszczenie może Cię nauczyć jak dodawać nowe Komponenty do Angulara.

Co robi kod w AppModule

Moduły Angulara zwane także NgModules zostały przestawione w Angularze 2 RC5 i są one genialny sposobem na organizację aplikacji. Na pewno lepszym niż kontrolery w Angularze 1.X.X. 

Każdy moduł może być zbiorem : Components (komponentów), directives (dyrektyw) i innych modułów.

Musi istnieć przynajmniej jeden moduł i tym modułem głównym jest AppModule. 

Kod AppModule można rozbić na dwie części : Jedna to lista stwierdzeń import, która wskazuję na wszystkie referencję do plików TypeScript wymagane przez daną aplikację. 

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { RouterModule } from '@angular/router';

import { AppComponent } from './app.component';
import { NavMenuComponent } from './nav-menu/nav-menu.component';
import { HomeComponent } from './home/home.component';

Jak widzisz mamy tutaj definicję do bazowego modułu NgModule, mamy tutaj moduł do formularzy i mamy tutaj moduł do wysłania zapytań HTTP. Nie zapomnijmy o module służącym do obsługi podstron w Angularze.

Później są nasze komponenty.W drugiej części tego pliku w definicji @NgModule mamy tablice, która zawiera w sobie zbiór : 

  • components (komponentów)
  • directives, (dyrektyw)
  • pipes (nie mam pojęcia jak to przetłumaczyć)
  • modules, (modułów)
  • provider (prowajderów - czyli polishonenglishon)
  • i tak dalej

W parametrze bootstrap deklarujemy, od którego Komponentu ma się uruchomić aplikacja i jest to AppComponent.

@NgModule({
  declarations: [
    AppComponent,
    NavMenuComponent,
    HomeComponent,
    CounterComponent,
    FetchDataComponent
  ],
  imports: [
    BrowserModule.withServerTransition({ appId: 'ng-cli-universal' }),
    HttpClientModule,
    FormsModule,
    RouterModule.forRoot([
      { path: '', component: HomeComponent, pathMatch: 'full' },
      { path: 'counter', component: CounterComponent },
      { path: 'fetch-data', component: FetchDataComponent },
    ])
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Aktualizacja NavMenu

To nie koniec zmian. Naszych Componentów już nie ma, ale wciąż są do nich linki w pasku nawigacyjnym.

Błąd w Angular po skasowaniu komponentów

Idziemy więc teraz do pliku "/ClientApp/app/components/nav-menu/navmenu.component.html" gdzie znajduję się układ naszego paska nawigacyjnego.

<li class="nav-item" [routerLinkActive]="['link-active']">
  <a class="nav-link text-dark" [routerLink]="['/counter']"
    >Counter</a
  >
</li>
<li class="nav-item" [routerLinkActive]="['link-active']">
  <a class="nav-link text-dark" [routerLink]="['/fetch-data']"
    >Fetch data</a
  >
</li>

Znajdujemy elementy listy, które odwołują się do tych skasowanych komponentów i po skasowaniu mamy już jest poprawną listę hiperlinków.

Teraz zmienimy stronę tytułową naszej strony by było to coś więcej niż Hello World. Idziemy do pliku /ClientApp/src/app/components/home/home.component.html

<h1>Zobacz Losową książkę</h1>
<p>Nie wiesz co przeczytać!</p>
<p>Ja Ci wylosuję dobrą książkę</p>
<p>Odmienię twoje życie</p>

Zmieniamy HTML-a i zaraz po zapisie Angular powinien się odświeżyć w twojej przeglądarce.

Nowa strona tytułowa w Angular

Teraz po stronie back-end kasujemy plik w Controllers/WeatherForecast.cs

Kasujemy kontroler WeatherController

Jesteśmy teraz gotowi na stworzenie swojej własnej aplikacji. Teraz gdy mamy minimalną, ale działającą aplikację .NET CORE 3.2 i Angular 9 możemy ją odrobinie rozbudować.

Interakcja pomiędzy Back-end, a Front-end

Skupimy się na interakcji pomiędzy Back-end, a Front-Endem. Zobaczymy jak stworzyć obsługę zapytania HTTP do ASP.NET Core przy użyciu kontrolerów. Przykład będzie bardzo prosty byśmy się nie zgubili.

Najpierw muszę dodać nowy kontroler. Klikam więc na folder i z opcji wybieram Add -> Controller...

Dodanie nowego Controllera w Visual Studio. Meni pomocnicze

W ASP.NET CORE kontrolery mogą robić mnóstwo rzeczy dlatego w następnym oknie masz tyle opcji i szablonów. Możesz np. utworzyć kontroler już z gotowym widokiem .cshtml tylko nam to nie jest potrzebne.

Wybierz opcję MVC Controller - Empty 

Dodanie nowego Controller w Visual Studio

Każdy kontroler musi mieć w nazwie słowo Controller. Dlatego swój kontroler nazywam BookController. Jak się domyślisz chce zwrócić losową książkę dla użytkownika.

Nazywamy kontroler BookController

Wciskasz OK i masz następujący kod przed sobą.

public class BookController : Controller
{
    public IActionResult Index()
    {
        return View();
    }
}

Ten domyślny kod chce zwrócić widok, ale my nie pracujemy na widokach więc trzeba ten kod zmienić.

Kontroler w ASP.NET CORE chce znaleźć widok pasujący do danej akcji

Chcemy zwracać dane w formacie JSON. Będzie to notacja, która będzie zrozumiała dla Angulara. 

Chcemy też coś zrobić ze ścieżką do naszego kontrolera. Jak widzisz domyślnie ścieżka idzie tak : {nazwa kontrolera bez słowa controller}/{akcja}

Wynika to z tego ustawiania w klasie startup.cs

app.UseRouting();

app.UseEndpoints(endpoints =>
{
    endpoints.MapControllerRoute(
        name: "default",
        pattern: "{controller}/{action=Index}/{id?}");
});

Czy możemy nadpisać to ustawienie nie grzebiąc w tych domyślnych ustawieniach? No oczywiście, że tak. Przy pomocy atrybutów możemy powiedzieć, że dany kontroler powinien pod innym adresem.

[Route("api/[controller]")]
public class BookController : Controller

Możemy np. powiedzieć by ten kontroler pracował pod adrsem : api/{nazwa kontrolera bez słowa controller}/{akcja}

Ja jednak chce pozbyć się słowa book więc umieszczam taki kod:

[Route("api")]
public class BookController : Controller

Teraz jak zwrócić JSON-a w tym kontrolerze? Najpierw stworzę klasę reprezentującą książkę. Utworzyłem więc sobie folder "Models" a w nim dodałem klasę Book

Dodanie nowego elementu w Visual Studio

Klasa ta ma tylko dwa pola : Tytuł książki i jego autora.

public class Book
{
    public string Title { get; set; }
    public string Author { get; set; }
}

W kontrolerze zmieniam nazwę metody, dodaje instancje klasy book, oraz zwracam nowy typ akcji, jakim jest JsonResult.

[Route("api")]
public class BookController : Controller
{
    public IActionResult Random()
    {
        Book book = new Book();
        book.Author = "Bibeault Katz";
        book.Title = "jQuery in Action";

        return new JsonResult(book);
    }
}

Sprawdźmy teraz czy moje prymitywne REST API działa. Pod adresem api/Random powinien zostać zwrócony JSON. Możesz zauważyć https://localhost:44382/api   będzie działać, ale nie https://localhost:44382/api/random

Dlaczego? Po deklaracji atrybutu Route na samym początku kontrolera oczekuje on także podania nazwy dla każdej metody. Ten przykład akurat działa, ponieważ domyślnie Route bierze pierwszą metodę z kontrolera i zwraca wyniki.

Jednakże przy większej ilości metod zacznie on wrzucać wyjątek.

ASP.NET CORE nie wiem o co chodzi gdy są dwie metody w kontrolerze

Dlatego musimy podać ścieżkę też, dla której nasze metody zostaną obsłużone. Oto przykład jak ten problem rozwiązać. Stworzyłem metodę, która niby zwróci losową książkę oraz metodę, która zwróci wszystkie książki.

[Route("api")]
public class BookController : Controller
{
    [Route("")]
    public IActionResult Random()
    {
        Book book = new Book();
        book.Author = "Bibeault Katz";
        book.Title = "jQuery in Action";

        return new JsonResult(book);
    }
    [Route("all")]
    public IActionResult AllBooks()
    {
        List<Book> list = new List<Book>();
        Book book = new Book();
        book.Author = "Bibeault Katz";
        book.Title = "jQuery in Action";
        Book book2 = new Book();
        book2.Author = "Robin Sen";
        book2.Title = "Android w Akcji";
        list.Add(book); list.Add(book2);

        return new JsonResult(list);
    }
}

Sprawdźmy każdy adres tej metody.

REST API w ASP.NET CORE rezultat ścieżek

Mamy już gotowy przykład po stronie back-end. Chce ci jednak jeszcze pokazać inny sposób na stworzenie pod pewnym adresem metody, która zwróci JSON, a jest nim middleware.

Middleware, jako alternatywa?

Jak pamiętasz middleware to są te komponenty które deklarujemy w potoku w metodzie Configure w pliku. Startup.cs

app.UseEndpoints(endpoints =>
{
    endpoints.MapControllerRoute(
        name: "default",
        pattern: "{controller}/{action=Index}/{id?}");
});

app.UseSpa(spa =>
{
    // To learn more about options for serving an Angular SPA from ASP.NET Core,
    // see https://go.microsoft.com/fwlink/?linkid=864501

    spa.Options.SourcePath = "ClientApp";

    if (env.IsDevelopment())
    {
        spa.UseAngularCliServer(npmScript: "start");
    }
});

Jak widzisz ponownie one określają także jakie zachowanie ma być pod jaką ścieżką. Przykładowo tutaj mówie o obsłudze MVC, a zaraz potem obsługuję aplikacja SPA Angular.

Nic nie stoi na przeszkodzie abyś mógł napisać swój middleware i go uruchomić w tym potoku.

public class RequestBookMiddleware
{
    private readonly RequestDelegate _next;

    public RequestBookMiddleware(RequestDelegate next)
    {

        _next = next;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        if (context.Request.Path.StartsWithSegments(new PathString("/healthtest")))
        {
            Book book = new Book();
            book.Author = "Bibeault Katz";
            book.Title = "jQuery in Action";

            context.Response.StatusCode = 200;

            context.Response.ContentType = "application/json";

            string jsonString = JsonConvert.SerializeObject(book);

            await context.Response.WriteAsync(jsonString, Encoding.UTF8);

            // to stop futher pipeline execution
            return;
        }

        // Call the next delegate/middleware in the pipeline
        await _next.Invoke(context);

    }
}

Oto przykład middleware, który zwróci książkę pod adresem /healthtest. Jeśli będzie inna ścieżka to przesyłam zapytanie HTTP dalej do potoku. Pozostaje go dodać do potoku i masz alternatywny styl obsługi zapytań HTTP.

app.UseMiddleware(typeof(RequestBookMiddleware));

Tworzenie jednak swoich middleware zostawić na bardziej zaawansowane przykłady. Może chciałbyś stworzyć middleware, który modyfikuje odpowiednio zapytanie HTTP.

Zdecydowanie styl MVC , jeśli chodzi o budowanie API REST-owych, które zwraca JSON jest dużo lepsze. Przynajmniej teraz masz dodatkowy wgląd jak mechanizm potoków zapytań HTTP działa ASP.NET CORE z middleware i kontrolerami.

Odebrać zapytanie Angularem

Tyle, jeżeli chodzi o Back-end, a teraz jak te JSON złapać i obsłużyć w Angularze?

Najpierw musi stworzyć nowy Component, który wyświetli na ten widok. Jak pamiętasz będzie folder składający się z trzech plików.

Component : plik .ts napisany w TypeScript który będzie zawierał komponent klasy, jak i referencję do wszystkich moduli , funkcji i zmiennych, które będą tam występować.

Szablon albo Template : plik .html, który jest rozszerzeniem składni Angular Template Syntax. Definiuje on UI naszego komponentu.

Style : plik .css, który będzie zawierały style przeznaczone tylko do tego komponentu

Czy zawsze muszą być 3 pliki
  O ile podzielenie komponentu na 3 pliki wydaje się najbardziej praktyczne to wymaga się tylko 1 pliku Componentu, a w nim można zaszyć HTML i CSS. Wybór pomiędzy oddzielnymi plikami, a trzymania wszystkiego w jednym jest kwestią gustu. Ktoś zawsze może się kłócić, że pracuje mu się lepiej z jednym plikiem. Ja będę korzystał z 3. Dlaczego? Bo mam jasne oddzielenie odpowiedzialności. 

Jeżeli jesteś przystosowanych do wzorców model-view-controller MVC lub model-view-viewmodel MVVM to możesz powiedzieć, że Component Angulara to wzorzec Controller/viewmodel gdzie szablon HTML reprezentuje viewmodel


Tworzymy więc folder, a w nim następujące pliki.

Dodanie nowego folderu w Visual Studio

  • random-book.component.ts
  • random-book.component.html
  • random-book.component.css

Visual Studio oferuje szablony do każdego z tych plików więc nie widzę byś miał jakikolwiek problem:

Tworzenie plik TypeScript w Angular

Mamy foldery więc pora je uzupełnić

Utworzenie trzech plików dla komponentu random-book

random-book.component.ts

Oto kod źródłowy :

import { Component, Inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Component({
  selector: 'app-random-book',
  templateUrl: './random-book.component.html',
  styleUrls: ['./random-book.component.css']
})

export class RandomBookComponent {
  public result: Book;
  constructor(
    private http: HttpClient,
    @Inject('BASE_URL') private baseUrl: string) {
  }
  ngOnInit() {
    this.http.get<Book>(this.baseUrl + 'api').subscribe(result => {
      this.result = result;
    }, error => console.error(error));
  }
}


interface Book {
  name: string;
  author: string;
}

Co tutaj się dzieje :

Na początku pliku deklaruje import a tam mówie, z jakich directives, pipes, services i innych Components będę korzystał.

Konstruktorze mojego komponentu, tworzy instancje service HttpClient. W nim także w zmiennej baseUrl robię wstrzykiwanie zależności . BASE_URL znajduje się w pliku /ClientApp/src/main.ts. Lepiej mieć ustawienie w jednym miejscu i w każdym komponencie.  BASE_URL określa adres bazowy URL naszej aplikacji. To tego bazowego adresu dodam słowo "api" abyśmy pobrali książkę.

Na końcu kodu też widzisz definicje książki w TypeScript. Odpowiedź JSON zostanie zmieniona na obiekt JavaScript tego interfejsu. Serializacja wykonuje się automatycznie.

Zanim pójdziemy dalej musimy omówić następujące tematy:

  • Imports
  • Dependency Injection
  • ngOnInit
  • Konstruktor
  • HttpClient
  • Observables
  • Interfejsy

Importy modułów

Określenia import służą do pobrania innych modułów JavaScript. Koncepcja modułów pojawiła się w ECMAScript (JavaScript) 2015 roku i zostało przyjęte także przez TypeScript

Moduł Angulara więc jest bazowo kolekcją zmiennych, funkcji, klas zgrupowanych w innej klasie. Każdy moduł ma swoją własną przestrzeń  (scope) . Czyli elementy są widoczne, tylko wewnątrz swojego modułu. No, chyba że użyjesz słowa export to wtedy jasno mówisz, że dany element może być zaimportowany gdzieś indziej. 

Aby skorzystać z zmiennych, funkcji,klas, interfejsów w danym module to musisz użyć słowa import. 

Jest to jak przestrzeń nazw w C# przy użyciu słowa using.

Dodatkowo jak widzisz sam Angular jest taki modułem, który muszę importować. Łatwo je rozpoznać, ponieważ zaczynają się one od @. 

Moduły ECMAScript
  O co chodzi z tymi modułami? Chciałbyś wiedzieć więcej.

Więcej możesz przeczytać tutaj: 

O modułach w TypeScript : https://www.typescriptlang.org/docs/handbook/modules.html

O ich importowaniu : https://www.typescriptlang.org/docs/handbook/module-resolution.html


Uwaga
Moduły JavaScript nie powinny być mylone z systemem modułów w Angularze, który bazuje na dekoratorze @NgModule. Widzieliśmy jak w pliku app.module.ts dodajemy inne moduły do NgModules. Język JavaScript i Angular używają podobnego słownictwa (import zamiast imports), (export zamiast exports). 

System modułów w JavaScript a w Angular

Gdybyś chciał poczytać jak ten dekorator działa tutaj masz link :

https://angular.io/guide/ngmodules

A tutaj masz więcej informacji na temat architektury NgModule i architektury modułów w JavaScript

https://angular.io/guide/architecture-modules#ngmodules-and-javascript-modules


Wstrzykiwanie zależności, czyli Dependency injection

Mówiliśmy o wstrzykiwaniu zależności już wcześniej. Ta technika występuje w końcu i ASP.NET CORE, jak i w Angularze. 

Czym jest Dependency injection? Ta technika występuje w klasach. Klasy potrzebują usług czy obiektów do swojego działania. Aby mieć programowanie modułowe i oddzielone od siebie niestety nie możemy pisać takiego kodu:

public TestClass() {
    var object = new Element();
    object.doStuff();
}

Tworzenie obiektu w klasie samo sobie nie jest niczym zły, ale tworzy problem, gdy wiele klas tworzy tak zmienne, obiekty. Zmiana kodu w jednym miejscu zrobiłaby lawinę bałaganu. Działanie klasy TestClass jest powiązane z klasą Element zbyt bezpośrednio.

Teraz jak by to wyglądało w przypadku wstrzykiwania zależności. Zamiast tworzenia instancji elementu możemy go przesłać do konstruktora. 

public TestClass(Element el) {
    el.doStuff();
}

Inny mechanizm, inna klasa zajmie tym co tam dokładnie ma się znajdować. 

Dependency Injection
O ile jestem fanem dokładnego pisania jak coś działa tym razem wole odesłać Cię do oficjalnych poradników

https://docs.microsoft.com/pl-pl/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-3.1

https://angular.io/guide/dependency-injection


W Angularze mechanizm Dependency injection daje odpowiednie parametry do klasy w czasie jej tworzenia. W przykładzie z klasą RandomBookComponent użyliśmy tej techniki do wstrzyknięcia instancji usługi HttpClient i zmiennej baseUrl. 

Będziemy mieć do nich dostęp w całej klasie. Są one w polach private więc tylko w naszej klasie skorzystamy z tych instancji.  Podobnie jest w C#. Co jest prywatne jest prywatne.

ngOnInit

ngOnInit jest to metoda, która uruchomi się, gdy klasa RandomBookComponent jest tworzona. Każdy Component w Angularze ma swój cykl życia i do niego są także odpowiednie metody. Mamy np. metody, gdy HTML jest niszczony lub tworzony.

Można to przyrównać do zdarzeń w C#. Oto ich lista:

  • ngOnChanges() : Odpowiada, gdy Angular ustawia bądź resetuje dane powiązane z elementami widoku HTML.  W tej metodzie znajduje się obiekt SimpleChanges, który określa obecny i poprzedni stan danej właściwości. Wywołany przed ngOnInit i za każdym razem, gdy dana z powiązana elementem widoku HTML się zmienia
  • ngOnInit() : Uruchamia się  przy tworzeniu komponentu, gdy wykonało się pierwsze powiązanie danych z widokiem
  • ngDoCheck() : Uruchamia się przy zmianie poza Angularem. Możesz ją wywołać wewnątrz metody ngOnChanges() i ngOnInit() i po nich się ona uruchomi.
  • ngAfterContentInit() : Odpowiada, gdy zawartość zostanie wczytana do końca
  • ngAfterContentChecked() : Odpowiada po sprawdzeniu zawartości przez Angular.
  • ngAfterViewInit() : Odpowiada, gdy widok i wszystkie inne widoki po drodze zostaną załadowane
  • ngAfterViewChecked : Gdy Angular sprawdzi widok i inne widoki po drodze wtedy ta metoda się uruchomi
  • ngOnDestory() : Gdy Angular pozbywa się danego komponentu  wtedy uruchamia się ta metoda. Dzieje się to przy zmianie strony w SPA w Angularze

Te wszystkie zdarzenia mogą zostać użyte przez Ciebie.

Dlaczego korzystamy z HttpClient w zdarzeniu ngOnInit(),żeby to zrozumieć musi omówić konstruktor.

Konstruktor klasy

Wszystkie klasy w TypeScript mają swój konstruktor i jest to metoda constuctor(). Angular najpierw go wywoła,a  potem będziemy mogli przejść do cyklu zdarzeń Angular danego komponentu.

Konstruktorze, więc najpierw wykonany wstrzykiwanie zależności, a potem te obiekty będą już istnieć dla metody ngOnInit(). Dodatkowo warto zaznaczyć, że nie możesz wykonać wstrzykiwania zależności w cyklu zdarzeń. Angular nie ma obsługi takie funkcjonalności.

HttpClient

Musimy mieć możliwość wysłania i odbierania danych JSON do naszych kontrolerów w ASP.NET CORE.  HttpClient pojawił się po raz pierwszy w Angularze 4.3.0-RC0. Nowa implementacja HttpClient znana jako @angular/http zastąpiła starą funkcjonalność i tymczasowo była ona w innej paczce. My piszemy aplikację od zera wiec nie jest to nasze zmartwienie.

Stare API Angulara miała wiele ograniczeń:

  • JSON nie był domyślnym formatem. Trzeba było jawnie ustawiać nagłówki i parsować JSON-a.
  • Nie było łatwego dostępu do potoku zapytań i odpowiedzi HTTP. 
  • Nie było natywnego sposobu na przesyłanie obiektów jako zapytań HTTP. Otrzymywać obiekty bezpośrednio też nie mogłeś.  Chociaż mogłeś rzutować JSON-a na interfejs.

Dobra wiadomość jest taka, że nowe API potrafi to wszystko. Testowanie i łapanie błędów też jest dużo łatwiejsze. 

Psssyt
Warto też zaznaczyć, że umieszczanie HttpClient w komponentach to kiepski pomysł. Dużo kodu będzie się powtarzać i lepiej rozbić tą funkcję do swoje własnej usługi, którą będziesz wstrzykiwać. Na razie jednak nie zawracajmy sobie tym głowy

Observables

Observables to potężna funkcjonalność, która pomaga przy danych asynchronicznych. Jest to kręgosłup biblioteki RxJs (ReactiveX JavaScript). Jest ona wymaga w Angularze . Planuje się jej dodanie na stałe do ECMAScript 7. Są to lepsze obietnice w JavaScript.

Observables mogą być konfigurowane do wysłania wartości, wiadomości,zdarzeń, synchronicznie i asynchronicznie.  Te wartości możesz otrzymać poprzez subskrypcję do obiektu Observable. 

Spójrzmy na ten kod jeszcze raz:

 ngOnInit() {
    this.http.get<Book>(this.baseUrl + 'api').subscribe(result => {
      this.result = result;
    }, error => console.error(error));
  }

get<Book>() : pobierze książkę z API w ASP.NET CORE. Metoda subscribe() tworzy obiekt Observable, który może zrobić dwie akcję w zależności od tego, czy dane zapytanie zakończy się sukcesem, czy błędem. To wszystko dzieje się asynchronicznie i zostanie uruchomione w innym wątku. Reszta aplikacji będzie działać nadal. 

Interfejsy

Teraz gdy wiemy jak HttpClient działa pytanie brzmi po co nam są interfejsy.  Dlaczego nie -czysty JSON jako anonimowy obiekt JavaScript?  Odpowiedź jest prosta, bo to zły pomysł:

Używamy TypeScript-a, ponieważ chcemy korzystać z definicji obiektów, a nie z anonimowych obiektów JavaScript. Wiemy z czego ma się składać ksiażka

Anonimowe obiekty ciężko sprawdzić. Jak masz sprawdzić jakiej właściwości brakuje w obiekcie, gdy nie masz jego szkieletu.

Anonimowe obiekty nie mogą być użyte ponownie

W prawdziwej aplikacji biznesowej nie moglibyśmy sobie pozwolić na utratę kontroli nad typem, jakim operujemy. Interfejsy więc górą

random-book.component.html

Teraz pora na obsługę widoku.

<h1>Losowa książka</h1>
<p>Oto rezultat:</p>

<p *ngIf="!result"><em>Loading...</em></p>

<div *ngIf="result">
  <div>Tytuł : {{ result.name }}</div>
  <div>Autor : {{ result.author}}</div>
</div>

ngIf jest strukturalną dyrektywą, która mówi czy stworzyć dany element HTML w zależności od warunku logicznego. Może być łączony z wyrażeniem else, jeśli if istnieje. Na początku nasz obiekt książki będzie pusty. Bycie undefined w JavaScript oznacza wartość false dla tego ngIf pocztkowo nie wyświetli naszych pól.

Dopiero po pobraniu pokażemy tytuł oraz nazwę autora.

{{result.name}} , {{result.author}} : to interpolacje, które wyświetlą zawartość naszych zmiennych. 

Wyświetlanie danych?
Chciałbyś dowiedzieć się więcej na temat ciekawych dyrektyw po stronie HTML. Zawsze możesz spojrzeć na oficjalną dokumentacje

Wyświetlanie danych : https://angular.io/guide/displaying-data

Składnia szablonu HTML : https://angular.io/guide/template-syntax

Strukturalne dyrektywy : https://angular.io/guide/structural-directives


Na razie nie dodaje nowych styli CSS więc zmiany tutaj się kończą. Pora na dodanie komponentu do Angulara. Trzeba dodać go w plikach :

  • app.module.ts
  • nav-menu.component.html

Do AppModule dodajemy import z naszym nowym komponentem. Później dodajemy go do deklaracji w @NgModule, a na koniec wybieramy dla niego adres nawigacyjny "/random".

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { RouterModule } from '@angular/router';

import { AppComponent } from './app.component';
import { NavMenuComponent } from './nav-menu/nav-menu.component';
import { HomeComponent } from './home/home.component';
import { RandomBookComponent } from './random-book/random-book.component';

@NgModule({
  declarations: [
    AppComponent,
    NavMenuComponent,
    HomeComponent,
    RandomBookComponent
  ],
  imports: [
    BrowserModule.withServerTransition({ appId: 'ng-cli-universal' }),
    HttpClientModule,
    FormsModule,
    RouterModule.forRoot([
      { path: '', component: HomeComponent, pathMatch: 'full' },
      { path: 'random', component: RandomBookComponent },
    ])
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Pozostało także dodać nowy link do naszego komponentu w nav-menu.component.html

<div class="navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse"
     [ngClass]="{ show: isExpanded }">
  <ul class="navbar-nav flex-grow">
    <li class="nav-item"
        [routerLinkActive]="['link-active']"
        [routerLinkActiveOptions]="{ exact: true }">
      <a class="nav-link text-dark" [routerLink]="['/']">Home</a>
    </li>
    <li class="nav-item" [routerLinkActive]="['link-active']">
      <a class="nav-link text-dark" [routerLink]="['/random']">Losuj</a>
    </li>
  </ul>
</div>

Gdy przetestujesz aplikacje powinieneś zobaczyć następujący widok.

Rezultat działania widoku losowej książki w Angular

W tym wpisie spędziliśmy trochę czasu z konfiguracją i budową kontrolerów Web API w ASP.NET CORE. Pokazałem także jak stworzyć własny middleware, który może także sterować zapytaniami HTTP.

Mając gotowe API skorzystaliśmy z Angulara by wyświetlić losową książkę. Oto jak wygląda komunikacja pomiędzy Angularem, ASP.NET CORE-m.

Analogicznie możesz szybko dodać kolejny widok, który pokaże wszystkie książki. Tworzysz kolejny folder, a w nim trzy pliki

Dodanie folderu do Visual Studio : show-books

Tworzysz następujący kod TypeScript.

import { Component, Inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Component({
  selector: 'app-show-books',
  templateUrl: './show-books.component.html',
  styleUrls: ['./show-books.component.css']
})

export class ShowBooksComponent {
  public result: Book[];
  constructor(
    private http: HttpClient,
    @Inject('BASE_URL') private baseUrl: string) {
  }
  ngOnInit() {
    this.http.get<Book[]>(this.baseUrl + 'api/all').subscribe(result => {
      this.result = result;
    }, error => console.error(error));
  }
}

interface Book {
  name: string;
  author: string;
}

Tym razem rezultatem zapytania do API będzie kolekcja książek. Jak widzisz kod za bardzo się nie zmienił. W widoku HTML stworzę tabelkę i skorzystam z dyrektywy ngFor, która będzie tworzyć elementy według pętli.

<h1>Wszystkie książki</h1>
<p>Oto rezultat:</p>

<p *ngIf="!result"><em>Loading...</em></p>

<table class='table table-striped' aria-labelledby="tableLabel"
       *ngIf="result">
  <thead>
    <tr>
      <th>Tytuł</th>
      <th>Author</th>
    </tr>
  </thead>
  <tbody>
    <tr *ngFor="let book of result">
      <td>{{ book.title }}</td>
      <td>{{ book.author }}</td>
    </tr>
  </tbody>
</table>

W AppModule dodaje kolejny komponent do obsługi.

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { RouterModule } from '@angular/router';

import { AppComponent } from './app.component';
import { NavMenuComponent } from './nav-menu/nav-menu.component';
import { HomeComponent } from './home/home.component';
import { RandomBookComponent } from './random-book/random-book.component';
import { ShowBooksComponent } from './show-books/show-books.component';

@NgModule({
  declarations: [
    AppComponent,
    NavMenuComponent,
    HomeComponent,
    RandomBookComponent,
    ShowBooksComponent
  ],
  imports: [
    BrowserModule.withServerTransition({ appId: 'ng-cli-universal' }),
    HttpClientModule,
    FormsModule,
    RouterModule.forRoot([
      { path: '', component: HomeComponent, pathMatch: 'full' },
      { path: 'random', component: RandomBookComponent },
      { path: 'all', component: ShowBooksComponent },
    ])
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Pozostało dodać mi jeszcze klawisz nawigacyjny do strony ze wszystkim książkami. 

<ul class="navbar-nav flex-grow">
  <li class="nav-item"
      [routerLinkActive]="['link-active']"
      [routerLinkActiveOptions]="{ exact: true }">
    <a class="nav-link text-dark" [routerLink]="['/']">Home</a>
  </li>
  <li class="nav-item" [routerLinkActive]="['link-active']">
    <a class="nav-link text-dark" [routerLink]="['/all']">Wszystkie</a>
  </li>
  <li class="nav-item" [routerLinkActive]="['link-active']">
    <a class="nav-link text-dark" [routerLink]="['/random']">Losuj</a>
  </li>
</ul>

i... Mamy przed sobą ładną tabelkę danych.

Wszystkie książki rezultat Angulara czytającego api w ASP.NET CORE

W następnym wpisie będziemy tworzyć formularze w Angularze. Zastanawiam się też czy w ASP.NET CORE nie zrobić logiki baz danych stosując Entity Framework.