Your search did not match any results.
Data Grid

Collaborative Editing


Multiple users can edit the DataGrid's data in real-time. In this demo, changes made in one DataGrid are broadcasted to the other DataGrid via the SignalR service.

To implement this functionality:

  1. Generate a session ID used to identify DataGrids that should be edited simultaneously (groupId in this demo).

  2. Configure CustomStores. In this demo, we use the createStore method (part of the extension). The onBeforeSend function is used to send the session ID from step 1 to the server.

  3. Create store instances—one per DataGrid.

  4. Create DataGrids and bind them to the store instances.

  5. Update all the store instances when a push notification is received (see the updateStores function).

Changes made collaboratively are lost if you refresh the page because the SignalR service broadcasts changes without saving them.

Copy to CodeSandBox
<div class="tables"> <div *ngFor="let dataSource of dataSources" class="column"> <dx-data-grid [dataSource]="dataSource" [height]="600" [showBorders]="true" [repaintChangesOnly]="true" [highlightChanges]="true"> <dxo-paging [enabled]="false"> </dxo-paging> <dxo-editing mode="cell" refreshMode="reshape" [allowUpdating]="true" [allowDeleting]="true" [allowAdding]="true" [useIcons]="true"> </dxo-editing> <dxi-column dataField="Prefix" caption="Title" [width]="50"> <dxi-validation-rule type="required"> </dxi-validation-rule> </dxi-column> <dxi-column dataField="FirstName"> <dxi-validation-rule type="required"> </dxi-validation-rule> </dxi-column> <dxi-column dataField="StateID" caption="State"> <dxi-validation-rule type="required"> </dxi-validation-rule> <dxo-lookup [dataSource]="stateDataSource" displayExpr="Name" valueExpr="ID"> </dxo-lookup> </dxi-column> <dxi-column dataField="BirthDate" dataType="date"> <dxi-validation-rule type="range" [max]="maxDate" message="Date can not be greater than 01/01/3000"> </dxi-validation-rule> </dxi-column> </dx-data-grid> </div> </div>
import { NgModule, Component, enableProdMode } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { DxDataGridModule, DxTextBoxModule } from 'devextreme-angular'; import { CollaborativeEditingService } from './app.service'; if(!/localhost/.test( { enableProdMode(); } @Component({ selector: 'demo-app', templateUrl: 'app/app.component.html', styleUrls: ['app/app.component.css'], preserveWhitespaces: true, providers: [CollaborativeEditingService] }) export class AppComponent { dataSources: Array<Object>; stateDataSource: Object; maxDate: Date = new Date(3000, 0); constructor(private service: CollaborativeEditingService) { this.dataSources = [this.service.createStore(), this.service.createStore()]; this.stateDataSource = this.service.createStatesStore(); this.service.getStoreChangedObservable().subscribe(events => { this.dataSources.forEach(store => store.push(events)); }); } } @NgModule({ imports: [ BrowserModule, DxDataGridModule, DxTextBoxModule ], declarations: [AppComponent], bootstrap: [AppComponent] }) export class AppModule { } platformBrowserDynamic().bootstrapModule(AppModule);
.tables { display: flex; } .column:first-child { width: 50%; padding-right: 15px; } .column:last-child { width: 50%; padding-left: 15px; }
import { Injectable } from '@angular/core'; import { HubConnectionBuilder, HttpTransportType } from '@aspnet/signalr'; import * as AspNetData from 'devextreme-aspnet-data-nojquery'; import Guid from 'devextreme/core/guid'; import { Subject } from 'rxjs'; const BASE_PATH = ''; @Injectable() export class CollaborativeEditingService { private storeChanged = new Subject<Array<Object>>(); private groupId: string = new Guid().toJSON(); private connection: Object; getStoreChangedObservable() { this.connection || this.initHubConnection(); return this.storeChanged.asObservable(); } createStatesStore() { return AspNetData.createStore({ key: 'ID', loadUrl: BASE_PATH + 'api/DataGridStatesLookup' }) } createStore() { const url = BASE_PATH + 'api/DataGridCollaborativeEditing'; return AspNetData.createStore({ key: 'ID', loadUrl: url, insertUrl: url, updateUrl: url, deleteUrl: url, onBeforeSend: (operation, ajaxSettings) => { = this.groupId; } }) } private initHubConnection() { const connection = this.createConnection(); connection.start() .then(() => { connection.on('update', (key, data) => {[{ type: 'update', key: key, data: data }]); }); connection.on('insert', data => {[{ type: 'insert', data: data }]); }); connection.on('remove', key => {[{ type: 'remove', key: key }]); }); }); } private createConnection() { const hubUrl = `${BASE_PATH}dataGridCollaborativeEditingHub?GroupId=${this.groupId}`; return new HubConnectionBuilder() .withUrl(hubUrl, { skipNegotiation: true, transport: HttpTransportType.WebSockets }) .build(); } }
// In real applications, you should not transpile code in the browser. You can see how to create your own application with Angular and DevExtreme here: // System.config({ transpiler: 'ts', typescriptOptions: { module: "system", emitDecoratorMetadata: true, experimentalDecorators: true }, meta: { 'typescript': { "exports": "ts" }, 'devextreme-aspnet-data-nojquery': { "esModule": true }, 'devextreme/localization.js': { "esModule": true } }, paths: { 'npm:': '' }, map: { 'ts': 'npm:plugin-typescript@8.0.0/lib/plugin.js', 'typescript': 'npm:typescript@3.5.3/lib/typescript.js', '@angular': 'npm:@angular', 'tslib': 'npm:tslib@2.2.0/tslib.js', 'rxjs': 'npm:rxjs@6.4.0', '@aspnet/signalr': 'npm:@aspnet/signalr@1.0.0/dist/cjs/index.js', 'devextreme-aspnet-data-nojquery': 'npm:devextreme-aspnet-data-nojquery@2.7.1/index.js', 'rrule': 'npm:rrule@2.6.6/dist/es5/rrule.js', 'luxon': 'npm:luxon@1.26.0/build/global/luxon.min.js', 'es6-object-assign': 'npm:es6-object-assign@1.1.0', 'devextreme': 'npm:devextreme@20.2.7', 'jszip': 'npm:jszip@3.6.0/dist/jszip.min.js', 'devextreme-quill': 'npm:devextreme-quill@0.10.3/dist/dx-quill.min.js', 'devexpress-diagram': 'npm:devexpress-diagram@2.0.23', 'devexpress-gantt': 'npm:devexpress-gantt@2.0.29', 'devextreme-angular': 'npm:devextreme-angular@20.2.6', 'preact': 'npm:preact@10.5.13/dist/preact.js', 'preact/hooks': 'npm:preact@10.5.13/hooks/dist/hooks.js' }, packages: { 'app': { main: './app.component.ts', defaultExtension: 'ts' }, 'devextreme': { defaultExtension: 'js' }, 'devextreme/events/utils': { main: 'index' }, 'devextreme/events': { main: 'index' }, 'es6-object-assign': { main: './index.js', defaultExtension: 'js' } }, packageConfigPaths: [ "npm:@angular/*/package.json", "npm:@angular/common/*/package.json", "npm:rxjs/package.json", "npm:rxjs/operators/package.json", "npm:devextreme-angular/*/package.json", "npm:devextreme-angular/ui/*/package.json", "npm:devextreme-angular/package.json", "npm:devexpress-diagram/package.json", "npm:devexpress-gantt/package.json", "npm:@aspnet/*/package.json", ] });
<!DOCTYPE html> <html xmlns=""> <head> <title>DevExtreme Demo</title> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" /> <link rel="stylesheet" type="text/css" href="" /> <link rel="stylesheet" type="text/css" href="" /> <script src=""></script> <script src=""></script> <script src=""></script> <script src=""></script> <script src="config.js"></script> <script> System.import('app').catch(console.error.bind(console)); </script> </head> <body class="dx-viewport"> <div class="demo-container"> <demo-app>Loading...</demo-app> </div> </body> </html>