ASP.NET CoreSignalR を使用したリアルタイム Web アプリ
このトピックでは、ASP.NET Core SignalR を使用してデータのストリーミングと受信の両方に対応するアプリケーションを作成する方法を説明します。
前提条件:
- ASP.NET Core と Angular の基本的な知識。
- .NET Core 3.1 がインストールされ、Visual Studio などの IDE。
この記事の終わりまでに、次のことがわかります:
- SignalR を追加して使用する方法。
- クライアント接続を開き、メソッドの呼び出しの概念を使用してクライアントごとにデータをストリーミングする方法。
- Observables を使用して Angular アプリケーションで SignalR サービスを使用する方法。
SignalR はいくつかの転送を利用し、クライアントとサーバーの機能 - WebSockets、サーバー送信イベント (SSE)、またはロングポーリング (英語) を考慮して、利用可能な最適な転送を自動的に選択します。
クライアントがサーバーにリアルタイムで接続されているときに、SSE とロングポーリングを除いて、WebSockets の観点から話すと、何かが起こったときはいつでも、サーバーはその WebSocket を介してクライアントにメッセージを送信することを認識します。一昔前のクライアントとサーバーでは、ロングポーリング転送が使用されます。
これは、SignalR が最新のクライアントとサーバーを処理する方法であり、利用可能な場合は内部で WebSockets を使用し、そうでない場合は他の技術とテクノロジーに適切にフォールバックします。

それはハンドシェイクのようなもので、クライアントとサーバーは何を使用するかについて合意します。これはプロセス ネゴシエーションと呼ばれます。

