Features

  • A reference implementation of JavaScript Scheduler dynamic loading

  • See what Scheduler performance you can expect when displaying 500 resources and 400 events per resource

  • The performance is only affected by the Scheduler viewport size, it's not affected by the total number of events

  • The backend uses ASP.NET MVC Core (.NET 5), Entity Framework and SQL Sever (LocalDB)

  • Includes a trial version of DayPilot Pro for JavaScript (see License below)

License

Licensed for testing and evaluation purposes. Please see the license agreement included in the sample project. You can use the source code of the tutorial if you are a licensed user of DayPilot Pro for JavaScript. Buy a license.

JavaScript Scheduler Configuration

Our JavaScript Scheduler configuration is simple and mostly rely on the default settings. We will use startDate and days properties to display the full year 2021, one cell per day (scale = "Day").

Right after initialization, we load the resources from the server using DayPilot.Scheduler.rows.load() call.

Views/Home/Index.cshtml

@{
    ViewData["Title"] = "JavaScript Scheduler: Dynamic Event Loading (ASP.NET Core)";
}

<script src="~/lib/daypilot/daypilot-all.min.js" asp-append-version="true"></script>

<div id="dp"></div>

<script>
    var dp = new DayPilot.Scheduler("dp");
    dp.startDate = "2021-01-01";
    dp.days = 365;
    dp.timeHeaders = [
        { groupBy: "Month" },
        { groupBy: "Day", format: "d" }
    ];
    dp.scale = "Day";

    dp.init();

    dp.rows.load("/api/Resources");
</script>

Dynamic Event Loading

As the next step, we enable dynamic event loading. This means the Scheduler will fire onScroll event for every scrollbar position change. The events for the current viewport will be loaded using a call to a REST API.

Client-side configuration:

dp.dynamicLoading = true;
dp.onScroll = function (args) {
    args.async = true;
    var url = "/api/Events?start=" + args.viewport.start + "&end=" + args.viewport.end + "&resources=" + args.viewport.resources.join(",");
    DayPilot.Http.ajax({
        url: url,
        success: function (a) {
            args.events = a.data;
            args.loaded();
        }
    });
};

Server-side API:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity.UI.Pages.Internal.Account;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Remotion.Linq.Clauses;
using Project.Models;

namespace Project.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class EventsController : ControllerBase
    {
        private readonly SchedulerContext _context;

        public EventsController(SchedulerContext context)
        {
            _context = context;
        }
        
        // ...

        // GET: api/Events
        [HttpGet]
        public IEnumerable GetEvents([FromQuery] DateTime start, [FromQuery] DateTime end, [FromQuery] string resources)
        {
            if (resources == null)
            {
                return Enumerable.Empty<object>();
            }
            var resArray = resources.Split(',').Select(Int32.Parse).ToList();
            return from e in _context.Events where !((e.End <= start) || (e.Start >= end)) && resArray.Contains(e.ResourceId) select new
            {
                e.Start,
                e.End,
                e.Id,
                e.Text,
                Resource = e.ResourceId
            };
        }

    }
}

Scheduler Performance Tuning

To improve the vertical scrolling performance, we disable the automatic row header width adjustment and set the row header width manually. This feature is convenient but it may harm smooth scrolling.

dp.rowHeaderWidthAutoFit = false;
dp.rowHeaderWidth = 120;

The existing events will be cleared when the Scheduler receives a new data set (args.clearEvents is set to true by default). This speeds up loading of the viewport events as they don't need to be compared with the existing events.

dp.onScroll = function (args) {
    var url = "/api/Events?start=" + args.viewport.start + "&end=" + args.viewport.end + "&resources=" + args.viewport.resources.join(",");
    DayPilot.Http.ajax({
        url: url,
        success: function (a) {
            args.events = a.data;
            args.loaded();
        }
    });
};

Data Layer: Entity Framework + SQL Server Database

