<div class="tables">
@for(dataSource of dataSources; track dataSource) {
<div class="column">
<dx-data-grid
[dataSource]="dataSource"
[height]="600"
[showBorders]="true"
[repaintChangesOnly]="true"
[highlightChanges]="true"
>
<dxo-data-grid-paging [enabled]="false"> </dxo-data-grid-paging>
<dxo-data-grid-editing
mode="cell"
refreshMode="reshape"
[allowUpdating]="true"
[allowDeleting]="true"
[allowAdding]="true"
[useIcons]="true"
>
</dxo-data-grid-editing>
<dxi-data-grid-column dataField="Prefix" caption="Title" [width]="50">
<dxi-data-grid-validation-rule type="required">
</dxi-data-grid-validation-rule>
</dxi-data-grid-column>
<dxi-data-grid-column dataField="FirstName">
<dxi-data-grid-validation-rule type="required">
</dxi-data-grid-validation-rule>
</dxi-data-grid-column>
<dxi-data-grid-column dataField="StateID" caption="State">
<dxi-data-grid-validation-rule type="required">
</dxi-data-grid-validation-rule>
<dxo-data-grid-lookup
[dataSource]="stateDataSource"
displayExpr="Name"
valueExpr="ID"
>
</dxo-data-grid-lookup>
</dxi-data-grid-column>
<dxi-data-grid-column dataField="BirthDate" dataType="date">
<dxi-data-grid-validation-rule
type="range"
[max]="maxDate"
message="Date can not be greater than 01/01/3000"
>
</dxi-data-grid-validation-rule>
</dxi-data-grid-column>
</dx-data-grid>
</div>
}
</div>
import { bootstrapApplication } from '@angular/platform-browser';
import { Component, enableProdMode, provideZoneChangeDetection } from '@angular/core';
import { DxDataGridModule, DxTextBoxModule } from 'devextreme-angular';
import { CustomStore } from 'devextreme-angular/common/data';
import { CollaborativeEditingService } from './app.service';
if (!/localhost/.test(document.location.host)) {
enableProdMode();
}
@Component({
selector: 'demo-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
preserveWhitespaces: true,
providers: [CollaborativeEditingService],
imports: [
DxDataGridModule,
DxTextBoxModule,
],
})
export class AppComponent {
dataSources: CustomStore[];
stateDataSource: CustomStore;
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));
});
}
}
bootstrapApplication(AppComponent, {
providers: [
provideZoneChangeDetection({ eventCoalescing: true, runCoalescing: true }),
],
});
.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';
import 'anti-forgery';
const BASE_PATH = 'https://js.devexpress.com/Demos/NetCore/';
class StoreChangedEvent {
type: 'update' | 'insert' | 'remove';
key?: string;
data?: Record<string, unknown>;
}
@Injectable()
export class CollaborativeEditingService {
private storeChanged = new Subject<StoreChangedEvent[]>();
private groupId: string = new Guid().toString();
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) => {
ajaxSettings.data.groupId = this.groupId;
},
});
}
private initHubConnection() {
const connection = this.createConnection();
connection.start()
.then(() => {
connection.on('update', (key, data) => {
this.storeChanged.next([{ type: 'update', key, data }]);
});
connection.on('insert', (data) => {
this.storeChanged.next([{ type: 'insert', data }]);
});
connection.on('remove', (key) => {
this.storeChanged.next([{ type: 'remove', key }]);
});
});
}
private createConnection() {
const hubUrl = `${BASE_PATH}dataGridCollaborativeEditingHub?GroupId=${this.groupId}`;
return new HubConnectionBuilder()
.withUrl(hubUrl, {
skipNegotiation: true,
transport: HttpTransportType.WebSockets,
})
.build();
}
}
<!DOCTYPE html>
<html lang="en">
<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=5.0" />
<link rel="stylesheet" type="text/css" href="https://cdn3.devexpress.com/jslib/26.1.5/css/dx.light.css" />
</head>
<body class="dx-viewport">
<div class="demo-container">
<demo-app>Loading...</demo-app>
</div>
<script src="https://cdn.jsdelivr.net/npm/zone.js@0.16.2/bundles/zone.umd.js"></script>
<script src="./bundle.js" type="module"></script>
</body>
</html>
using DevExtreme.AspNet.Data;
using DevExtreme.AspNet.Mvc;
using DevExtreme.MVC.Demos.Hubs;
using DevExtreme.MVC.Demos.Models.DataGrid;
using DevExtreme.MVC.Demos.Models.SampleData;
using Microsoft.AspNet.SignalR;
using Newtonsoft.Json;
using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Formatting;
using System.Web.Http;
using DevExpress.Data.Utils;
namespace DevExtreme.MVC.Demos.Controllers.ApiControllers {
public class DataGridCollaborativeEditingController : ApiController {
static readonly NonCryptographicRandom random = NonCryptographicRandom.System;
IHubContext hubContext;
public DataGridCollaborativeEditingController() {
hubContext = GlobalHost.ConnectionManager.GetHubContext<DataGridCollaborativeEditingHub>();
}
[HttpGet]
public object Get(DataSourceLoadOptions loadOptions) {
return DataSourceLoader.Load(SampleData.DataGridEmployees, loadOptions);
}
[HttpPost]
public HttpResponseMessage Post(FormDataCollection form) {
var values = form.Get("values");
var groupId = form.Get("groupId");
var newEmployee = new Employee();
JsonConvert.PopulateObject(values, newEmployee);
newEmployee.ID = random.Next(int.MaxValue);
Validate(newEmployee);
if(!ModelState.IsValid) {
return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState.GetFullErrorMessage());
}
// db.Employees.Add(newEmployee);
// db.SaveChanges();
hubContext.Clients.Group(groupId).insert(newEmployee);
return Request.CreateResponse(newEmployee);
}
[HttpPut]
public HttpResponseMessage Put(FormDataCollection form) {
var key = Convert.ToInt32(form.Get("key"));
var values = form.Get("values");
var groupId = form.Get("groupId");
var employee = new Employee();
JsonConvert.PopulateObject(values, employee);
employee.ID = key;
Validate(employee);
if(!ModelState.IsValid) {
return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState.GetFullErrorMessage());
}
// db.SaveChanges();
hubContext.Clients.Group(groupId).update(key, employee);
return Request.CreateResponse(employee);
}
[HttpDelete]
public void Delete(FormDataCollection form) {
var key = Convert.ToInt32(form.Get("key"));
var groupId = form.Get("groupId");
// var employee = db.Employees.First(a => a.ID == key);
// db.Employees.Remove(employee);
// db.SaveChanges();
hubContext.Clients.Group(groupId).remove(key);
}
}
}
using Microsoft.AspNet.SignalR;
using System.Threading.Tasks;
namespace DevExtreme.MVC.Demos.Hubs {
public class DataGridCollaborativeEditingHub : Hub {
public override Task OnConnected() {
var groupId = Context.QueryString["GroupId"];
Groups.Add(Context.ConnectionId, groupId);
return base.OnConnected();
}
}
}