Pull to refresh

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

Reading time 8 min
Views 49K
Всем привет! Хочу поделиться своим опытом использования 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">Отправить</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 наступает по всем фронтам
Tags:
Hubs:
+6
Comments 34
Comments Comments 34

Articles