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.
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 ASP.NET Core 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
andend
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
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).