ASP.NET Core, Angular 2, SignalR для чайників

Всім привіт! Хочу поділитися своїм досвідом використання ASP.Net Core і Angular 2 з використанням SignalR.

Будучи програмістом 1С, часто доводиться вирішувати завдання, які на 1С вирішити складно або неможливо. Дуже допомагає знання .Net. Але ось, що стосується клієнтської частини сайтів, то тут багато тонкощів (JavaScript, CSS, JQuery ітд), які швидко забуваються, якщо ними не користуватися.

Angular 2 дозволяє значно спростити створення клієнтської частини. Так TypeScript значно ближче до C# (і головне дозволяє використовувати Руслиш), а з шаблонами нескладно розібратися знаючи Razor і Xaml.

Головне, що ви працюєте з даними, за аналогією з WPF. При цьому є купа контролів.

Хочу поділитися з такими ж бідолахами як я, або хто тільки починає вивчення Angular 2, ASP.Net Core, так як витратив багато часу на пошуки матеріалів для вивчення.

Для тренування на кішках був обраний мій проект 1C Messenger для відправки повідомлень, файлів і обміну даними між користувачами 1С, веб сторінки, мобільними додатками а ля Skype, WhatsApp. Исходники Тут

Поки не вийшов. Net Core 1.2 і NetStandard 2, зараз немає підтримки клієнта для SignalR під .Net Core

Отже, почнемо. Для роботи нам знадобиться:

1. ASP.NET Core + Angular 2 шаблон для Visual Studio
2. Посібник ASP.NET Core
3. Керівництво по Angular 2
4. Керівництво по TypeScript
5. Компоненти від PrimeNG
6. Компоненти Bootstrap

ASP.NET Core + Angular 2 шаблон для Visual Studio це чудовий шаблон, яка налаштовує ваше програми для використання ASP.Net Core і Angular 2 створюючи купу json-файлів і наполягаючи для використання webpack. Для прикладу можна почитати статтю Запускаємо Angular2 c Visual Studio 2015

І найголовніше: ми можемо змінювати ts і html файли під час налагодження і бачити зміни при оновленні сторінки. За це відповідає класи з Microsoft.AspNetCore.SpaServices.dll
WebpackDevMiddlewareOptions і SpaRouteExtensions. Так так! Налагоджувати ми можемо в браузері. При цьому Ts файли лежать в папці (немає домену)

Плюс з шаблоном йде купа прикладів! Але всі вони збережені в ASCII. Якщо будете використовувати кирилицю, нудно пересохранить ts і html в UTF-8.

Компоненти від PrimeNG дозволяють значно спростити створення клієнтського коду. Крім того на них можна вивчити створення власних компонент і модифікувати існуючі.

Почнемо з створення хаба SignalR. Зручно використовувати типізовані хаби.

public interface IHelloHub
{

// На клієнта незалежно як названі методи на сервері
//буде використовуватися Camel нотація
// тому всі методи починаємо з прописних літер


Task sendEcho(string str, string Кому);

// Події Клієнту
// Потрібно враховувати, що Xamarin поки підтримувати передачу тільки 4 параметрів
Task addMessage(string Name, string str, string ConnectionId);

}


То при реалізації цього інтерфейсу буду контролюватися не тільки метод sendEcho. Але і Clients.Others.addMessage і Clients.Client(Кому).addMessage.

public class HelloHub : Hub<IHelloHub>
{

// Надішлемо повідомлення всім користувачам, крім відсилає якщо Кому порожній рядок
// Якщо Кому визначений, то відправимо конкретному користувачу та ID Кому
public async Task sendEcho(string str, string Кому)
{
var user = new User();
if (!ПользовательЗарегистрирован(user))
return;

if (string.IsNullOrWhiteSpace(Кому))
await Clients.Others.addMessage(user.Name, str, user.ConnectionId);
else
await Clients.Client(Кому).addMessage(user.Name, str, user.ConnectionId);
}
}

Крім того, цей інтерфейс зручно використовувати .Net додатку, і можна створити кодогенератор для TypeScript опису сервісу.

Тепер перейдемо до створення клієнта. Спочатку створимо загальний сервіс Один сервіс для всіх компонентів

Це дозволить нам тримати сервіс живим протягом усього життя сторінки, а компонент буде зчитувати дані зчитувати дані, після переходів усередині сторінки.

Код сервісу SignalR
/// <reference path="../models/ForHelloHub.ts"/>
import { IHelloHub, DataInfo, ChatMessage, User} from "../models/ForHelloHub";
import { EventEmitter } from '@angular/core';
import { SelectItem } from 'primeng/primeng';
declare var $: any;