The database uses a simple schema defined using model classes (Event and Resource):

Models/Data.cs

public class Event
{
    public int Id { get; set; }
    public DateTime Start { get; set; }
    public DateTime End { get; set; }
    public int ResourceId { get; set; }
    public string Text { get; set; }
}

public class Resource
{
    public int Id { get; set; }
    public string Name { get; set; }
}

The initial migration is already created (see Migrations directory). In order to create the database, you need to run Update-Database in the console:

Update-Database

The database will be automatically initialized with 500 resources:

Models/Data.cs

using System;
using Microsoft.EntityFrameworkCore;

namespace TutorialAspnetCoreSchedulerLarge.Models
{
    public class SchedulerContext : DbContext
    {
        public DbSet<Event> Events { get; set; }
        public DbSet<Resource> Resources { get; set; }

        public SchedulerContext(DbContextOptions<SchedulerContext> options):base(options) { }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            for (var y = 1; y <= 500; y++)
            {
                modelBuilder.Entity<Resource>().HasData(new Resource { Id = y, Name = "Resource " + y });
            }            
        }
    }

    // ...

}

The database connection string is defined in appsetings.json. It uses a database called DayPilot.TutorialSchedulerDynamic and connects using LocalDB.

appsettings.json

{
    "Logging": {
        "LogLevel": {
            "Default": "Warning"
        }
    },
    "AllowedHosts": "*",
    "ConnectionStrings": {
        "SchedulerContext": "Server=(localdb)\\mssqllocaldb;Database=DayPilot.TutorialSchedulerDynamic;Trusted_Connection=True"
    }
}

Scheduler API Controllers

The controllers are generated from the model classes. The EventsController contains two custom methods:

The GetEvents() method loads events for the viewport (specified using start, end, and resources query string parameters).

  • The start and end query string parameters hold the start and end date/time.

  • The resources query string parameter holds a list of resource ids, separated by comma.

We will use these parameters to limit the database query.

[Route("api/[controller]")]
[ApiController]
public class EventsController : ControllerBase
{
    private readonly SchedulerContext _context;

    // ...


    // GET: api/Events
    [HttpGet]
    public IEnumerable GetEvents([FromQuery] DateTime start, [FromQuery] DateTime end, [FromQuery] string resources)
    {
        if (resources == null)
        {
            return Enumerable.Empty<object>();
        }
        var resArray = resources.Split(',').Select(Int32.Parse).ToList();
        return from e in _context.Events
            where !((e.End <= start) || (e.Start >= end)) && resArray.Contains(e.ResourceId)
            select new
            {
                e.Start,
                e.End,
                e.Id,
                e.Text,
                Resource = e.ResourceId
            };
    }
}

The GenerateEvents() method resets the [Events] table content and generate a new event set.

[Route("api/[controller]")]
[ApiController]
public class EventsController : ControllerBase
{
    private readonly SchedulerContext _context;

    // ...

    [HttpPost("generate")]
    public object GenerateEvents([FromQuery] int count)
    {
        _context.Database.ExecuteSqlRaw("TRUNCATE TABLE [Events]");

        var r = _context.Resources;

        int eventCountPerRow = count / r.ToArray().Length;

        int i = 1;
        var start = new DateTime(2021, 1, 1);
        foreach (var resource in r)
        {
            for (var x = 0; x < eventCountPerRow; x++)
            {
                var e = new Event
                {
                    Start = start.AddDays(x),
                    End = start.AddDays(x + 1),
                    ResourceId = resource.Id,
                    Text = "Event " + i
                };
                _context.Events.Add(e);
                i++;
            }
        }
        _context.SaveChanges();

        return Tuple.Create("OK");

    }

}

Generating Sample Events

asp.net-core-javascript-scheduler-generate-events.png

In order to generate sample data, click the "Generate..." link above the Scheduler. This will call a REST endpoint that generates 200,000 events (400 events per row).