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 8), 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 relies on the default settings. We will use the startDate and days properties to display the full year of 2025, with one cell per day (scale = "Day").

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

Views/Home/Index.cshtml

@page
@model IndexModel
@{
    ViewData["Title"] = "Home page";
}

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


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

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

    const app = {
        init() {
            scheduler.rows.load("/api/Resources");
        }
    };
    app.init();


</script>

Dynamic Event Loading

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

Client-side configuration:

const scheduler = new DayPilot.Scheduler("dp", {

    // ...
    
    dynamicLoading: true,
    onScroll: async args => {
        args.async = true;
        const url = "/api/Events?start=" + args.viewport.start + "&end=" + args.viewport.end + "&resources=" + args.viewport.resources.join(",");
        const { data } = await DayPilot.Http.get(url);
        args.clearEvents = true;
        args.events = 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 async Task<IEnumerable<object>> 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();

            var events = await _context.Events
                .Where(e => !((e.End <= start) || (e.Start >= end)) && resArray.Contains(e.ResourceId))
                .Select(e => new
                {
                    e.Start,
                    e.End,
                    e.Id,
                    e.Text,
                    Resource = e.ResourceId
                })
                .ToListAsync();

            return events;
        }


    }
}

Improving Scheduler Performance

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 might harm smooth scrolling.

const scheduler = new DayPilot.Scheduler("dp", {
    
    // ...
    rowHeaderWidthAutoFit: false,
    rowHeaderWidth: 120,

    // ...
    
});

For more tips on improving the scrolling performance, see the following docs topic:

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 database will be created and initialized automatically when the application is run for the first time.

The SQL Server database initialization is performed in Program.cs:

using (var serviceScope = app.Services.CreateScope())
{
    var services = serviceScope.ServiceProvider;
    try
    {
        var context = services.GetRequiredService<SchedulerContext>();
        context.Database.EnsureCreated();
    }
    catch (Exception ex)
    {
        var logger = services.GetRequiredService<ILogger<Program>>();
        logger.LogError(ex, "An error occurred creating the DB.");
    }
}

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.TutorialAspNetCoreSchedulerDynamic;Trusted_Connection=True"
    }
}

Scheduler API Controllers

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

1. 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 async Task<IEnumerable<object>> 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();

        var events = await _context.Events
            .Where(e => !((e.End <= start) || (e.Start >= end)) && resArray.Contains(e.ResourceId))
            .Select(e => new
            {
                e.Start,
                e.End,
                e.Id,
                e.Text,
                Resource = e.ResourceId
            })
            .ToListAsync();

        return events;
    }

}

2. 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 async Task<IActionResult> GenerateEvents([FromQuery] int count)
    {
        await _context.Database.ExecuteSqlRawAsync("TRUNCATE TABLE [Events]");

        var r = await _context.Resources.ToListAsync();

        int eventCountPerRow = count / r.Count;

        int i = 1;
        var start = new DateTime(2025, 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++;
            }
        }
        await _context.SaveChangesAsync();

        return Ok(new { result = "OK" });
    }
}

Generating Sample Events

ASP.NET Core Scheduler with Dynamic Event Loading - Generate Data

In order to generate sample data, click the “Generate” button above the Scheduler. This will call a REST endpoint that generates 2,000, 20,000 or 200,000 events (400 events per row).