SignalR Group messages with ngrx and Angular

This article shows how SignalR can be used to send grouped messages to an Angular SignalR client, which uses ngrx to handle the SignalR events in the Angular client.

Code: https://github.com/damienbod/AspNetCoreAngularSignalR

Other posts in this series:

SignalR Groups

SignalR allows messages to be sent to specific groups if required. You can read about this here:

https://docs.microsoft.com/en-us/aspnet/signalr/overview/guide-to-the-api/working-with-groups

The documentation is for the old SignalR, but most is still relevant.

To get started, add the SignalR Nuget package to the csproj file where the Hub(s) are to be implemented.

<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.0.0-alpha1-final" />

In this application, the NewsItem class is used to send the messages between the SignalR clients and server.

namespace AspNetCoreAngularSignalR.SignalRHubs
{
    public class NewsItem
    {
        public string Header { get; set; }
        public string NewsText { get; set; }
        public string Author { get; set; }
        public string NewsGroup { get; set; }
    }
}

The NewsHub class implements the SignalR Hub which can send messages with NewsItem classes, or let the clients join, or leave a SignalR group. When the Send method is called, the class uses the NewsGroup property to send the messages only to clients in the group. If the client is not a member of the group, it will receive no message.

using Microsoft.AspNetCore.SignalR;
using System.Threading.Tasks;

namespace AspNetCoreAngularSignalR.SignalRHubs
{
    public class NewsHub : Hub
    {
        public Task Send(NewsItem newsItem)
        {
            return Clients.Group(newsItem.NewsGroup).InvokeAsync("Send", newsItem);
        }

        public async Task JoinGroup(string groupName)
        {
            await Groups.AddAsync(Context.ConnectionId, groupName);
            await Clients.Group(groupName).InvokeAsync("JoinGroup", groupName);
        }

        public async Task LeaveGroup(string groupName)
        {
            await Clients.Group(groupName).InvokeAsync("LeaveGroup", groupName);
            await Groups.RemoveAsync(Context.ConnectionId, groupName);
        }
    }
}

The SignalR hub is configured in the Startup class. The path defined in the hub, must match the configuration in the SignalR client.

app.UseSignalR(routes =>
{
	routes.MapHub<NewssHub>("looney");
});

Angular Service for the SignalR client

To use SignalR in the Angular application, the npm package @aspnet/signalr-client needs to be added to the packages.json file.

"@aspnet/signalr-client": "1.0.0-alpha2-final"

The Angular NewsService is used to send SignalR events to the ASP.NET Core server and also to handle the messages received from the server. The send, joinGroup and leaveGroup functions are used in the ngrx store effects and the init method adds event handlers for SignalR events and dispatches ngrx actions when a message is received.

import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { HubConnection } from '@aspnet/signalr-client';
import { NewsItem } from './models/news-item';
import { Store } from '@ngrx/store';
import { NewsState } from './store/news.state';
import * as NewsActions from './store/news.action';

@Injectable()
export class NewsService {

    private _hubConnection: HubConnection;

    constructor(private store: Store<any>) {
        this.init();
    }

    public send(newsItem: NewsItem): NewsItem {
        this._hubConnection.invoke('Send', newsItem);
        return newsItem;
    }

    public joinGroup(group: string): void {
        this._hubConnection.invoke('JoinGroup', group);
    }

    public leaveGroup(group: string): void {
        this._hubConnection.invoke('LeaveGroup', group);
    }

    private init() {

        this._hubConnection = new HubConnection('/looney');

        this._hubConnection.on('Send', (newsItem: NewsItem) => {
            this.store.dispatch(new NewsActions.ReceivedItemAction(newsItem));
        });

        this._hubConnection.on('JoinGroup', (data: string) => {
            this.store.dispatch(new NewsActions.ReceivedGroupJoinedAction(data));
        });

        this._hubConnection.on('LeaveGroup', (data: string) => {
            this.store.dispatch(new NewsActions.ReceivedGroupLeftAction(data));
        });

        this._hubConnection.start()
            .then(() => {
                console.log('Hub connection started')
            })
            .catch(err => {
                console.log('Error while establishing connection')
            });
    }

}

Using ngrx to manage SignalR events

The NewsState interface is used to save the application state created from the SignalR events, and the user interactions.

import { NewsItem } from '../models/news-item';