SignalR の例
このデモの目的は、ASP.NET Core SignalR を使用してリアルタイム データ ストリームを表示する財務用スクリーン ボードを紹介することです。
import { NgModule } from "@angular/core";
import { FormsModule } from "@angular/forms";
import { BrowserModule } from "@angular/platform-browser";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { AppComponent } from "./app.component";
import { IgxPreventDocumentScrollModule } from "./directives/prevent-scroll.directive";
import { IgxCategoryChartModule } from "igniteui-angular-charts";
import {
IgxGridModule,
IgxButtonGroupModule,
IgxIconModule,
IgxSliderModule,
IgxToggleModule,
IgxButtonModule,
IgxExcelExporterService,
IgxCsvExporterService,
IgxSwitchModule,
IgxRippleModule,
IgxDialogModule,
IgxToastModule,
IgxGridComponent
} from "igniteui-angular";
import { GridFinJSDockManagerComponent } from "./grid-finjs-dock-manager/grid-finjs-dock-manager.component";
import { HttpClientModule } from "@angular/common/http";
import { SignalRService } from "./services/signal-r.service";
import { CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
import { FloatingPanesService } from "./services/floating-panes.service";
import {
DockSlotComponent,
GridHostDirective
} from "./grid-finjs-dock-manager/dock-slot.component";
import { defineCustomElements } from 'igniteui-dockmanager/loader';
defineCustomElements();
@NgModule({
bootstrap: [AppComponent],
declarations: [
AppComponent,
GridFinJSDockManagerComponent,
DockSlotComponent,
GridHostDirective
],
imports: [
BrowserModule,
BrowserAnimationsModule,
FormsModule,
IgxPreventDocumentScrollModule,
IgxGridModule,
IgxButtonGroupModule,
IgxIconModule,
IgxSliderModule,
IgxToggleModule,
IgxButtonModule,
IgxSwitchModule,
IgxRippleModule,
IgxCategoryChartModule,
IgxDialogModule,
IgxToastModule,
HttpClientModule
],
providers: [
IgxExcelExporterService,
IgxCsvExporterService,
SignalRService,
FloatingPanesService
],
entryComponents: [
IgxGridComponent,
DockSlotComponent
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class AppModule {}
ts
import { AfterViewInit, ChangeDetectorRef, Component, ComponentFactoryResolver, ElementRef, Renderer2, OnDestroy, OnInit, DoCheck, TemplateRef, ViewChild, ViewContainerRef, ViewEncapsulation } from '@angular/core';
import { AbsoluteScrollStrategy, ConnectedPositioningStrategy, DefaultSortingStrategy, GridColumnDataType, IgxColumnComponent, IgxGridComponent, IgxOverlayOutletDirective, IgxSelectComponent, OverlaySettings, SortingDirection } from 'igniteui-angular';
import { IgcDockManagerLayout, IgcDockManagerPaneType, IgcSplitPane, IgcSplitPaneOrientation } from 'igniteui-dockmanager';
import { Subject } from 'rxjs';
import { first, takeUntil } from 'rxjs/operators';
import { FloatingPanesService } from '../services/floating-panes.service';
import { SignalRService } from '../services/signal-r.service';
import { DockSlotComponent, GridHostDirective } from './dock-slot.component';
@Component({
encapsulation: ViewEncapsulation.None,
providers: [SignalRService, FloatingPanesService],
selector: 'app-finjs-dock-manager',
templateUrl: './grid-finjs-dock-manager.component.html',
styleUrls: ['./grid-finjs-dock-manager.component.scss']
})
export class GridFinJSDockManagerComponent implements OnInit, OnDestroy, AfterViewInit, DoCheck {
@ViewChild('grid1', { static: true }) public grid1: IgxGridComponent;
@ViewChild('grid2', { static: true }) public grid2: IgxGridComponent;
@ViewChild(GridHostDirective) public host: GridHostDirective;
@ViewChild('dock', { read: ElementRef }) public dockManager: ElementRef<HTMLIgcDockmanagerElement>;
@ViewChild('priceTemplate', { read: TemplateRef })
public priceTemplate: TemplateRef<any>;
@ViewChild(IgxSelectComponent) public select: IgxSelectComponent;
@ViewChild('freq', { read: IgxSelectComponent }) public selectFrequency: IgxSelectComponent;
@ViewChild(IgxOverlayOutletDirective) outlet: IgxOverlayOutletDirective;
public isDarkTheme = true;
public frequencyItems: number[] = [300, 600, 900];
public frequency = this.frequencyItems[1];
public dataVolumeItems: number[] = [100, 500, 1000, 5000, 10000];
public dataVolume: number = this.dataVolumeItems[1];
public isLoading = true;
public data: any;
public liveData = true;
public columnFormat = { digitsInfo: '1.3-3'};
public columnFormatChangeP = { digitsInfo: '2.3-3'};
public slotCounter = 1;
public customOverlaySettings: OverlaySettings = {
positionStrategy: new ConnectedPositioningStrategy(),
scrollStrategy: new AbsoluteScrollStrategy()
};
public freqOverlaySettings: OverlaySettings = {
positionStrategy: new ConnectedPositioningStrategy(),
scrollStrategy: new AbsoluteScrollStrategy()
};
public docLayout: IgcDockManagerLayout = {
rootPane: {
type: IgcDockManagerPaneType.splitPane,
orientation: IgcSplitPaneOrientation.horizontal,
panes: [
{
type: IgcDockManagerPaneType.contentPane,
contentId: 'actionPane',
header: 'Actions pane',
size: 20,
isPinned: false,
allowClose: false
},
{
size: 50,
type: IgcDockManagerPaneType.contentPane,
contentId: 'gridStockPrices',
header: 'Stock Market Data',
allowClose: false
},
{
type: IgcDockManagerPaneType.splitPane,
orientation: IgcSplitPaneOrientation.vertical,
size: 50,
panes: [
{
type: IgcDockManagerPaneType.documentHost,
size: 50,
rootPane: {
type: IgcDockManagerPaneType.splitPane,
orientation: IgcSplitPaneOrientation.horizontal,
panes: [
{
type: IgcDockManagerPaneType.tabGroupPane,
panes: [
{
type: IgcDockManagerPaneType.contentPane,
contentId: 'forexMarket',
header: 'Market Data 1'
},
{
type: IgcDockManagerPaneType.contentPane,
contentId: 'content4',
header: 'Market Data 2'
}
]
}
]
}},
{
type: IgcDockManagerPaneType.contentPane,
contentId: 'etfStockPrices',
header: 'Market Data 3',
size: 50,
allowClose: false
}
]
}
]
},
floatingPanes: []
};
public columns: { field: string,
width: string,
sortable: boolean,
filterable: boolean,
type: GridColumnDataType,
groupable?: boolean,
cellClasses?: string,
bodyTemplate?: string } [] = [
{ field: 'buy', width: '110px', sortable: false, filterable: false, type: 'currency' },
{ field: 'sell', width: '110px', sortable: false, filterable: false, type: 'currency' },
{ field: 'openPrice', width: '120px', sortable: true, filterable: true, type: 'currency'},
{ field: 'lastUpdated', width: '120px', sortable: true, filterable: true, type: 'date'},
{ field: 'spread', width: '110px', sortable: false, filterable: false, type: 'number' },
{ field: 'volume', width: '110px', sortable: true, filterable: false, type: 'number' },
{ field: 'settlement', width: '100px', sortable: true, filterable: true, type: 'string', groupable: true },
{ field: 'country', width: '100px', sortable: true, filterable: true, type: 'string'},
{ field: 'highD', width: '110px', sortable: true, filterable: false, type: 'currency' },
{ field: 'lowD', width: '110px', sortable: true, filterable: false, type: 'currency' },
{ field: 'highY', width: '110px', sortable: true, filterable: false, type: 'currency' },
{ field: 'lowY', width: '110px', sortable: true, filterable: false, type: 'currency' },
{ field: 'startY', width: '110px', sortable: true, filterable: false, type: 'currency' },
{ field: 'indGrou', width: '136px', sortable: false, filterable: false, type: 'string'},
{ field: 'indSect', width: '136px', sortable: false, filterable: false, type: 'string'},
{ field: 'indSubg', width: '136px', sortable: false, filterable: false, type: 'string'},
{ field: 'secType', width: '136px', sortable: false, filterable: false, type: 'string'},
{ field: 'issuerN', width: '136px', sortable: false, filterable: false, type: 'string'},
{ field: 'moodys', width: '136px', sortable: false, filterable: false, type: 'string'},
{ field: 'fitch', width: '136px', sortable: false, filterable: false, type: 'string'},
{ field: 'dbrs', width: '136px', sortable: false, filterable: false, type: 'string'},
{ field: 'collatT', width: '136px', sortable: false, filterable: false, type: 'string'},
{ field: 'curncy', width: '136px', sortable: false, filterable: false, type: 'string'},
{ field: 'security', width: '136px', sortable: false, filterable: false, type: 'string'},
{ field: 'sector', width: '136px', sortable: false, filterable: false, type: 'string'},
{ field: 'cusip', width: '136px', sortable: false, filterable: false, type: 'string'},
{ field: 'ticker', width: '136px', sortable: false, filterable: false, type: 'string'},
{ field: 'cpn', width: '136px', sortable: false, filterable: false, type: 'string'}
];
private destroy$ = new Subject<any>();
constructor(public dataService: SignalRService, private paneService: FloatingPanesService, private cdr: ChangeDetectorRef, private componentFactoryResolver: ComponentFactoryResolver, private elementRef: ElementRef, private renderer:Renderer2) {}
public ngOnInit() {
this.dataService.startConnection(this.frequency, this.dataVolume, true, false);
this.data = this.dataService.data;
this.data.pipe(takeUntil(this.destroy$)).subscribe((data) => {
if (data.length !== 0) {
this.isLoading = false;
};
});
}
public ngOnDestroy() {
this.dataService.stopLiveData();
this.destroy$.next(true);
this.destroy$.complete();
}
public ngDoCheck() {
if (this.isDarkTheme) {
this.renderer.removeClass(this.elementRef.nativeElement, 'light-theme');
this.renderer.addClass(this.elementRef.nativeElement, 'dark-theme');
}
else {
this.renderer.removeClass(this.elementRef.nativeElement, 'dark-theme');
this.renderer.addClass(this.elementRef.nativeElement, 'light-theme');
}
}
public ngAfterViewInit() {
setTimeout(() => {
const x = (this.dockManager.nativeElement.getBoundingClientRect().width / 3);
const y = (this.dockManager.nativeElement.getBoundingClientRect().height / 3);
this.paneService.initialPanePosition = { x, y };
this.grid2.selectColumns(['price', 'change', 'changeP']);
this.customOverlaySettings.target = this.select.inputGroup.element.nativeElement;
this.customOverlaySettings.outlet = this.outlet;
this.freqOverlaySettings.target = this.selectFrequency.inputGroup.element.nativeElement;
this.freqOverlaySettings.outlet = this.outlet;
this.grid1.groupingExpressions = [{
dir: SortingDirection.Desc,
fieldName: 'category',
ignoreCase: false,
strategy: DefaultSortingStrategy.instance()
},
{
dir: SortingDirection.Desc,
fieldName: 'type',
ignoreCase: false,
strategy: DefaultSortingStrategy.instance()
},
{
dir: SortingDirection.Desc,
fieldName: 'settlement',
ignoreCase: false,
strategy: DefaultSortingStrategy.instance()
}];
}, 500);
}
public paramsChanged() {
this.dataService.hasRemoteConnection ? this.dataService.broadcastParams(this.frequency, this.dataVolume, true, false) :
this.dataService.startConnection(this.frequency, this.dataVolume, true, false);
this.data = this.dataService.data;
}
public stopFeed() {
this.dataService.stopLiveData();
}
public streamData(event) {
event.checked ? this.paramsChanged() : this.stopFeed();
this.liveData = event.checked;
}
private negative = (rowData: any): boolean => rowData['changeP'] < 0;
private positive = (rowData: any): boolean => rowData['changeP'] > 0;
private changeNegative = (rowData: any): boolean => rowData['changeP'] < 0 && rowData['changeP'] > -1;
private changePositive = (rowData: any): boolean => rowData['changeP'] > 0 && rowData['changeP'] < 1;
private strongPositive = (rowData: any): boolean => rowData['changeP'] >= 1;
private strongNegative = (rowData: any, key: string): boolean => rowData['changeP'] <= -1;
public trends = {
changeNeg: this.changeNegative,
changePos: this.changePositive,
negative: this.negative,
positive: this.positive,
strongNegative: this.strongNegative,
strongPositive: this.strongPositive
};
public trendsChange = {
changeNeg2: this.changeNegative,
changePos2: this.changePositive,
strongNegative2: this.strongNegative,
strongPositive2: this.strongPositive
};
public createGrid() {
const id: string = 'slot-' + this.slotCounter++;
const splitPane: IgcSplitPane = {
type: IgcDockManagerPaneType.splitPane,
orientation: IgcSplitPaneOrientation.horizontal,
floatingWidth: 550,
floatingHeight: 350,
panes: [
{
type: IgcDockManagerPaneType.contentPane,
header: id,
contentId: id
}
]
};
this.paneService.appendPane(splitPane);
this.dockManager.nativeElement.layout.floatingPanes.push(splitPane);
this.docLayout = { ...this.dockManager.nativeElement.layout };
this.cdr.detectChanges();
const dockSlotComponentFactory = this.componentFactoryResolver.resolveComponentFactory(DockSlotComponent);
const dockSlotComponent = this.host.viewContainerRef.createComponent(dockSlotComponentFactory);
dockSlotComponent.instance.id = id;
dockSlotComponent.instance.viewInit.pipe(first()).subscribe(() => {
const gridViewContainerRef = dockSlotComponent.instance.gridHost.viewContainerRef;
this.loadGridComponent(gridViewContainerRef, dockSlotComponent.instance.destroy$);
});
}
public loadGridComponent(viewContainerRef: ViewContainerRef, destructor: Subject<any>) {
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(IgxGridComponent);
viewContainerRef.clear();
const componentRef = viewContainerRef.createComponent(componentFactory);
const grid = (componentRef.instance as IgxGridComponent);
grid.autoGenerate = true;
this.dataService.data.pipe(takeUntil(destructor)).subscribe(d => grid.data = d);
grid.columnInit.pipe(takeUntil(destructor)).subscribe((col: IgxColumnComponent) => {
if (col.field === 'price') {
col.cellClasses = this.trends;
col.bodyTemplate = this.priceTemplate;
}
if (col.field === 'change' || col.field === 'changeP') {
col.cellClasses = this.trendsChange;
}
});
grid.columnSelection = 'multiple';
grid.cellSelection = 'none';
grid.displayDensity = 'compact';
this.cdr.detectChanges();
}
}
ts
<igc-dockmanager #dock class="dock-m-position igx-scrollbar" [layout]="docLayout">
<div class="actionPane" slot="actionPane" style="height: 100%; padding: 20px;">
<div class="actionItem">
Change theme: <br/> <igx-switch [(ngModel)]="isDarkTheme">Dark Mode</igx-switch>
</div>
<div class="actionItem">
Start/Stop live data: <igx-switch [(ngModel)]="liveData" (change)="streamData($event)">{{ liveData ===
true ? 'Streaming' : 'Not Streaming' }}</igx-switch>
</div>
<div class="actionItem">
<igx-select [(ngModel)]="dataVolume" (ngModelChange)="paramsChanged()" [overlaySettings]="customOverlaySettings">
<label igxLabel>Change data volume</label>
<igx-prefix>
<igx-icon>view_list</igx-icon>
</igx-prefix>
<igx-select-item *ngFor="let item of dataVolumeItems" [value]="item">
{{item}}
</igx-select-item>
</igx-select>
</div>
<div class="actionItem">
<igx-select [(ngModel)]="frequency" (ngModelChange)="paramsChanged()" [overlaySettings]="freqOverlaySettings" #freq>
<label igxLabel>Change update frequency</label>
<igx-prefix>
<igx-icon>cell_wifi</igx-icon>
</igx-prefix>
<igx-select-item *ngFor="let item of frequencyItems" [value]="item">
{{item}}
</igx-select-item>
</igx-select>
</div>
<div igxButton (click)="createGrid()" [disabled]="docLayout.floatingPanes.length >= 5">Add Floating Pane</div>
<div igxOverlayOutlet #outlet></div>
</div>
<div slot="gridStockPrices" style="height: 100%;">
<igx-grid #grid1 [data]="data | async" [displayDensity]="'compact'" [isLoading]="isLoading"
[allowFiltering]="true" [filterMode]="'excelStyleFilter'" [primaryKey]="'id'"
[columnSelection]="'multiple'" [cellSelection]="'none'" [outlet]="filteringOverlayOutlet">
<igx-column [field]="'id'" [width]="'70px'" [hidden]="true" [sortable]="true"></igx-column>
<igx-column [field]="'category'" [width]="'120px'" [sortable]="true"></igx-column>
<igx-column [field]="'type'" [width]="'100px'" [sortable]="true" [filterable]='false'>
</igx-column>
<igx-column [field]="'contract'" [width]="'100px'" [sortable]="true" [groupable]="true">
</igx-column>
<igx-column [field]="'price'" [width]="'130px'" dataType="number" [cellClasses]="trends"
[sortable]="true">
<ng-template igxCell let-cell="cell" #priceTemplate>
<div class="finjs-icons">
<span>{{cell.value | currency:'USD':'symbol':'1.4-4'}}</span>
<igx-icon *ngIf="trends.positive(cell.row.data)">trending_up</igx-icon>
<igx-icon *ngIf="trends.negative(cell.row.data)">trending_down</igx-icon>
</div>
</ng-template>
</igx-column>
<igx-column [field]="'change'" [width]="'120px'" dataType="number" [headerClasses]="'headerAlignSyle'"
[sortable]="true" [cellClasses]="trendsChange">
</igx-column>
<igx-column [field]="'changeP'" [width]="'110px'" dataType="percent"
[pipeArgs]="columnFormatChangeP" [sortable]="true" [cellClasses]="trendsChange">
</igx-column>
<igx-column *ngFor="let c of columns" [field]="c.field" [width]="c.width"
[sortable]="c.sortable" [filterable]="c.filterable" [dataType]="c.type"
[cellClasses]="c.cellClasses" [bodyTemplate]="c.bodyTemplate" [groupable]="c.groupable">
</igx-column>
</igx-grid>
</div>
<div slot="forexMarket" style="height: 100%;">
<igx-grid #grid2 [data]="data | async" [displayDensity]="'compact'" [isLoading]="isLoading"
[allowFiltering]="true" [filterMode]="'excelStyleFilter'" [primaryKey]="'id'" [outlet]="filteringOverlayOutlet"
[columnSelection]="'multiple'" [cellSelection]="'none'">
<igx-column [field]="'id'" [width]="'70px'" [hidden]='true' [sortable]="true"></igx-column>
<igx-column [field]="'category'" [width]="'120px'" [sortable]="true" [groupable]="true"></igx-column>
<igx-column [field]="'type'" [width]="'100px'" [sortable]="true" [filterable]='false' [groupable]="true">
</igx-column>
<igx-column [field]="'contract'" [width]="'100px'" [sortable]="true" [groupable]="true">
</igx-column>
<igx-column [field]="'price'" [width]="'120px'" dataType="number" [cellClasses]="trends"
[sortable]="true">
<ng-template igxCell let-cell="cell">
<div class="finjs-icons">
<span>{{cell.value | currency:'USD':'symbol':'1.4-4'}}</span>
<igx-icon *ngIf="trends.positive(cell.row.data)">trending_up</igx-icon>
<igx-icon *ngIf="trends.negative(cell.row.data)">trending_down</igx-icon>
</div>
</ng-template>
</igx-column>
<igx-column [field]="'change'" [width]="'120px'" dataType="number" [headerClasses]="'headerAlignSyle'"
[sortable]="true" [cellClasses]="trendsChange">
</igx-column>
<igx-column [field]="'changeP'" [width]="'110px'" dataType="percent"
[pipeArgs]="columnFormatChangeP" [sortable]="true" [cellClasses]="trendsChange">
</igx-column>
<igx-column *ngFor="let c of columns" [field]="c.field" [width]="c.width"
[sortable]="c.sortable" [filterable]="c.filterable" [dataType]="c.type"
[cellClasses]="c.cellClasses" [bodyTemplate]="c.bodyTemplate" [groupable]="c.groupable">
</igx-column>
</igx-grid>
</div>
<div slot="content4" style="height: 100%;">
<igx-grid #grid3 [data]="data | async" [displayDensity]="'compact'" [isLoading]="isLoading"
[allowFiltering]="true" [filterMode]="'excelStyleFilter'" [primaryKey]="'id'" [outlet]="filteringOverlayOutlet"
[columnSelection]="'multiple'" [cellSelection]="'none'">
<igx-column [field]="'id'" [width]="'70px'" [hidden]='true' [sortable]="true"></igx-column>
<igx-column [field]="'category'" [width]="'120px'" [sortable]="true" [groupable]="true"></igx-column>
<igx-column [field]="'type'" [width]="'100px'" [sortable]="true" [filterable]='false' [groupable]="true">
</igx-column>
<igx-column [field]="'contract'" [width]="'100px'" [sortable]="true" [groupable]="true">
</igx-column>
<igx-column [field]="'price'" [width]="'120px'" dataType="number" [cellClasses]="trends"
[sortable]="true">
<ng-template igxCell let-cell="cell">
<div class="finjs-icons">
<span>{{cell.value | currency:'USD':'symbol':'1.4-4'}}</span>
<igx-icon *ngIf="trends.positive(cell.row.data)">trending_up</igx-icon>
<igx-icon *ngIf="trends.negative(cell.row.data)">trending_down</igx-icon>
</div>
</ng-template>
</igx-column>
<igx-column [field]="'change'" [width]="'120px'" dataType="number" [headerClasses]="'headerAlignSyle'"
[sortable]="true" [cellClasses]="trendsChange">
</igx-column>
<igx-column [field]="'changeP'" [width]="'110px'" dataType="percent"
[pipeArgs]="columnFormatChangeP" [sortable]="true" [cellClasses]="trendsChange">
</igx-column>
<igx-column *ngFor="let c of columns" [field]="c.field" [width]="c.width"
[sortable]="c.sortable" [filterable]="c.filterable" [dataType]="c.type"
[cellClasses]="c.cellClasses" [bodyTemplate]="c.bodyTemplate" [groupable]="c.groupable">
</igx-column>
</igx-grid>
</div>
<div slot="etfStockPrices" style="height: 100%;">
<igx-grid #grid4 [data]="data | async" [displayDensity]="'compact'" [isLoading]="isLoading"
[allowFiltering]="true" [filterMode]="'excelStyleFilter'" [primaryKey]="'id'" [outlet]="filteringOverlayOutlet"
[columnSelection]="'multiple'" [cellSelection]="'none'">
<igx-paginator></igx-paginator>
<igx-column [field]="'id'" [width]="'70px'" [hidden]='true' [sortable]="true"></igx-column>
<igx-column [field]="'category'" [width]="'120px'" [sortable]="true" [groupable]="true"></igx-column>
<igx-column [field]="'type'" [width]="'100px'" [sortable]="true" [filterable]='false' [groupable]="true">
</igx-column>
<igx-column [field]="'contract'" [width]="'100px'" [sortable]="true" [groupable]="true">
</igx-column>
<igx-column [field]="'price'" [width]="'120px'" dataType="number" [cellClasses]="trends"
[sortable]="true">
<ng-template igxCell let-cell="cell">
<div class="finjs-icons">
<span>{{cell.value | currency:'USD':'symbol':'1.4-4'}}</span>
<igx-icon *ngIf="trends.positive(cell.row.data)">trending_up</igx-icon>
<igx-icon *ngIf="trends.negative(cell.row.data)">trending_down</igx-icon>
</div>
</ng-template>
</igx-column>
<igx-column [field]="'change'" [width]="'120px'" dataType="number" [headerClasses]="'headerAlignSyle'"
[sortable]="true" [cellClasses]="trendsChange">
</igx-column>
<igx-column [field]="'changeP'" [width]="'110px'" dataType="percent"
[pipeArgs]="columnFormatChangeP" [sortable]="true" [cellClasses]="trendsChange">
</igx-column>
<igx-column *ngFor="let c of columns" [field]="c.field" [width]="c.width"
[sortable]="c.sortable" [filterable]="c.filterable" [dataType]="c.type" [cellClasses]="c.cellClasses"
[bodyTemplate]="c.bodyTemplate" [groupable]="c.groupable">
</igx-column>
</igx-grid>
</div>
<ng-template #host gridHost>
</ng-template>
</igc-dockmanager>
<div class="dark-theme" #filteringOverlayOutlet="overlay-outlet" igxOverlayOutlet></div>
html
@use 'igniteui-dockmanager/dist/collection/styles/igc.themes';
@use '../../variables' as *;
.actionItem {
margin-block-end: rem(20px);
}
.finjs-icons {
display: flex;
align-items: center;
igx-icon {
font-size: rem(16px);
width: rem(16px);
height: rem(16px);
margin-inline-start: rem(4px);
}
}
.changePos,
.changeNeg,
.strongPositive,
.strongNegative {
color: contrast-color(null, 'gray', 500) !important;
.igx-grid__td-text {
padding: rem(2px) rem(5px);
}
}
.positive {
color: color(null, 'success', 500) !important;
}
.positive.strongPositive {
.igx-grid__td-text {
color: color(null, 'success', 500, .8) !important;
}
}
.negative {
color: color(null, 'error', 500) !important;
}
.negative.strongNegative {
.igx-grid__td-text {
color: color(null, 'success', 500, .8) !important;
}
}
.changePos {
.igx-grid__td-text {
background: color(null, 'success', 500, .5);
}
}
.changePos1 {
background: color(null, 'success', 500, .5);
color: contrast-color(null, 'gray', 900);
}
.changePos2 {
.igx-grid__td-text {
border-inline-end: rem(4px) solid color(null, 'success', 500, .5);
padding-inline-end: rem(15px);
}
}
.changeNeg {
.igx-grid__td-text {
background: color(null, 'error', 500, .5);
}
}
.changeNeg1 {
background: color(null, 'error', 500, .5);
color: contrast-color(null, 'gray', 900);
}
.changeNeg2 {
.igx-grid__td-text {
border-inline-end: rem(4px) solid color(null, 'error', 500, .5);
padding-inline-end: rem(9px);
}
}
.strongPositive {
.igx-grid__td-text {
background: color(null, 'success', 500);
}
}
.strongPositive1 {
background: color(null, 'success', 500);
color: contrast-color(null, 'gray', 900);
}
.strongPositive2 {
.igx-grid__td-text {
border-inline-end: rem(4px) solid color(null, 'success', 500);
padding-inline-end: rem(15px);
}
}
.strongNegative {
.igx-grid__td-text {
background: color(null, 'error', 500);
color: contrast-color(null, 'gray', 900);
}
}
.strongNegative1 {
background: color(null, 'error', 500);
color: contrast-color(null, 'gray', 900);
}
.strongNegative2 {
.igx-grid__td-text {
border-inline-end: rem(4px) solid color(null, 'error', 500);
padding-inline-end: rem(9px);
}
}
:host ::ng-deep {
.grid-area {
margin-block-start: 1rem;
overflow-y: hidden;
overflow-x: hidden;
width: 100%;
}
.igx-grid__td--column-selected.changePos1,
.igx-grid__td--column-selected.changePos2,
.igx-grid__td--column-selected.changePos {
background-color: color(null, 'success', 500) !important;
.finjs-icons,
.igx-grid__td-text {
color: contrast-color(null, 'gray', 900);;
}
}
.igx-grid__td--column-selected.changeNeg1,
.igx-grid__td--column-selected.changeNeg2,
.igx-grid__td--column-selected.changeNeg {
background-color: color(null, 'error', 500) !important;
.finjs-icons,
.igx-grid__td-text {
color: contrast-color(null, 'gray', 900);
}
}
.igx-grid__td--column-selected.strongPositive1,
.igx-grid__td--column-selected.strongPositive2,
.igx-grid__td--column-selected.strongPositive {
background-color: color(null, 'success', 500) !important;
.finjs-icons,
.igx-grid__td-text {
color: contrast-color(null, 'gray', 900);
}
}
.igx-grid__td--column-selected.strongNegative1,
.igx-grid__td--column-selected.strongNegative2,
.igx-grid__td--column-selected.strongNegative {
background-color: color(null, 'error', 500) !important;
.finjs-icons,
.igx-grid__td-text {
color: contrast-color(null, 'gray', 900);
}
}
}
scss
このサンプルが気に入りましたか? 完全な Ignite UI for Angularツールキットにアクセスして、すばやく独自のアプリの作成を開始します。無料でダウンロードできます。
SignalR サーバーの構成
ASP.NET Core アプリを作成する
LASP.NET Core SignalR アプリをセットアップする方法を見てみましょう。
Visual Studio の [ファイル] >> [新規作成] >> [プロジェクト] で、[ASP.NET Core Web アプリケーション] を選択し、セットアップに従います。構成上の問題が発生した場合は、Microsoft の公式ドキュメント チュートリアルに従ってください。

SignalR 構成のセットアップ
以下のコードを Startup.cs ファイルに追加します。
Configure
メソッドのエンドポイント部分。
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapHub<StreamHub>("/streamHub");
});
cs
- SignalR の使用法を
ConfigureServices
メソッドに追加します。
services.AddSignalR(options =>
{
options.EnableDetailedErrors = true;
});
cs
上記の変更により、SignalR が ASP.NET Core の依存関係挿入およびルーティング システムに追加されます。
それでは、追加の基本構成をセットアップしましょう。properties/launchSettings.json ファイルを開き、それに応じて変更します:
"profiles": {
"WebAPI": {
"commandName": "Project",
"launchBrowser": false,
"applicationUrl": "https://localhost:5001;http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
json
サーバー側のプロジェクトは localhost:5001
で実行され、クライアント側は localhost:4200
で実行されるため、これら 2 つの間の通信を確立するには、CORS を有効にする必要があります。Startup.cs クラスを開いて、変更してみましょう。
public void ConfigureServices(IServiceCollection services)
{
services.AddCors(options =>
{
options.AddPolicy("CorsPolicy", builder => builder
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials()
.WithOrigins("http://localhost:4200"));
});
...
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
...
app.UseCors("CorsPolicy");
...
cs
クロス オリジン リソース共有の有効化で特定の問題が発生した場合は、Microsoft の公式トピックを確認してください。
SignalR ハブのセットアップ
SignalR ハブとは何かを説明することから始めましょう。
SignalR ハブ API を使用すると、サーバーから接続されたクライアントのメソッドを呼び出すことができます。サーバー コードでは、クライアントによって呼び出されるメソッドを定義します。SignalR には、呼び出しと呼ばれるこの概念があります。実際には、特定のメソッドを使用してクライアントからハブを呼び出すことができます。クライアント コードでは、サーバーから呼び出されるメソッドを定義します。
実際のハブはサーバー側にあります。クライアントがいて、ハブがそれらすべての間にあると想像してください。ハブでメソッドを呼び出すことにより、Clients.All.doWork()
を使用してすべてのクライアントに何かを言うことができます。これは、接続されているすべてのクライアントに適用されます。また、特定のメソッドの呼び出し元である 1 つのクライアントとのみ通信できます。

接続、グループ、およびメッセージの管理を担当する基本 Hub クラスを継承する StreamHub クラスを作成しました。Hub クラスはステートレスであり、特定のメソッドの新しい呼び出しはそれぞれ、このクラスの新しいインスタンスにあることに注意してください。インスタンス プロパティに状態を保存することは無意味です。代わりに、静的プロパティを使用することをお勧めします。この場合、静的キー値ペアのコレクションを使用して、接続されている各クライアントのデータを保存します。
このクラスの他の便利なプロパティは、Clients、Context、および Groups です。これらは、一意の ConnectionID に基づいて特定の動作を管理するのに役立ちます。また、このクラスは次の便利なメソッドを提供します:
- OnConnectedAsync() - ハブとの新しい接続が確立されたときに呼び出されます。
- OnDisconnectedAsync(Exception) - ハブとの接続が終了したときに呼び出されます。
これにより、接続が確立または閉じられたときに追加ロジックを実行できます。このアプリケーションでは、Context connection ID を取得し、それを使用して特定の間隔でデータを送り返す UpdateParameters メソッドも追加しました。ご覧のとおり、他のクライアントからのストリーミング介入を防ぐ一意の ConnectionID を介して通信します。
public async void UpdateParameters(int interval, int volume, bool live = false, bool updateAll = true)
{
...
var connection = Context.ConnectionId;
var clients = Clients;
...
if (!clientConnections.ContainsKey(connection))
{
clientConnections.Add(connection, new TimerManager(async() =>
{
...
await Send(newDataArray, client, connection);
}, interval));
} else
{
clientConnections[connection].Stop();
clientConnections[connection] = new TimerManager(async () =>
{
var client = clients.Client(connection);
..
await Send(newDataArray, client, connection);
}, interval);
}
...
}
cs
データの準備ができたら、SendAsync
メソッドを使用して transferdata
イベントを発行してデータを転送します。
public async Task Send(FinancialData[] array, IClientProxy client, string connection)
{
await client.SendAsync("transferdata", array);
}
...
public override Task OnDisconnectedAsync(Exception exception)
{
StopTimer();
clientConnections.Remove(Context.ConnectionId);
return base.OnDisconnectedAsync(exception);
}
cs
クライアント アプリケーションは、登録されたイベントをリスニングします。
private registerSignalEvents() {
this.hubConnection.onclose(() => {
this.hasRemoteConnection = false;
});
this.hubConnection.on('transferdata', (data) => {
this.data.next(data);
})
}
ts
ASP.NET Core アプリケーションの公開な GitHub リポジトリはこちらにあります。
SignalR クライアント ライブラリを作成する
SignalR サービスを利用するために Angular プロジェクトを作成します。
実際のアプリケーションを含む Github リポジトリはこちらにあります。
まず、SignalR をインストールすることから始めます:
npm install @microsoft/signalr
coffeescript
サーバーに向けて HTTP リクエストを送信するため、HttpClientModule も必要であることに注意してください。
以下に、ハブ接続ビルダーを処理する signal-r.service.ts ファイルを示します。
export class SignalRService implements OnDestroy {
public data: BehaviorSubject<any[]>;
public hasRemoteConnection: boolean;
private hubConnection: signalR.HubConnection;
...
constructor(private zone: NgZone, private http: HttpClient) {
this.data = new BehaviorSubject([]);
}
...
public startConnection = (interval = 500, volume = 1000, live = false, updateAll = true) => {
this.hubConnection = new signalR.HubConnectionBuilder()
.configureLogging(signalR.LogLevel.Trace)
.withUrl('https://www.infragistics.com/angular-apis/webapi/streamHub')
.build();
this.hubConnection
.start()
.then(() => {
...
this.registerSignalEvents();
this.broadcastParams(interval, volume, live, updateAll);
})
.catch(() => { ... });
}
public broadcastParams = (frequency, volume, live, updateAll = true) => {
this.hubConnection.invoke('updateparameters', frequency, volume, live, updateAll)
.then(() => console.log('requestLiveData', volume))
.catch(err => {
console.error(err);
});
}
private registerSignalEvents() {
this.hubConnection.onclose(() => {
this.hasRemoteConnection = false;
});
this.hubConnection.on('transferdata', (data) => {
this.data.next(data);
});
}
...
ts
app.component で、新しく作成された startConnection
メソッドを使用します。
constructor(public dataService: SignalRService) {}
public ngOnInit() {
this.dataService.startConnection(this.frequency, this.dataVolume, true, false);
}
...
ts
グリッドのデータ バインディング
これまでクライアント コードで見てきたように、transferdata
イベントのリスナーを設定します。このイベントは、更新されたデータ配列を引数として受け取ります。新しく受信したデータをグリッドに渡すために、オブザーバブルを使用します。これを設定するには、グリッドのデータ ソースを次のようにデータ オブザーバブルにバインドする必要があります。
<igx-grid [data]='data | async'> ... </igx-grid>
html
サーバーからクライアントに新しいデータを受信するたびに、データ オブザーバブルの next()
メソッドを呼び出します。
this.hubConnection.on('transferdata', (data) => {
this.data.next(data);
})
ts
トピックの重要ポイント
アプリケーションを更新するのではなく、データがいつ更新されるかを確認するだけの場合は、ASP.NET Core SignalR を検討してください。データが大きいと思う場合、または無限のスピンを表示してクライアントをブロックせずにスムーズなユーザー エクスペリエンスが必要な場合は、ストリーミング コンテンツを使用することを強くお勧めします。
SignalR Hub 通信の使用は簡単で直感的であり、Angular Observables を使用すると、WebSockets でデータ ストリーミングを使用する強力なアプリケーションを作成できます。