Dzisiaj pora na kolejny post dotyczący wykorzystania SignalR w ASP.NET Core. Ostatnim razem pisałem o implementacji po stronie serwera, tym razem zajmiemy się klientem aplikacji z wykorzystaniem frameworka Angular. Poniżej zamieszam odnośniki do powiązanych postów.
Część pierwsza: SignalR w ASP.NET Core – instalacja i uruchomienie
Część druga: SignalR w ASP.NET Core i Angular – przykładowa implementacja
Połączenie z serwerem
Gdy pisałem o konfiguracji, podałem na przykładzie w jaki sposób możemy połączyć się z serwerem. W tym celu wykorzystałem tzw. resolver w Angularze. Działa on na takiej zasadzie, że zapytanie wykonane w nim wykonuje się przed załadowaniem naszego komponentu, czyli np. planszy do gry. Poniżej przedstawię trochę inny przykład, gdzie ręcznie wywołujemy funkcję odpowiedzialną za połączenie. Na początek dodajemy fabrykę, która potrafi utworzyć nam nowe połączenie.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
//Connection factory import { SignalR, ISignalRConnection } from '../../lib/ng2-signalr'; import { Injectable } from '@angular/core'; @Injectable() export class SignalRConnectionFactory { constructor(private _signalR: SignalR) { } createConnection(): Promise<ISignalRConnection> { return this._signalR.connect(); } } |
Fabrykę wykorzystujemy tylko do nawiązania nowego połączenia. Jeżeli będę chciał wykorzystywać inne konfigurację niż standardowa, nie chcę dodawać tego kodu do klasy odpowiedzialnej za samo przetwarzanie komunikatów.
Dwustronna komunikacja
Następnie dodaję serwis, który będzie przetrzymywał otwarte połączenie z serwerem oraz rozsyłał komunikaty do komponentów w Angularze. Cały klient będzie miał tylko jedno otwarte połączenie, ponieważ otwarcie takiego połączenia jest czasochłonne. Dodatkowo nie ma sensu obciążać serwera wieloma połączeniami w ramach jednej aplikacji klienckiej. Poniżej przedstawiam taką klasę, która obsługuje dwa typy zdarzeń:
- Połączenie klienta do serwera, inicjowane prze klienta
- Zmiana drużyny która wykonuje ruch, inicjowane prze serwer
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
//Game hub import { Injectable } from '@angular/core'; import { Observable } from 'rxjs/Observable'; import { SignalRConnectionFactory } from '../../core/signalr/signalr-connection.factory'; import { ISignalRConnection, BroadcastEventListener } from '../../lib/ng2-signalr'; import { TeamChanged } from "../game.models"; @Injectable() export class GameHubService { private connection: ISignalRConnection; private onSubscribed$: BroadcastEventListener<string>; private onTeamChanged$: BroadcastEventListener<TeamChanged>; constructor(private connectionFactory: SignalRConnectionFactory) { } connect() { return Observable .fromPromise(this.connectionFactory .createConnection()) .map((connection) => { this.connection = connection; return true; }); } public subscribeToGame(gameId: number) : Promise<string> { return this.connection .invoke("Subscribe", gameId); } public teamChanged(onTeamChanged: (args: TeamChanged) => void) { if (!this.onTeamChanged$) this.onTeamChanged$ = this.connection.listenFor('TeamChanged'); this.onTeamChanged$.subscribe(onTeamChanged); } public subscribed(onSubscribed: (message: string) => void) { if (!this.onSubscribed$) this.onSubscribed$ = this.connection.listenFor('Subscribed'); this.onSubscribed$.subscribe(onSubscribed); } disconnect() { if (this.onSubscribed$) this.onSubscribed$.unsubscribe(); } } |
Posiada ona kilka funkcji, omówię krótko każdą z nich.
connect
– odpowiada za nawiązanie połączenia w ramach klienta. Połączenie jest przechowywanie w hubie i dzielone pomiędzy wszystkie komponenty.disconnect
– analogicznie do funkcji powyżej, zamyka ono połączenie z serwerem.subscribeToGame
– zdarzenie inicjowane po stronie klienta. Za jej pomocą dołączamy do kanału obecnej gry, dzięki temu otrzymamy komunikaty powiązane z grą.subscribed
– zdarzenie inicjowane po tym, jak nowy klient zapisze się do naszej gry.teamChanged
– zdarzenie inicjowane po tym, gdy jedna z drużyn wykona ruch, który spowoduje zmianę kolejki na inną drużynę.
Wykorzystanie serwisu w aplikacji
Gdy mamy już gotowy serwis, możemy wykorzystać go w naszej aplikacji. Na początek w głównym komponencie gry wywołujemy połączenie z serwerem.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
//Main game component. private connectToGame(gameId: number) { this.gameHub.connect() .mergeMap(connected => { if (connected) { return this.gameHub .subscribeToGame(gameId) } Observable.throw("Could not connect to game hub"); }) .mergeMap(connectionId => { return this.gameService .getGame(gameId); }) .subscribe(result => { this.game = result; this.board.initialize(this.game); this.sideNav.initialize(this.game); }); } |
Dzięki jej wywołaniu kolejno:
- nawiązujemy połączenie z serwerem
- zapisujemy się do gry
- ładujemy dane gry
- inicjalizujemy komponenty
Przykładem komponentu wykorzystującego ten serwis może być menu boczne aplikacji. Z jego poziomu drużyna może pominąć kolejkę, co będzie skutkowało zmianą obecnie aktywnej drużyny. Dodatkowo, gdy nastąpi zmiana drużyny, będziemy mogli nasłuchiwać na takie zdarzenie i wyświetlić drużynę wykonującą obecnie ruch.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
//Sidenav component import { Component, Input } from '@angular/core'; import { GameHubService, GameService } from '../services/game-services.imports'; import { Game, TeamChanged } from '../game.models'; @Component({ selector: 'game-sidenav', templateUrl: './game-sidenav.component.html', styleUrls: ['./game-sidenav.component.scss'] }) export class GameSidenavComponent { game: Game; constructor(private gameHub: GameHubService, private gameService: GameService) { } private skipRound() { this.gameService .skipRound(this.game.id); } onTeamChanged(event: TeamChanged) { console.log(`Team changed. New team: ${event.newTeamId}. Previous team: ${event.lastTeamId}.`); } initialize(game: Game) { this.game = game; this.gameHub.teamChanged(this.onTeamChanged); } } |
Mamy tutaj trzy elementy na które należy zwrócić uwagę:
initialize
– tutaj komponent zgłasza chęć otrzymywania komunikatów związanych ze zmianą aktywnej drużyny.teamChanged
przyjmuje funkcję, która wykona się za każdym razem, gdy takie zdarzenie nastąpi.onTeamChanged
– wspomniana wyżej funkcja, która wykona się po wystąpieniu zdarzenia zmiany drużyny. Tutaj wyświetlony zostanie tylko komunikat w konsoli.skipRound
– pominięcie tury. Nie odwołujemy się tutaj bezpośrednio do SignalR’a, ale wykonujemy zwykłe zapytanie do serwera. On później roześle informacje o zmianie aktywnej drużyny
Podsumowanie
Wydaje mi się, że w tych trzech wpisach pokazałem, jak wykorzystać w naszej aplikacji bibliotekę SignalR i tworzyć aplikacje działające w czasie rzeczywistym. Zachęcam do wypróbowania jej we własnym zakresie. Jeżeli jednak miałbym być szczery, to na chwilę obecną proponuję wykorzystanie tej biblioteki w starszej wersji frameworka, chyba że tak jak ja chcecie po prostu pobawić się z nowszą wersją frameworka .NET. Co prawda, pojawił się już ASP.NET Core Preview 1, ale nie jest to wersja stabilna. Taka pojawi się dopiero pod koniec obecnego roku.