export interface NewsState {
    newsItems: NewsItem[],
    groups: string[]
};

The news.action action classes are used to connect, define the actions for events which are dispatched from Angular components, the SignalR Angular service, or ngrx effects. These actions are used in the hubConnection.on event, which receives the SignalR messages, and dispatches the proper action.

import { Action } from '@ngrx/store';
import { NewsItem } from '../models/news-item';

export const JOIN_GROUP = '[news] JOIN_GROUP';
export const LEAVE_GROUP = '[news] LEAVE_GROUP';
export const JOIN_GROUP_COMPLETE = '[news] JOIN_GROUP_COMPLETE';
export const LEAVE_GROUP_COMPLETE = '[news] LEAVE_GROUP_COMPLETE';
export const SEND_NEWS_ITEM = '[news] SEND_NEWS_ITEM';
export const SEND_NEWS_ITEM_COMPLETE = '[news] SEND_NEWS_ITEM_COMPLETE';
export const RECEIVED_NEWS_ITEM = '[news] RECEIVED_NEWS_ITEM';
export const RECEIVED_GROUP_JOINED = '[news] RECEIVED_GROUP_JOINED';
export const RECEIVED_GROUP_LEFT = '[news] RECEIVED_GROUP_LEFT';

export class JoinGroupAction implements Action {
    readonly type = JOIN_GROUP;

    constructor(public group: string) { }
}

export class LeaveGroupAction implements Action {
    readonly type = LEAVE_GROUP;

    constructor(public group: string) { }
}


export class JoinGroupActionComplete implements Action {
    readonly type = JOIN_GROUP_COMPLETE;

    constructor(public group: string) { }
}

export class LeaveGroupActionComplete implements Action {
    readonly type = LEAVE_GROUP_COMPLETE;

    constructor(public group: string) { }
}
export class SendNewsItemAction implements Action {
    readonly type = SEND_NEWS_ITEM;

    constructor(public newsItem: NewsItem) { }
}

export class SendNewsItemActionComplete implements Action {
    readonly type = SEND_NEWS_ITEM_COMPLETE;

    constructor(public newsItem: NewsItem) { }
}

export class ReceivedItemAction implements Action {
    readonly type = RECIEVED_NEWS_ITEM;

    constructor(public newsItem: NewsItem) { }
}

export class ReceivedGroupJoinedAction implements Action {
    readonly type = RECIEVED_GROUP_JOINED;

    constructor(public group: string) { }
}

export class ReceivedGroupLeftAction implements Action {
    readonly type = RECIEVED_GROUP_LEFT;

    constructor(public group: string) { }
}

export type Actions
    = JoinGroupAction
    | LeaveGroupAction
    | JoinGroupActionComplete
    | LeaveGroupActionComplete
    | SendNewsItemAction
    | SendNewsItemActionComplete
    | ReceivedItemAction
    | ReceivedGroupJoinedAction
    | ReceivedGroupLeftAction;


The newsReducer ngrx reducer class receives the actions and changes the state as required. For example, when a RECEIVED_NEWS_ITEM event is sent from the Angular SignalR service, it creates a new state with the new message appended to the existing items.

import { NewsState } from './news.state';
import { NewsItem } from '../models/news-item';
import { Action } from '@ngrx/store';
import * as newsAction from './news.action';

export const initialState: NewsState = {
    newsItems: [],
    groups: ['group']
};

export function newsReducer(state = initialState, action: newsAction.Actions): NewsState {
    switch (action.type) {

        case newsAction.RECEIVED_GROUP_JOINED:
            return Object.assign({}, state, {
                newsItems: state.newsItems,
                groups: (state.groups.indexOf(action.group) > -1) ? state.groups : state.groups.concat(action.group)
            });

        case newsAction.RECEIVED_NEWS_ITEM:
            return Object.assign({}, state, {
                newsItems: state.newsItems.concat(action.newsItem),
                groups: state.groups
            });

        case newsAction.RECEIVED_GROUP_LEFT:
            const data = [];
            for (const entry of state.groups) {
                if (entry !== action.group) {
                    data.push(entry);
                }
            }
            console.log(data);
            return Object.assign({}, state, {
                newsItems: state.newsItems,
                groups: data
            });
        default:
            return state;

    }
}

The ngrx store is configured in the module class.