export class HelloHub implements IHelloHub
{
// Усі повідомлення
public allMessages: ChatMessage[];
// Прапор підключення до Хабу
public connectionExists: Boolean;
// Користувач зареєстрував ім'я
public isRegistered: Boolean;
// $.connection.helloHub.server
private server: any;
// $.connection.helloHub.client
private client: any;
// $.connection.helloHub
private chat: any;

// ID підключення
private userId: string;
// Підключені користувачі
public Users: SelectItem[];
// Подія про зміну списку користувачів
public onChangeUser: EventEmitter<void> = new EventEmitter<void>(); 
// Про отримання повідомлення подія
public onAddMessage: EventEmitter<void> = new EventEmitter<void>();
// Подія про підключення до хабу
public onConnected: EventEmitter<void> = new EventEmitter<void>();
// Подія про реєстрацію имент користувача.
public onRegistered: EventEmitter<void> = new EventEmitter<void>();

constructor() {
this.userId = "";
// Встановимо початковий список з ім'ям "Всім". При його виборі
// повідомлення будуть відправлені усім користувачам, крім поточного
this.Users = [{ label: "Всім", value: ""}];
this.connectionExists = false;
this.isRegistered = false;

this.chat = $.connection.helloHub;
this.server = this.chat.server;
this.client = this.chat.client;

// Встановимо обробники подій
this.registerOnServerEvents();
this.allMessages = new Array<ChatMessage>();

// Подсоединимся до Хабу
this.startConnection();
}


// Сортування користувачів по імені. Всім повинна бути першою
private sortUsers() {
this.Users.sort((a, b: SelectItem) => {
if (a.label == "Всім") return -1;

return a.label.toLocaleUpperCase().localeCompare(b.label.toLocaleUpperCase());

});

}


//встановимо обробники подій від сервера
private registerOnServerEvents(): void {

let self = this;


// Про отримання повідомлення подія
//Task addMessage(string Name, string str, string ConnectionId);
this.client.addMessage = (name: string message: string, ConnectionId: string) => {
// Додавання повідомлень на веб-сторінку 
console.log('addMessage' + message);
self.addMessage(name, message,ConnectionId);
};


// Про реєстрацію користувача подія
//Task onConnected(string id, string userName, List < User > users);
this.client.onConnected = function (id: string userName: string, allUsers: User[]) {
self.isRegistered = true;
self.userId = id;
// Додавання всіх користувачів
for (let user of allUsers) {

self.addUser(user.ConnectionId, user.Name);
}

self.sortUsers();
// Повідомимо про зміну списку користувачів
self.onRegistered.emit();
};


//Task onNewUserConnected(string id, string userName);
// Додаємо нового користувача
this.client.onNewUserConnected = (id: string name: string) => {

self.addUser(id, name);
};

//Task onUserDisconnected(string id, string Name);
// Видаляємо користувача
this.client.onUserDisconnected = (id: string userName: string) => {

let idx = self.Users.findIndex((cur: SelectItem) => {
return cur.value == id;
});

if (idx != -1) {
return self.Users.splice(idx, 1);

};

} 
}

// Знайдемо користувача з id
// Якщо не знаходимо то створюємо нового користувача
findUser(userName:string id: string): SelectItem
{
let idx = this.Users.findIndex((cur: SelectItem) => {
return cur.value == id;
});

if (idx != -1) {
return this.Users[idx];
}
return { label: userName, value:id }

}
// Опрацюємо повідомлення з сервера
addMessage(name: string message: string, ConnectionId: string): void {

this.allMessages.splice(0, 0, new ChatMessage(message, new Date this.findUser(name, ConnectionId)));
this.onAddMessage.emit();

}


// Додамо користувача і відсортуємо по найменуванню
addUser(id: string name: string): void {
if (this.userId === "") return;

if (this.userId !== id) {
let usr = { label: name, value: id };
this.Users.push(usr);
this.sortUsers();
this.onChangeUser.emit();
}
}

// Підключимося до Хабу
private startConnection(): void {
let self = this;
$.connection.hub.start().done((data: any) => {
console.log('startConnection' + data);
self.connectionExists = true;
self.onConnected.emit();
console.log('Send onConnected');
}).fail((error) => {
console.log('Could not connect' + error);

});
}

/ / ======= методи і події сервера

// Відішлемо повідомлення Всім або конкретному користувачеві
sendEcho(str: string, Кому: string)
{

this.server.sendEcho(str, Кому);
}

// Відішлемо повідомлення на ім'я
sendByName(message: string, Кому: string)
{

this.server.sendByName(message, Кому);
}


// Зареєструватися на сервері по імені
connect(userName: string)
{
this.server.connect(userName);

}


}


