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 widget are broadcasted to the other widget via the SignalR service.

To implement this functionality:

  1. Generate a session ID used to identify widgets 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 widget.
  4. Create widgets 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
<template> <div class="tables"> <Grid :data-source="store1" class="column" /> <Grid :data-source="store2" class="column" /> </div> </template> <script> import { HubConnectionBuilder, HttpTransportType } from '@aspnet/signalr'; import * as AspNetData from 'devextreme-aspnet-data-nojquery'; import Grid from './Grid.vue'; import Guid from 'devextreme/core/guid'; const BASE_PATH = ''; const url = `${BASE_PATH }api/DataGridCollaborativeEditing/`; const groupId = new Guid().toJSON(); const createStore = () => AspNetData.createStore({ key: 'ID', loadUrl: url, insertUrl:url, updateUrl:url, deleteUrl:url, onBeforeSend: function(operation, ajaxSettings) { = groupId; } }); const store1 = createStore(); const store2 = createStore(); const updateStores = (events) => { store1.push(events); store2.push(events); }; export default { components: { Grid }, data() { return { store1, store2 }; } }; const hubUrl = `${BASE_PATH}dataGridCollaborativeEditingHub?GroupId=${groupId}`; const connection = new HubConnectionBuilder() .withUrl(hubUrl, { skipNegotiation: true, transport: HttpTransportType.WebSockets }) .build(); connection.start() .then(function() { connection.on('update', function(key, data) { updateStores([{ type: 'update', key: key, data: data }]); }); connection.on('insert', function(data) { updateStores([{ type: 'insert', data: data }]); }); connection.on('remove', function(key) { updateStores([{ type: 'remove', key: key }]); }); }); </script> <style scoped> .tables { display: flex; } .column:first-child { width: 50%; padding-right: 15px; } .column:last-child { width: 50%; padding-left: 15px; } </style>
<template> <DxDataGrid :data-source="dataSource" :height="600" :show-borders="true" :repaint-changes-only="true" :highlight-changes="true" > <DxPaging :enabled="false" /> <DxEditing :allow-deleting="true" :allow-updating="true" :allow-adding="true" :use-icons="true" mode="cell" refresh-mode="reshape" /> <DxColumn :width="50" data-field="Prefix" caption="Title" > <DxRequiredRule/> </DxColumn> <DxColumn data-field="FirstName" > <DxRequiredRule/> </DxColumn> <DxColumn :lookup="lookup" data-field="StateID" caption="State" > <DxRequiredRule/> </DxColumn> <DxColumn data-field="BirthDate" data-type="date" > <DxRangeRule :max="maxDate" message="Date can not be greater than 01/01/3000" /> </DxColumn> </DxDataGrid> </template> <script> import { DxDataGrid, DxColumn, DxPaging, DxEditing, DxRequiredRule, DxRangeRule } from 'devextreme-vue/data-grid'; import * as AspNetData from 'devextreme-aspnet-data-nojquery'; const statesStore = AspNetData.createStore({ key: 'ID', loadUrl: '' }); export default { components: { DxDataGrid, DxColumn, DxPaging, DxEditing, DxRequiredRule, DxRangeRule }, props: { dataSource: { type: Object, default: ()=>({}) } }, data() { return { maxDate: new Date(3000, 0), lookup: { dataSource: statesStore, displayExpr: 'Name', valueExpr: 'ID' } }; } }; </script>
import Vue from 'vue'; import App from './App.vue'; new Vue({ el: '#app', components: { App }, template: '<App/>' });
<!DOCTYPE html> <html> <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 type="text/javascript" src="config.js"></script> <script type="text/javascript"> System.import('./index.js'); </script> </head> <body class="dx-viewport"> <div class="demo-container"> <div id="app"> </div> </div> </body> </html>
System.config({ transpiler: 'plugin-babel', paths: { 'npm:': '' }, map: { vue: 'npm:vue@2.6.3/dist/vue.esm.browser.js', 'vue-loader': 'npm:dx-systemjs-vue-browser@latest/index.js', 'devextreme': 'npm:devextreme@19.2', 'devextreme-vue': 'npm:devextreme-vue@19.2', jszip: 'npm:jszip@3.1.3/dist/jszip.min.js', 'quill': 'npm:quill@1.3.7/dist/quill.js', 'devexpress-diagram': 'npm:devexpress-diagram', 'devexpress-gantt': 'npm:devexpress-gantt', 'devextreme-aspnet-data-nojquery': 'npm:devextreme-aspnet-data-nojquery@2.5.1', 'plugin-babel': 'npm:systemjs-plugin-babel@0/plugin-babel.js', 'systemjs-babel-build': 'npm:systemjs-plugin-babel@0/systemjs-babel-browser.js', 'tslib': 'npm:tslib@1.6.1', '@aspnet/signalr': 'npm:@aspnet/signalr@1.0.0/dist/cjs/index.js' }, meta: { '*.vue': { loader: 'vue-loader' }, 'devextreme-aspnet-data-nojquery': { 'esModule': true } }, packages: { 'devextreme-vue': { main: 'index.js' }, 'devextreme': { defaultExtension: 'js' } }, babelOptions: { sourceMaps: false, stage0: true } });