Aplicación web en tiempo real con ASP.NET Core SignalR
En este tema, veremos cómo crear aplicaciones para la transmisión y la recepción de datos con ASP.NET Core SignalR .
Lo que necesitarás:
Un conocimiento básico de ASP.NET Core y Angular.
.NET Core 3.1 instalado e IDE como Visual Studio.
Lo que sabrá al final de este artículo:
Cómo agregar y usar SignalR.
Cómo abrir la conexión del Cliente y utilizar el concepto de invocación de método para transmitir datos por Cliente.
Cómo consumir el servicio SignalR con la aplicación Angular mediante Observables.
SignalR aprovecha varios transportes y selecciona automáticamente el mejor transporte disponible según las capacidades del cliente y del servidor: WebSockets, Server Send Events o Long-polling .
Cuando hablamos en términos de WebSockets (excluyendo SSE y Long-polling de la ecuación) cuando el cliente está conectado en tiempo real al servidor, cada vez que sucede algo, el servidor sabrá enviar un mensaje a través de ese WebSocket al cliente. Con clientes y servidores de la vieja escuela, se utilizaría el transporte de sondeo largo.
Así es como SignalR maneja clientes y servidores modernos, utiliza WebSockets internamente cuando está disponible y recurre elegantemente a otras técnicas y tecnologías cuando no lo está:
Es como un apretón de manos, el Cliente y el Servidor acuerdan qué usar y lo usan. Esto se llama proceso de negociación .
Ejemplo de señalR
El propósito de esta demostración es mostrar un tablero de pantalla financiera con un flujo de datos en tiempo real utilizando ASP.NET Core SignalR .
import { AfterViewInit, ChangeDetectorRef, Component, ComponentFactoryResolver, ElementRef, Renderer2, OnDestroy, OnInit, DoCheck, TemplateRef, ViewChild, ViewContainerRef, ViewEncapsulation, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core' ;
import { AbsoluteScrollStrategy, ConnectedPositioningStrategy, DefaultSortingStrategy, GridColumnDataType, IgxColumnComponent, IgxGridComponent, IgxOverlayOutletDirective, IgxSelectComponent, OverlaySettings, SortingDirection, IgxSwitchComponent, IgxLabelDirective, IgxPrefixDirective, IgxIconComponent, IgxSelectItemComponent, IgxButtonDirective, IgxCellTemplateDirective, IgxPaginatorComponent } 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' ;
import { FormsModule } from '@angular/forms' ;
import { NgFor, NgIf, AsyncPipe, CurrencyPipe } from '@angular/common' ;
@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' ],
imports : [IgxSwitchComponent, FormsModule, IgxSelectComponent, IgxLabelDirective, IgxPrefixDirective, IgxIconComponent, NgFor, IgxSelectItemComponent, IgxButtonDirective, IgxOverlayOutletDirective, IgxGridComponent, IgxColumnComponent, IgxCellTemplateDirective, NgIf, IgxPaginatorComponent, GridHostDirective, AsyncPipe, CurrencyPipe],
schemas : [CUSTOM_ELEMENTS_SCHEMA]
})
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' ;
this .cdr.detectChanges();
}
}
ts コピー <igc-dockmanager #dock class ="dock-m-position ig-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" [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" [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" [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" [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 ]="isDarkTheme ? 'dark-theme' : 'light-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 );
}
}
igx-grid {
--ig-size: var(--ig-size-small);
.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 );
}
}
}
igx-select {
--ig-size: var(--ig-size-small);
}
scss コピー
¿Te gusta esta muestra? Obtenga acceso a nuestro kit de herramientas de Ignite UI for Angular completo y comience a crear sus propias aplicaciones en minutos. Descárgalo gratis.
Configuración del servidor SignalR
Crear ASP.NET Core aplicación
Veamos cómo configurar la aplicación SignalR ASP.NET Core. En Visual Studio, desde Archivo >> Nuevo proyecto , elija ASP.NET Core aplicación web y siga la configuración. No dude en seguir el tutorial de documentación oficial de Microsoft si experimenta alguna dificultad de configuración.
Configuración de SignalR
Agregue el siguiente código al archivo Startup.cs :
Parte del punto final del método Configure
.
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapHub<StreamHub>("/streamHub" );
});
cs
Agregue el uso de SignalR al método ConfigureServices
.
services.AddSignalR(options =>
{
options.EnableDetailedErrors = true ;
});
cs
Los cambios anteriores agregan SignalR al sistema de enrutamiento e inserción de dependencias ASP.NET Core.
Ahora, configuremos una configuración básica adicional. Abra el archivo properties/launchSettings.json y modifíquelo en consecuencia:
"profiles" : {
"WebAPI" : {
"commandName" : "Project" ,
"launchBrowser" : false ,
"applicationUrl" : "https://localhost:5001;http://localhost:5000" ,
"environmentVariables" : {
"ASPNETCORE_ENVIRONMENT" : "Development"
}
}
}
json
Nuestro proyecto del lado del servidor se ejecutará en localhost:5001
y el lado del cliente se ejecutará en localhost:4200
, por lo que para establecer comunicación entre esos dos, debemos habilitar CORS. Abramos la clase Startup.cs y modifíquela:
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
Si experimenta un problema específico al habilitar el uso compartido de recursos entre orígenes, consulte el tema oficial de Microsoft .
Configuración del concentrador SignalR
Comencemos explicando qué es un concentrador SignalR. La API de SignalR Hub le permite llamar a métodos en clientes conectados desde el servidor. En el código del servidor, usted define los métodos que llama el cliente. En SignalR existe este concepto llamado Invocación : en realidad, puedes llamar al centro desde el cliente con un método particular. En el código del cliente, usted define métodos que se llaman desde el servidor.
El centro real se encuentra en el lado del servidor. Imagina que tienes Clientes y el Hub está entre todos ellos. Puede decirle algo a todos los Clientes con Clients.All.doWork()
invocando un método en el centro. Esto irá a todos los clientes conectados. Además, puede comunicarse con un solo cliente, que es el que llama, porque es el que llama de ese método en particular.
Hemos creado una clase StreamHub que hereda la clase base Hub, que es responsable de administrar conexiones, grupos y mensajería. Es bueno tener en cuenta que la clase Hub no tiene estado y cada nueva invocación de un determinado método se realiza en una nueva instancia de esta clase. Es inútil guardar el estado en las propiedades de la instancia; más bien sugerimos usar propiedades estáticas; en nuestro caso usamos una colección estática de pares clave-valor para almacenar datos para cada cliente conectado.
Otras propiedades útiles de esta clase son Clientes , Contexto y Grupos . Pueden ayudarle a gestionar cierto comportamiento basándose en el ConnectionID único. Además, esta clase le proporciona los siguientes métodos útiles:
OnConnectedAsync(): se llama cuando se establece una nueva conexión con el concentrador.
OnDisconnectedAsync (Excepción): se llama cuando finaliza una conexión con el concentrador.
Nos permiten realizar cualquier lógica adicional cuando se establece o cierra una conexión. En nuestra aplicación, también agregamos el método UpdateParameters que obtiene una ID de conexión de contexto y la usa para enviar datos en un intervalo determinado. Como puede ver, nos comunicamos a través de un ConnectionID único que evita una intervención de transmisión por parte de otros Clientes.
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
Cuando los datos están listos, los transferimos emitiendo un evento transferdata
con la ayuda del método SendAsync
.
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
Nuestra aplicación cliente estaría escuchando los eventos registrados:
private registerSignalEvents ( ) {
this .hubConnection.onclose(() => {
this .hasRemoteConnection = false ;
});
this .hubConnection.on('transferdata' , (data ) => {
this .data.next(data);
})
}
ts
El repositorio público de GitHub de la aplicación ASP.NET Core se puede encontrar aquí .
Crear biblioteca cliente SignalR
Crearemos un proyecto Angular para poder consumir el servicio SignalR. El repositorio de Github con la aplicación real se puede encontrar aquí .
First, start by installing SignalR:
npm install @microsoft/signalr
coffeescript
Tenga en cuenta que enviaremos la solicitud HTTP a nuestro servidor, por lo que también necesitamos HttpClientModule.
A continuación encontrará el archivo signal-r.service.ts que maneja el generador de conexiones del concentrador.
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://es.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
En su app.component agregue use el método startConnection
recién creado
constructor (public dataService: SignalRService ) {}
public ngOnInit ( ) {
this .dataService.startConnection(this .frequency, this .dataVolume, true , false );
}
...
ts
Enlace de datos de cuadrícula
Como hemos visto hasta ahora en nuestro código de cliente, configuramos un detector para el evento transferdata
, que recibe como argumento la matriz de datos actualizada. Para pasar los datos recién recibidos a nuestra grilla utilizamos un observable. Para configurar eso, necesitamos vincular la fuente de datos de la cuadrícula a los datos observables de esta manera:
<igx-grid [data ]='data | async' > ... </igx-grid >
html
Cada vez que se reciben nuevos datos del servidor al cliente, llamamos al método next()
de los datos observables.
this .hubConnection.on('transferdata' , (data ) => {
this .data.next(data);
})
ts
Conclusiones del tema
Si no desea actualizar la aplicación, sino simplemente ver cuándo se actualizan los datos, debe considerar ASP.NET Core SignalR. Definitivamente recomiendo optar por la transmisión de contenido cuando crea que sus datos son grandes, o si desea una experiencia de usuario fluida sin bloquear el cliente mostrando giros interminables.
Usar la comunicación de SignalR Hub es fácil e intuitivo y con la ayuda de Angular Observables, puedes crear una aplicación poderosa que utilice la transmisión de datos con WebSockets.