Тепер перейдемо до створення компонента:

Компонент для відображення отриманих повідомлень і відправки повідомлень
import { Component, NgZone, ViewChild, AfterViewInit } from '@angular/core';
import { HelloHub } from '../services/HelloHubServise';
import { ChatMessage } from '../models/ForHelloHub';
import { SelectItem} from 'primeng/primeng';
import { Dropdown2 } from '../Dropdown/Dropdown';


@Component({
selector: 'p-HelloHubComponent',
template: require('./SignalRHelloHub.html')
})


export class HelloHubComponent {
@ViewChild(Dropdown2)
public dropdown: Dropdown2;

public allMessages: ChatMessage[];
public Users: SelectItem[];
public selectedUser: string;
public Message: string;
public selectUsername: boolean=false;
constructor(private _signalRService: HelloHub) {
// Підключимося до подій від сервісу
this.subscribeToEvents();

// Отримаємо всі повідомлення отримані за час існування сторінки
this.allMessages = this._signalRService.allMessages;
// Отримаємо дані про користувачів
this.Users = _signalRService.Users;

}

// Метод відправки повідомлень в залежності від обраних даних
public sendMessage() {



if (this.dropdown.value == "") // Якщо Вибрано "Всім" відправляємо всім користувачам, крім відправника
{
this._signalRService.sendEcho(this.Message, "");
}
else {

// В 1С може бути декілька користувачів з одним іменем 
if (!this.selectUsername) // Якщо не стоїть галка "По Імені" то відправляємо конкретного мользователю
this._signalRService.sendEcho(this.Message, this.dropdown.value);
else // відправляємо повідомлення всім користувачам з вибраним ім'ям
this._signalRService.sendByName(this.Message, this.dropdown.selectedOption.label);
}

this.Message = "";

}

private subscribeToEvents(): void {

let self = this;

// Оновимо дані про отримані повідомлення
this._signalRService.onAddMessage.subscribe(() => {
self.allMessages = this._signalRService.allMessages;
});

// Оновимо дані про користувачів
this._signalRService.onChangeUser.subscribe(() =>
{ this.Users = self._signalRService.Users; }
);
}


}



Компонент просто зчитує і оновлює дані по події з Сервісу і відправляє повідомлення через методи сервісу. Ну і покажемо HTML код компонента.

HTML шаблон
<div class="container" id="MainMessage">
<form role="form" (ngSubmit)="sendMessage()">

<div class="form-group">
<textarea type="text" [(ngModel)]="Message" name="Message" class="form-control" placeholder="Повідомлення"></textarea>
</div>

<div class="form-group">
<div class="btn-group open">
<button type="submit" class="btn btn-info">&Надіслати lt;/button>
</div>
<div class="btn-group" id="users">
<p-dropdown2 [options]="Users" [(ngModel)]="selectedUser" name="dropdownUsers"></p-dropdown2>
</div>
<div class="btn-group" id="SendByName">
<div class="checkbox">
<label>
<input type="checkbox" name="CheckBoxSendByName" [(ngModel)]="selectUsername" [disabled]="dropdown.value==""> За іменем
</label>
</div>


</div>
</div>


</form>


</div>

<div class="row">

<div class="col-xs-12 col-md-8" id="GetingMessage">
<template ngFor let-item [ngForOf]="allMessages">
<div class='panel panel-primary'>
<div class='panel-heading'>
{{item.From.label}} від {{item.Sent.toLocaleString()}}
</div>
<div class='panel-body'>
{{item.Message}}
</div>
</div>
</template>
</div>
<div class="col-xs-6 col-md-4">

</div>
</div>

У нас є форма для відправки повідомлення, що містить textarea з прив'язкою властивості Message, dropdown з даними про підключених користувачів, checkbox з прив'язкою selectUsername для відправки повідомлення по імені.

І є блок з отриманими повідомленнями allMessages. Сам Html вийшов компактним, а весь код на дуже приємне TypeScript.

Зверну увагу на застосування self всередині замикання. Це пов'язано з JS. Сам компілятор TS в JS генерує:

var _this = this;

і замінює

var transport = this.chat.transport;

на

var transport = _this.chat.transport;

Але для налагодження зручно використовувати проміжну змінну

let self = this;

Исходники можна подивитися тут.

Ну і до купи стаття і шаблони Create great looking, fast, mobile apps using JavaScript, Angular 2, and Ionic 2

Angular 2 наступає по всіх фронтах
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.