Smart.Scheduler SignalR App

Smart.Scheduler with SignalR

The following tutorial shows how to setup a real-time SignalR web app with Smart.Scheduler. The application will contain two Smart.Schedulers that will use the same dataSource and when an event is dragged/resized/inserted/removed a push notification will be sent to the SignalR server to notify the other Smart.Scheduler on the page to update the corresponding event as well.

What is SignalR?

ASP.NET SignalR is a library for ASP.NET developers that simplifies the process of adding real-time web functionality to applications. Real-time web functionality is the ability to have server code push content to connected clients instantly as it becomes available, rather than having the server wait for a client to request new data.

SignalR can be used to add any sort of "real-time" web functionality to your ASP.NET application. While chat is often used as an example, you can do a whole lot more.

In order to create a Real-time Web Application with Smart.Scheduler and SignalR, the following steps should be taken:

Prerequisites:

  • Visual Studio Code
  • C# for Visual Studio Code Plug-in
  • .NET Core 3.1 SDK or later

Steps:

  1. Create a new web app project - Open the VS Code terminal inside the folder where the new web app should be created and run the following command:
    dotnet new webapp -o SignalRScheduler

    Then browse to the newly created folder via the following command:

    cd SignalRScheduler
  2. Add the SignalR client librarty - the SignalR server library is included in the ASP.NET Core 3.1 framework. However the JavaScript client library is not included in the project. In order to obtain it we are going to use Library Manager (LibMan) to get the client library from unpkg. Open the VS Code integrated terminal and enter the following command in order to install LibMan:
    dotnet tool install -g Microsoft.Web.LibraryManager.Cli

    Then install the SignalR client library via the following command:

    libman install @microsoft/signalr@latest -p unpkg -d wwwroot/js/signalr --files dist/browser/signalr.js --files dist/browser/signalr.min.js

    The parameters of the command specify the following options:

    • Use the unpkg provider.
    • Copy files to the wwwroot/js/signalr destination.
    • Copy only the specified files.
    • The following output should be displayed in the terminal when the command is executed:

      wwwroot/js/signalr/dist/browser/signalr.js written to disk
      wwwroot/js/signalr/dist/browser/signalr.min.js written to disk
      Installed library "@microsoft/signalr@latest" to "wwwroot/js/signalr"
  3. Create a SignalR Hub - from inside the SignalRScheduler folder create a new folder called Hubs and inside that folder a new file called SchedulerHub.cs with the following content:
    using Microsoft.AspNetCore.SignalR;
    using System.Threading.Tasks;
    
    namespace SignalRSchedulerSync.Hubs
    {
        public class SchedulerHub : Hub
        {
            public async Task UpdateEvent(object schedulerId, object eventObj)
            {
                await Clients.All.SendAsync("EventUpdated", schedulerId, eventObj);
            }
    
            public async Task RemoveEvent(object schedulerId, object eventObj)
            {
                await Clients.All.SendAsync("EventRemoved", schedulerId, eventObj);
            }
    
            public async Task InsertEvent(object schedulerId, object eventObj)
            {
                await Clients.All.SendAsync("EventInserted", schedulerId, eventObj);
            }
        }
    }
    

    That file will contain the methods for the SignalR service that will be called from the client in order to push a notification to all of the clients of the app.

  4. Configure SignalR - The SignalR server must be configured to pass SignalR requests to SignalR. In order to do the Startup.cs file located in the SignalRScheduler project folder should be updated like so:
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading.Tasks;
    using Microsoft.AspNetCore.Builder;
    using Microsoft.AspNetCore.Hosting;
    using Microsoft.AspNetCore.HttpsPolicy;
    using Microsoft.Extensions.Configuration;
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Extensions.Hosting;
    using SignalRSchedulerSync.Hubs; //Import the newly created Scheduler Hub
    
    namespace SignalRScheduler
    {
        public class Startup
        {
            public Startup(IConfiguration configuration)
            {
                Configuration = configuration;
            }
    
            public IConfiguration Configuration { get; }
    
            // This method gets called by the runtime. Use this method to add services to the container.
            public void ConfigureServices(IServiceCollection services)
            {
                services.AddRazorPages();
                services.AddSignalR(); //Add the SignalR service
            }
    
            // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
            public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
            {
                if (env.IsDevelopment())
                {
                    app.UseDeveloperExceptionPage();
                }
                else
                {
                    app.UseExceptionHandler("/Error");
                    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                    app.UseHsts();
                }
    
                app.UseHttpsRedirection();
                app.UseStaticFiles();
    
                app.UseRouting();
    
                app.UseAuthorization();
    
                app.UseEndpoints(endpoints =>
                {
                    endpoints.MapRazorPages();
                    endpoints.MapHub<SchedulerHub>("/schedulerHub"); //Map the Scheduler Hub to the endpoints
                });
            }
        }
    }
    

    These changes add SignalR to the ASP.NET Core dependency injection and routing systems.

  5. Download the smart-webcomponents NPM package - in order to use the Smart.Scheduler, the smart-webcomponents NPM package should be downloaded first. From inside the SignalRScheduler\wwwroot\js folder open a terminal and execute the following command:
    npm init

    The npm init command will create a package.json file that will allow to install NPM dependencies to the project.

    Next install the Smart library with the following command:

    npm i smart-webcomponents

    As a result a new folder called node_modules should be created in the current directory containing the smart-webcomponents library files.

  6. Add the SignalR client - the necessary JS files for the SignalR client will be added to the Pages\Sahred\_Layout.cshtml file. It contains the layout HTML structure of the application. The file should contain the following code:
    <!DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>@ViewData["Title"] - SignalRScheduler</title>
        <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
        <link rel="stylesheet" href="~/js/node_modules/smart-webcomponents/source/styles/smart.default.css"
            type="text/css" />
        <link rel="stylesheet" href="~/css/site.css" />
    </head>
    
    <body>
        <header>
            <h4>SignalR Scheduler</h4>
            <p>The following demo shows how two users can edit data in the Scheduler at the same time. The SignalR
                service is used to broadcast the changes to the server triggering push notifications.</p>
    
            <p> As a result when
                dragging/resizing/inserting/deleting an event in one Scheduler automatically reflects to the other.</p>
        </header>
        <main>
            @RenderBody()
        </main>
    
        <footer class="border-top footer text-muted">
            © 2021 - SignalR Scheduler - <a href="https://www.htmlelements.com/">Smart HTMLElements</a>
        </footer>
    
        <script src="~/lib/jquery/dist/jquery.min.js"></script>
        <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
        <script src="~/js/site.js" asp-append-version="true"></script>
        <script src="~/js/signalr/dist/browser/signalr.js"></script>
    
        <script type="module" src="~/js/node_modules/smart-webcomponents/source/modules/smart.scheduler.js"></script>
        <script type="module" src="~/js/schedule.js"></script>
    
        @await RenderSectionAsync("Scripts", required: false)
    </body>
    
    </html>
    

    Notice the smart.default.css style reference in the header section. It imports all necessary styles for the Smart components. Also notice the imported module scripts at the bottom of the Body section.

  7. Configure the App Main Section - update the content of the index.cshtml file located in the SignalRScheduler\Pages folder like so:
    @page
    @model IndexModel
    @{
        ViewData["Title"] = "SignalR with Smart.Scheduler";
    }
    
    <smart-scheduler id="scheduler"></smart-scheduler>
    <smart-scheduler id="scheduler2"></smart-scheduler>
                            

    This file contains the main section of the application with two Smart.Schedulers that will be used to demonstrate real-time event manipulation.

  8. Create the 'schedule.js' file - this file handles the demo logic. It creates a connection to the SignalR server and updates the Smart.Scheduler on event change. Here's what the content of the file looks like:
    const connection = new window.signalR.HubConnectionBuilder().withUrl('/schedulerHub').build(),
        scheduler = document.getElementById('scheduler'),
        scheduler2 = document.getElementById('scheduler2'),
        today = new Date(),
        todayDate = today.getDate(),
        currentYear = today.getFullYear(),
        currentMonth = today.getMonth(),
        data = [
            {
                label: 'Google AdWords Strategy',
                dateStart: new Date(currentYear, currentMonth, todayDate, 9, 0),
                dateEnd: new Date(currentYear, currentMonth, todayDate, 10, 30),
                backgroundColor: '#E67C73'
            }, {
                label: 'New Brochures',
                dateStart: new Date(currentYear, currentMonth, todayDate - 1, 11, 30),
                dateEnd: new Date(currentYear, currentMonth, todayDate - 1, 14, 15),
                backgroundColor: '#8E24AA'
            }, {
                label: 'Brochure Design Review',
                dateStart: new Date(currentYear, currentMonth, todayDate + 2, 13, 15),
                dateEnd: new Date(currentYear, currentMonth, todayDate + 2, 16, 15),
                backgroundColor: '#039BE5'
            }
        ];
    
    window.Smart('#scheduler', class {
        get properties() {
            return {
                dataSource: data,
                view: 'day'
            }
        }
    });
    
    window.Smart('#scheduler2', class {
        get properties() {
            return {
                dataSource: data,
                view: 'day'
            }
        }
    });
    
    //Disable send button until connection is established
    scheduler.disabled = scheduler2.disabled = true;
    
    //Listen for real-time changes to the Schedulers
    connection.on('EventUpdated', function (schedulerId, eventObj) {
        if (schedulerId === scheduler.id) {
            scheduler2.updateEvent(eventObj, eventObj);
        }
        else {
            scheduler.updateEvent(eventObj, eventObj);
        }
    });
    
    connection.on('EventRemoved', function (schedulerId, eventObj) {
        if (schedulerId === scheduler.id) {
            scheduler2.removeEvent(eventObj, eventObj);
        }
        else {
            scheduler.removeEvent(eventObj, eventObj);
        }
    });
    
    connection.on('EventInserted', function (schedulerId, eventObj) {
        if (schedulerId === scheduler.id) {
            scheduler2.insertEvent(eventObj);
        }
        else {
            scheduler.insertEvent(eventObj);
        }
    });
    
    connection.start().then(function () {
        scheduler.disabled = scheduler2.disabled = false;
    }).catch((err) => console.error(err.toString()));
    
    //Update Scheduler on task Update
    scheduler.addEventListener('dragStart', updateDropTarget);
    scheduler.addEventListener('dragEnd', updateEvent);
    scheduler.addEventListener('resizeEnd', updateEvent);
    scheduler.addEventListener('itemRemove', removeEvent);
    scheduler.addEventListener('itemInsert', insertEvent);
    
    scheduler2.addEventListener('dragEnd', updateEvent);
    scheduler2.addEventListener('dragStart', updateDropTarget);
    scheduler2.addEventListener('resizeEnd', updateEvent);
    scheduler2.addEventListener('itemRemove', removeEvent);
    scheduler2.addEventListener('itemInsert', insertEvent);
    
    function updateDropTarget(event) {
        if (event.target.id === scheduler.id) {
            scheduler2.disableDrop = true;
        }
        else {
            scheduler.disableDrop = true;
        }
    }
    
    function updateEvent(event) {
        const details = event.detail,
            eventObj = details.item;
    
        if (event.type === 'resizeEnd') {
            const itemDateRange = details.itemDateRange;
    
            eventObj.dateStart = itemDateRange.dateStart;
            eventObj.dateEnd = itemDateRange.dateEnd;
        }
        else if (event.type === 'dragEnd') {
            scheduler.disableDrop = scheduler2.disableDrop = false;
        }
    
        connection.invoke('UpdateEvent', event.target.id, eventObj).catch((err) => console.error(err.toString()));
    }
    
    function removeEvent(event) {
        const details = event.detail,
            eventObj = details.item;
    
        connection.invoke('RemoveEvent', event.target.id, eventObj).catch((err) => console.error(err.toString()));
    }
    
    function insertEvent(event) {
        const details = event.detail,
            eventObj = details.item;
    
        connection.invoke('InsertEvent', event.target.id, eventObj).catch((err) => console.error(err.toString()));
    }
    

    By adding EventListeners to the Scheduler for their 'dragEnd', 'resizeEnd', 'itemInsert', 'itemRemove' events we can push a notification to the SignalR service by invoking the corresponding server method. Then via the connection.on method we are able to listen for the notifications and update the Schedulers.

  9. Run the Application - in order to run the application open a terminal inside the root folder of the project - SignalRScheduler and execute the following command:
    dotnet watch run -p SignalRScheduler.csproj

    Open https://localhost:5001/ in any browser to view the Application.

    Demo

    Scheduler Signal R