StoreModule.forFeature('news', {
     newsitems: newsReducer,
}),
 EffectsModule.forFeature([NewsEffects])

The store is then used in the different Angular components. The component only uses the ngrx store to send, receive SignalR data.

import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Store } from '@ngrx/store';
import { NewsState } from '../store/news.state';
import * as NewsActions from '../store/news.action';
import { NewsItem } from '../models/news-item';

@Component({
    selector: 'app-news-component',
    templateUrl: './news.component.html'
})

export class NewsComponent implements OnInit {
    public async: any;
    newsItem: NewsItem;
    group = 'group';
    newsState$: Observable<NewsState>;

    constructor(private store: Store<any>) {
        this.newsState$ = this.store.select<NewsState>(state => state.news.newsitems);
        this.newsItem = new NewsItem();
        this.newsItem.AddData('', '', 'me', this.group);
    }

    public sendNewsItem(): void {
        this.newsItem.NewsGroup = this.group;
        this.store.dispatch(new NewsActions.SendNewsItemAction(this.newsItem));
    }

    public join(): void {
        this.store.dispatch(new NewsActions.JoinGroupAction(this.group));
    }

    public leave(): void {
        this.store.dispatch(new NewsActions.LeaveGroupAction(this.group));
    }

    ngOnInit() {
    }
}

The component template then displays the data as required.

<div class="container-fluid">

    <h1>Send some basic news messages</h1>

    <div class="row">
        <form class="form-inline" >
            <div class="form-group">
                <label for="header">Group</label>
                <input type="text" class="form-control" id="header" placeholder="your header..." name="header" [(ngModel)]="group" required>
            </div>
            <button class="btn btn-primary" (click)="join()">Join</button>
            <button class="btn btn-primary" (click)="leave()">Leave</button>
        </form>
    </div>
    <hr />
    <div class="row">
        <form class="form" (ngSubmit)="sendNewsItem()" #newsItemForm="ngForm">
            <div class="form-group">
                <label for="header">Header</label>
                <input type="text" class="form-control" id="header" placeholder="your header..." name="header" [(ngModel)]="newsItem.header" required>
            </div>
            <div class="form-group">
                <label for="newsText">Text</label>
                <input type="text" class="form-control" id="newsText" placeholder="your newsText..." name="newsText" [(ngModel)]="newsItem.newsText" required>
            </div>
            <div class="form-group">
                <label for="newsText">Author</label>
                <input type="text" class="form-control" id="author" placeholder="your newsText..." name="author" [(ngModel)]="newsItem.author" required>
            </div>
            <button type="submit" class="btn btn-primary" [disabled]="!newsItemForm.valid">Send News to: {{group}}</button>
        </form>
    </div>

    <div class="row" *ngIf="(newsState$|async)?.newsItems.length > 0">
        <div class="table-responsive">
            <table class="table table-striped">
                <thead>
                    <tr>
                        <th>#</th>
                        <th>header</th>
                        <th>Text</th>
                        <th>Author</th>
                        <th>roup</th>
                    </tr>
                </thead>
                <tbody>
                    <tr *ngFor="let item of (newsState$|async)?.newsItems; let i = index">
                        <td>{{i + 1}}</td>
                        <td>{{item.header}}</td>
                        <td>{{item.newsText}}</td>
                        <td>{{item.author}}</td>
                        <td>{{item.newsGroup}}</td>
                    </tr>
                </tbody>
            </table>
        </div>
    </div>
 
    <div class="row" *ngIf="(newsState$|async)?.length <= 0">
        <span>No news items</span>
    </div>
</div>

When the application is started, SignalR messages can be sent, received and displayed from the instances of the Angaulr application.


Links

https://github.com/aspnet/SignalR

https://github.com/aspnet/SignalR#readme

https://github.com/ngrx

https://www.npmjs.com/package/@aspnet/signalr-client

https://dotnet.myget.org/F/aspnetcore-ci-dev/api/v3/index.json

https://dotnet.myget.org/F/aspnetcore-ci-dev/npm/

https://dotnet.myget.org/feed/aspnetcore-ci-dev/package/npm/@aspnet/signalr-client

https://www.npmjs.com/package/msgpack5

Advertisements

2 comments

  1. […] SignalR Group messages with ngrx and Angular – Damien Bowden […]

  2. […] SignalR Group messages with ngrx and Angular (Damien Bowden) […]

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: