Всем привет! Хочу поделиться своим опытом использования 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 наступает по всем фронтам
Поделиться с друзьями
-->

Комментарии (13)


  1. ggrnd0
    26.12.2016 18:53

    Зачем bootstrap для PrimeNG? там чего то не хватает?


    1. Serginio1
      26.12.2016 20:01

      bootstrap для кучи, если самому захочется поэксперементировать, да и что бы понимать как это все устроено


  1. Serginio1
    28.12.2016 10:28

    Вот смотрю я на развитие Angular 2 Ionic 2 Это очень близко к WPF. Сейчас для .Net Core нет кроссплатформенного декстопного GUI. Но вот есть идея скрестить Angular 2 и .Net Core. В свое время я сделал обертку для использования классов .Net Core из неуправляемого кода Мои статьи

    В обертке реализован доступ к классам из сборок, итераторы, вывод типов для дженериков, методы расширения, поддержка событий. То есть можно писать код близкий к C# на 1С. К моему огромному сожалению 1С это оказалось не нужно. В том плане, что они не хотят модернизировать Native Api для передачи в параметрах объектов и возврате объектов из методов. Поэтому ссылки на объекты передаются виде строки и из которых создаются объекты ВК. Это очень неудобно.

    Но вот подумалось, что эту технологию можно прикрутить к браузеру через плагин. То есть можно создавать обертку на стороне браузера. При этом можно для TypeScript генерить интерфейсы (в свое время предлагал псевдоинтерфейсы для динамиков в .Net) и использовать IntelliSense на полную катушку. То есть можно как в Xamarin писать единую общую часть в Dll, а GUI писать отдельно. Я знаю, что 1С использует технологию «Создание компонент с использованием технологии Native API». и в Web клиенте через плагины. Буду благодарен за конструктивную критику, идеи и ссылки на создание плагинов.


    1. denismaster
      02.01.2017 19:57

      Не знаю конечно, как для веба, все таки там и обычная связка с вторым Ангуляром отлично работает, а вот скрестить что-то похожее на Electron.js, второй Ангуляр и .NET, это было бы интересно)


      1. Serginio1
        02.01.2017 20:56
        -1

        спасибо. Интересно. Сейчас стал изучать хромовский Native Client SDK

        Скачал по ссылке https://developer.chrome.com/native-client/sdk/download хромовского клиента.
        Нашел часто используемую структуру в файле ..\nacl_sdk\pepper_49\include\ppapi\c\ppVar.h

        typedef enum {
           /**
           * Represents a JavaScript object. This vartype is not currently usable
           * from modules, although it is used internally for some tasks. These objects
           * are reference counted, so AddRef() and Release() must be used properly to
           * avoid memory leaks.
           */
          PP_VARTYPE_OBJECT = 6,
        


        Эх теперь с С разбираться.


  1. kxl
    28.12.2016 11:54
    +2

    Я 16 лет занимаюсь разработкой на 1С, немного меньше на C#.
    НО! Никогда не пытаюсь смешивать английский и русский в коде. Ведь всего-то надо подумать о названии метода по английски и освоить для себя <summary/>.
    А то, задумайтесь, как 1С-программист, как бы вы работали с модулями конфигурации, если бы часть переменных и методов были на английском, часть на французском, а все то, что касается встроенного языка (и прикладных объектов) — по-русски.


    1. DjoNIK
      28.12.2016 12:15
      +1

      Так-то и именование методов в lowerCamelCase: sendEcho, addMessage, etc. — какой-то Java-style.


      1. Serginio1
        28.12.2016 12:27

        Там суть такова, что для JSON и наименования методов практикуется Кэмел нотация. А на стороне клиента все методы будут в Кэмел. Поэтому, что бы не путаться проще задать им названия сразу в Кэмел


        1. Splo1ter
          28.12.2016 14:09
          +2

          Там же вроде как есть аттрибут для именования полей со стороны клиента(Именуем в Паскаль кейс, вешаем аттрибут для клиентского именования и вуаля).


          1. Serginio1
            28.12.2016 14:18

            Можно и так. Просто я в статье ориентировался на интерфейс который создавался на стороне Asp.Net и по нему создавался класс на TS. В дальнейшем можно кодогенератор прикрутить как для TS так и .Net клиентов


    1. Serginio1
      28.12.2016 12:23

      А у меня так повсеместно. Я использую Использование классов .Net в 1С для новичков. А там все классы на английском.
      Кроме того и сама 1С использует Руслиш. HttpСоединение, ФабрикаXDTO, ЗаписьXML итд.
      Просто привыкаешь самодокументироваться на русском.
      Просто я не очень хорошо знаю английский, поэтому мне приходится тратить время на осмысленные названия. И при этом давать комментарии на русском.


      1. voodoo_dn
        28.12.2016 14:13
        +2

        Не стоит так делать. Стиль 1С — не лучшая практика. С# далеко не 1С, может стоит в нем придерживаться стандартов(английские название, camelcase, etc.)?


  1. Serginio1
    28.12.2016 14:16
    -1

    Так в этой статье всего один метод и один параметр.
    Кроме того самодокументация лучше чем английское название и русский комментарий, особенно сложных названий, там где на английском будет неоднозначное определение. Но я уже кучу копий по поводу руслиша сломал. Можно почитать все предыдущие статьи.