DevExtreme v23.2 is now available.

Explore our newest features/capabilities and share your thoughts with us.

Your search did not match any results.

SignalR Service

This demo shows how you can use a SignalR service to synchronize appointments across different devices. To emulate such a setup, each Scheduler on this page reads data from its own separate data store. Changes made in one control are repeated in the other and persist until the browser session has expired.

Follow the steps below to implement this functionality. Note again that this demo repeats all steps twice for the two Schedulers. Your project will have a single control and a single store.

  1. Configure a CustomStore. In this demo, we use the createStore method (part of the DevExtreme.AspNet.data extension).

  2. Create the Scheduler and use its dataSource property to bind it to the store instance.

  3. When a push notification is received, call the store's push(changes) method to update the store's data (see the connection.on event handlers).

For server-side configuration, refer to the ASP.NET MVC version of this demo.

For more information about integration with push services, refer to the following help topic: Integration with Push Services.

Backend API
import React from 'react'; import Scheduler, { SchedulerTypes } from 'devextreme-react/scheduler'; import * as AspNetData from 'devextreme-aspnet-data-nojquery'; import { HubConnectionBuilder, HttpTransportType } from '@aspnet/signalr'; const BASE_PATH = 'https://js.devexpress.com/Demos/NetCore/'; const url = `${BASE_PATH}api/SchedulerSignalR`; const createStore = () => AspNetData.createStore({ key: 'AppointmentId', loadUrl: url, insertUrl: url, updateUrl: url, deleteUrl: url, onBeforeSend(method, ajaxOptions) { ajaxOptions.xhrFields = { withCredentials: true }; }, }); const store1 = createStore(); const store2 = createStore(); const currentDate = new Date(2021, 3, 27); const views: SchedulerTypes.ViewType[] = ['day', 'workWeek']; const connection = new HubConnectionBuilder() .withUrl(`${BASE_PATH}schedulerSignalRHub`, { skipNegotiation: true, transport: HttpTransportType.WebSockets, }) .build(); connection .start() .then(() => { connection.on('update', (key, data) => { store1.push([{ type: 'update', key, data }]); store2.push([{ type: 'update', key, data }]); }); connection.on('insert', (data) => { store1.push([{ type: 'insert', data }]); store2.push([{ type: 'insert', data }]); }); connection.on('remove', (key) => { store1.push([{ type: 'remove', key }]); store2.push([{ type: 'remove', key }]); }); }); const App = () => ( <div className="schedulers"> <div className="column-1"> <Scheduler timeZone="America/Los_Angeles" dataSource={store1} views={views} defaultCurrentView="day" defaultCurrentDate={currentDate} height={600} startDayHour={9} endDayHour={19} remoteFiltering={true} dateSerializationFormat="yyyy-MM-ddTHH:mm:ssZ" textExpr="Text" startDateExpr="StartDate" descriptionExpr="Description" endDateExpr="EndDate" allDayExpr="AllDay" /> </div> <div className="column-2"> <Scheduler timeZone="America/Los_Angeles" dataSource={store2} views={views} defaultCurrentView="day" defaultCurrentDate={currentDate} height={600} startDayHour={9} endDayHour={19} remoteFiltering={true} dateSerializationFormat="yyyy-MM-ddTHH:mm:ssZ" textExpr="Text" startDateExpr="StartDate" endDateExpr="EndDate" allDayExpr="AllDay" /> </div> </div> ); export default App;
import React from 'react'; import Scheduler from 'devextreme-react/scheduler'; import * as AspNetData from 'devextreme-aspnet-data-nojquery'; import { HubConnectionBuilder, HttpTransportType } from '@aspnet/signalr'; const BASE_PATH = 'https://js.devexpress.com/Demos/NetCore/'; const url = `${BASE_PATH}api/SchedulerSignalR`; const createStore = () => AspNetData.createStore({ key: 'AppointmentId', loadUrl: url, insertUrl: url, updateUrl: url, deleteUrl: url, onBeforeSend(method, ajaxOptions) { ajaxOptions.xhrFields = { withCredentials: true }; }, }); const store1 = createStore(); const store2 = createStore(); const currentDate = new Date(2021, 3, 27); const views = ['day', 'workWeek']; const connection = new HubConnectionBuilder() .withUrl(`${BASE_PATH}schedulerSignalRHub`, { skipNegotiation: true, transport: HttpTransportType.WebSockets, }) .build(); connection.start().then(() => { connection.on('update', (key, data) => { store1.push([{ type: 'update', key, data }]); store2.push([{ type: 'update', key, data }]); }); connection.on('insert', (data) => { store1.push([{ type: 'insert', data }]); store2.push([{ type: 'insert', data }]); }); connection.on('remove', (key) => { store1.push([{ type: 'remove', key }]); store2.push([{ type: 'remove', key }]); }); }); const App = () => ( <div className="schedulers"> <div className="column-1"> <Scheduler timeZone="America/Los_Angeles" dataSource={store1} views={views} defaultCurrentView="day" defaultCurrentDate={currentDate} height={600} startDayHour={9} endDayHour={19} remoteFiltering={true} dateSerializationFormat="yyyy-MM-ddTHH:mm:ssZ" textExpr="Text" startDateExpr="StartDate" descriptionExpr="Description" endDateExpr="EndDate" allDayExpr="AllDay" /> </div> <div className="column-2"> <Scheduler timeZone="America/Los_Angeles" dataSource={store2} views={views} defaultCurrentView="day" defaultCurrentDate={currentDate} height={600} startDayHour={9} endDayHour={19} remoteFiltering={true} dateSerializationFormat="yyyy-MM-ddTHH:mm:ssZ" textExpr="Text" startDateExpr="StartDate" endDateExpr="EndDate" allDayExpr="AllDay" /> </div> </div> ); export default App;
import React from 'react'; import ReactDOM from 'react-dom'; import App from './App.tsx'; ReactDOM.render( <App />, document.getElementById('app'), );
window.exports = window.exports || {}; window.config = { transpiler: 'ts', typescriptOptions: { module: 'system', emitDecoratorMetadata: true, experimentalDecorators: true, jsx: 'react', }, meta: { 'react': { 'esModule': true, }, 'typescript': { 'exports': 'ts', }, 'devextreme/time_zone_utils.js': { 'esModule': true, }, 'devextreme/localization.js': { 'esModule': true, }, 'devextreme/viz/palette.js': { 'esModule': true, }, 'devextreme-aspnet-data-nojquery': { 'esModule': true, }, }, paths: { 'npm:': 'https://unpkg.com/', }, defaultExtension: 'js', map: { 'ts': 'npm:plugin-typescript@4.2.4/lib/plugin.js', 'typescript': 'npm:typescript@4.2.4/lib/typescript.js', 'react': 'npm:react@17.0.2/umd/react.development.js', 'react-dom': 'npm:react-dom@17.0.2/umd/react-dom.development.js', 'prop-types': 'npm:prop-types@15.8.1/prop-types.js', '@aspnet/signalr': 'npm:@aspnet/signalr@1.0.27/dist/cjs', 'tslib': 'npm:tslib@2.6.2/tslib.js', 'devextreme-aspnet-data-nojquery': 'npm:devextreme-aspnet-data-nojquery@3.0.0/index.js', 'rrule': 'npm:rrule@2.6.4/dist/es5/rrule.js', 'luxon': 'npm:luxon@1.28.1/build/global/luxon.min.js', 'es6-object-assign': 'npm:es6-object-assign@1.1.0', 'devextreme': 'npm:devextreme@23.2.5/cjs', 'devextreme-react': 'npm:devextreme-react@23.2.5/cjs', 'jszip': 'npm:jszip@3.10.1/dist/jszip.min.js', 'devextreme-quill': 'npm:devextreme-quill@1.6.4/dist/dx-quill.min.js', 'devexpress-diagram': 'npm:devexpress-diagram@2.2.5/dist/dx-diagram.js', 'devexpress-gantt': 'npm:devexpress-gantt@4.1.51/dist/dx-gantt.js', '@devextreme/runtime': 'npm:@devextreme/runtime@3.0.12', 'inferno': 'npm:inferno@7.4.11/dist/inferno.min.js', 'inferno-compat': 'npm:inferno-compat/dist/inferno-compat.min.js', 'inferno-create-element': 'npm:inferno-create-element@7.4.11/dist/inferno-create-element.min.js', 'inferno-dom': 'npm:inferno-dom/dist/inferno-dom.min.js', 'inferno-hydrate': 'npm:inferno-hydrate@7.4.11/dist/inferno-hydrate.min.js', 'inferno-clone-vnode': 'npm:inferno-clone-vnode/dist/inferno-clone-vnode.min.js', 'inferno-create-class': 'npm:inferno-create-class/dist/inferno-create-class.min.js', 'inferno-extras': 'npm:inferno-extras/dist/inferno-extras.min.js', 'devextreme-cldr-data': 'npm:devextreme-cldr-data@1.0.3', // SystemJS plugins 'plugin-babel': 'npm:systemjs-plugin-babel@0.0.25/plugin-babel.js', 'systemjs-babel-build': 'npm:systemjs-plugin-babel@0.0.25/systemjs-babel-browser.js', // Prettier 'prettier/standalone': 'npm:prettier@2.8.4/standalone.js', 'prettier/parser-html': 'npm:prettier@2.8.4/parser-html.js', }, packages: { 'devextreme': { defaultExtension: 'js', }, 'devextreme-react': { main: 'index.js', }, 'devextreme/events/utils': { main: 'index', }, 'devextreme/localization/messages': { format: 'json', defaultExtension: '', }, 'devextreme/events': { main: 'index', }, '@aspnet/signalr': { main: 'index.js', defaultExtension: 'js', }, 'es6-object-assign': { main: './index.js', defaultExtension: 'js', }, }, packageConfigPaths: [ 'npm:@devextreme/*/package.json', 'npm:@devextreme/runtime@3.0.12/inferno/package.json', ], babelOptions: { sourceMaps: false, stage0: true, react: true, }, }; System.config(window.config);
import React from 'react'; import ReactDOM from 'react-dom'; import App from './App.js'; ReactDOM.render(<App />, document.getElementById('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="https://cdn3.devexpress.com/jslib/23.2.5/css/dx.light.css" /> <script src="../signalr-session-id.js"></script> <script src="https://unpkg.com/core-js@2.6.12/client/shim.min.js"></script> <script src="https://unpkg.com/systemjs@0.21.3/dist/system.js"></script> <script type="text/javascript" src="config.js"></script> <link rel="stylesheet" type="text/css" href="styles.css" /> <script type="text/javascript"> System.import("./index.tsx"); </script> </head> <body class="dx-viewport"> <div class="demo-container"> <div id="app"></div> </div> </body> </html>
.schedulers { display: flex; } .column-1 { padding-right: 5px; } .column-2 { padding-left: 5px; } .dx-scheduler-small .dx-scheduler-view-switcher.dx-tabs { display: table; }
using DevExtreme.AspNet.Data; using DevExtreme.AspNet.Mvc; using DevExtreme.MVC.Demos.Hubs; using DevExtreme.MVC.Demos.Models; using Microsoft.AspNet.SignalR; using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Formatting; using System.Web; using System.Web.Http; namespace DevExtreme.MVC.Demos.Controllers.ApiControllers { public class SchedulerSignalRController : ApiController { InMemoryAppointmentsDataContext _data = new InMemoryAppointmentsDataContext(); IHubContext hubContext; public SchedulerSignalRController() { hubContext = GlobalHost.ConnectionManager.GetHubContext<SchedulerSignalRHub>(); } [HttpGet] public HttpResponseMessage Get(DataSourceLoadOptions loadOptions) { return Request.CreateResponse(DataSourceLoader.Load(_data.Appointments, loadOptions)); } [HttpPost] public HttpResponseMessage Post(FormDataCollection form) { var values = form.Get("values"); var newAppointment = new Appointment(); JsonConvert.PopulateObject(values, newAppointment); Validate(newAppointment); if(!ModelState.IsValid) return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState.GetFullErrorMessage()); _data.Appointments.Add(newAppointment); _data.SaveChanges(); var groupName = GetGroupName(); if(groupName != null) { hubContext.Clients.Group(GetGroupName()).insert(newAppointment); } return Request.CreateResponse(HttpStatusCode.Created); } [HttpPut] public HttpResponseMessage Put(FormDataCollection form) { var key = Convert.ToInt32(form.Get("key")); var values = form.Get("values"); var appointment = _data.Appointments.First(a => a.AppointmentId == key); JsonConvert.PopulateObject(values, appointment); Validate(appointment); if(!ModelState.IsValid) return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState.GetFullErrorMessage()); _data.SaveChanges(); var groupName = GetGroupName(); if(groupName != null) { hubContext.Clients.Group(GetGroupName()).update(key, appointment); } return Request.CreateResponse(HttpStatusCode.OK); } [HttpDelete] public void Delete(FormDataCollection form) { var key = Convert.ToInt32(form.Get("key")); var appointment = _data.Appointments.First(a => a.AppointmentId == key); _data.Appointments.Remove(appointment); _data.SaveChanges(); var groupName = GetGroupName(); if(groupName != null) { hubContext.Clients.Group(GetGroupName()).remove(key); } } string GetGroupName() { var cookie = Request.Headers.GetCookies(SchedulerSignalRHub.GroupIdKey).FirstOrDefault(); if(cookie != null) { return cookie[SchedulerSignalRHub.GroupIdKey].Value; } return null; } } }
using System; using System.Threading.Tasks; using System.Web; using Microsoft.AspNet.SignalR; namespace DevExtreme.MVC.Demos.Hubs { public class SchedulerSignalRHub : Hub { public static string GroupIdKey = "dx-SchedulerSignalRHub-groupId"; private static readonly Random random = new Random(); public override Task OnConnected() { Cookie cookie; Context.RequestCookies.TryGetValue(GroupIdKey, out cookie); string groupId; if(cookie != null) { groupId = cookie.Value; } else { groupId = random.Next(0, int.MaxValue).ToString(); var newCookie = new HttpCookie(GroupIdKey, groupId); Context.Request.GetHttpContext().Response.Cookies.Add(newCookie); } Groups.Add(Context.ConnectionId, groupId); return base.OnConnected(); } } }