When building a calendar or scheduler view for your app, there's a good deal of nuance involved. The Telerik UI for Blazor Scheduler aims to handle all of those complexities for you.
If you’ve ever tried to build a calendar or scheduler view for your app, you know there’s a good deal of nuance involved, especially if you want to enable “advanced” functionality like drag and drop, drag to resize and multiple views (day, week, month).
The Blazor Scheduler from Progress Telerik UI for Blazor aims to handle all of those complexities for you: spin up a new instance, throw it some events and you should be good to go.
So how does it hold up in practice? Let’s find out.
First we need some data for our scheduler.
We’ll start with some simple hardcoded events to get the ball rolling, then switch to “real” events when we know the calendar is working.
Let’s create a service to return those hardcoded events:
CalendarService.cs
public class CalendarService
{
private readonly List<CalendarEntry> _entries = new List<CalendarEntry>
{
new CalendarEntry
{
Title = "A Holiday",
Start = new DateTime(2023,2,13),
End = new DateTime(2023,2,13),
IsAllDay = true
}
};
public IEnumerable<CalendarEntry> List()
{
return _entries.ToList();
}
}
Here’s our CalendarEntry
class:
public class CalendarEntry
{
public Guid Id { get; }
public string Title { get; set; }
public string Description { get; set; }
public DateTime Start { get; set; }
public DateTime End { get; set; }
public bool IsAllDay { get; set; }
public CalendarEntry()
{
Id = Guid.NewGuid();
}
}
The field names here are the default ones which Telerik Scheduler will look for. If we stick to these names, we don’t need to perform any additional mapping for the scheduler to interact with and display our events (holidays in this case).
We’ll need to ensure this is registered as a service for our Blazor app:
Program.cs
...
builder.Services.AddSingleton<CalendarService>();
...
This is a Blazor Server app; by registering this as a Singleton, we can ensure we get some useful data to interact with that won’t be wiped clean every time we refresh the site. In a production app we’d likely point this at a database rather than using hardcoded data like this.
Now we can go ahead and have a go at displaying those calendar entries using TelerikScheduler
:
Calendar.razor
@page "/Calendar"
@inject CalendarService CalendarService
<TelerikScheduler Data="entries" @bind-Date="@StartDate" Height="600px">
<SchedulerViews>
<SchedulerWeekView StartTime="@DayStart" />
</SchedulerViews>
</TelerikScheduler>
@code {
public DateTime StartDate { get; set; } = DateTime.Now;
public DateTime DayStart { get; set; } = new DateTime(2023, 2, 1, 8, 0, 0);
private IEnumerable<CalendarEntry> entries;
protected override async Task OnInitializedAsync()
{
entries = CalendarService.List();
}
...
}
We’ve created a scheduler with a single “view” (SchedulerWeekView
). Views offer
different ways to represent your calendar data, by day, week, month, etc.
We’ve also established a StartDate
(the calendar will show events on and around this date when the calendar first loads).
Finally, we’ve specified a StartTime
for the SchedulerWeekView
. Although this is a DateTime
, we’re only interested in the time part of the
DateTime
which is used to control the first hour which is shown in the view.
With this, we have a fully functional calendar.
So far so good, but what if we want to let the user add their own entries to the calendar?
First we need to enable event creation, then provide a method that will contain the logic we need to run when an item is added.
<TelerikScheduler ... AllowCreate="true" OnCreate="OnItemCreated">
...
</TelerikScheduler>
The method takes an argument of type SchedulerCreateEventArgs
.
@code {
...
private async Task OnItemCreated(SchedulerCreateEventArgs args)
{
var item = args.Item as CalendarEntry;
CalendarService.Add(item);
entries = CalendarService.List();
}
}
The incoming SchedulerCreateEventArgs
includes an Item
property which we can cast to our specific model type (CalendarEntry
).
We can forward that item on to our CalendarService
(so it can take care of adding it to the list of entries), and re-fetch the entries (to see the new item in the list).
Here’s that Add
method in CalendarService
.
CalendarService.cs
namespace DemoServer.Services;
public class CalendarService
{
...
public void Add(CalendarEntry item)
{
_entries.Add(item);
}
}
With these changes we can double-click the calendar and we’ll be presented with a handy form for adding a new event:
We can also enable users to edit existing events, with the AllowUpdate
and OnUpdate
parameters:
<TelerikScheduler ... AllowUpdate="true" OnUpdate="OnItemEdited">
...
</TelerikScheduler>
We’ve enabled updating of items with AllowUpdate
and when the user edits an item our OnItemUpdated
method will be invoked.
private async Task OnItemUpdated(SchedulerUpdateEventArgs args)
{
var item = args.Item as CalendarEntry;
CalendarService.Edit(item);
entries = CalendarService.List();
}
Here, once again, we cast the incoming item on args.Item
to our CalendarEntry
type.
We then let CalendarService
handle the actual updating, before refreshing the data to reflect our changes.
CalendarService.cs
namespace DemoServer.Services;
public class CalendarService
{
...
public void Edit(CalendarEntry item)
{
var existingEntry = _entries.FirstOrDefault(x => x.Id == item.Id);
_entries.Remove(existingEntry);
_entries.Add(item);
}
}
(In a production app, we’d probably edit the existing entry, but it’s easier for our demo to delete the old entry and replace it with a new entry for the modified item).
With this, if a user double-clicks an existing entry, they’ll see the built-in Edit UI.
Just before we go on to tackle item deletion, a little bit of duplication has snuck in here, with multiple identical calls to refresh the calendar data. Let’s move that code to a separate method which can be called from multiple places.
@code {
public DateTime StartDate { get; set; } = DateTime.Now;
public DateTime DayStart { get; set; } = new DateTime(2023, 2, 1, 8, 0, 0);
private IEnumerable<CalendarEntry> entries;
protected override async Task OnInitializedAsync()
{
await LoadData();
}
private async Task LoadData()
{
entries = CalendarService.List();
}
private async Task OnItemCreated(SchedulerCreateEventArgs args)
{
var item = args.Item as CalendarEntry;
CalendarService.Add(item);
await LoadData();
}
private async Task OnItemEdited(SchedulerUpdateEventArgs args)
{
var item = args.Item as CalendarEntry;
CalendarService.Edit(item);
await LoadData();
}
}
Now we have a single LoadData
method that we can call whenever we want to (re)fetch our calendar data.
If you’ve ever tried to implement “drag and drop” functionality, you know that it’s not always straightforward. Happily, in this case Telerik UI for Blazor Scheduler takes care of this for us.
With editing enabled, we can drag an existing entry to a new time slot (on the same or a different day) and the event will be automatically updated.
It’s also possible to edit an entry by clicking on one of its edges and extending (or reducing) the amount of space it takes up (thereby editing the start and/or end time of the entry).
As you might expect, you can enable deletion of entries too:
<TelerikScheduler ... AllowDelete="true" OnDelete="Callback">
@code {
...
private async Task OnDelete(SchedulerDeleteEventArgs args)
{
var item = args.Item as CalendarEntry;
CalendarService.Delete(item);
await LoadData();
}
}
By now this feels pretty familiar. We cast the incoming item to an instance of CalendarEntry
, then forward it on to CalendarService
which performs the actual deletion:
CalendarService.cs
...
public void Delete(CalendarEntry item)
{
var existingEntry = _entries.FirstOrDefault(x => x.Id == item.Id);
_entries.Remove(existingEntry);
}
Finally, let’s look at a slightly more advanced scenario.
Say we wanted to pull in a list of public holidays and show them in the calendar, but, crucially, these entries should be “read only” (not editable by the user).
While there is no built-in concept of read-only events for the Scheduler component, there is a handy way to implement this functionality with minimal code.
First, let’s add some public holidays to our calendar. For this, we’ll use the handy Public Holidays API from Nager.Date.
Let’s create a new service to fetch the holidays and return them as entries for our calendar:
using DemoServer.Services;
namespace CalendarDemo.Services;
public class PublicHolidayService
{
private readonly HttpClient _httpClient;
public PublicHolidayService(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<IEnumerable<CalendarEntry>> ListForYear(int year)
{
var holidays = await _httpClient.GetFromJsonAsync<NagerHoliday[]>($"https://date.nager.at/api/v3/publicholidays/{year}/GB");
return holidays.Select(x=>new CalendarEntry
{
Title = $"{x.Name} ({x.CountryCode})",
IsAllDay = true,
Start = x.Date,
End = x.Date,
Description = x.LocalName
});
}
public class NagerHoliday
{
public DateTime Date { get; set; }
public string LocalName { get; set; }
public string Name { get; set; }
public string CountryCode { get; set; }
}
}
This makes an HTTP fetch to the Nager API to fetch holidays for the given year
, then converts them into instances of our CalendarEntry
class.
With this registered as a service in Program.cs…
Program.cs
...
builder.Services.AddScoped<PublicHolidayService>();
...
We can go ahead and use it in our component:
@page "/Calendar"
@inject CalendarService CalendarService
@inject PublicHolidayService PublicHolidayService
...
Now we can modify LoadData
to combine the entries from both services (our original CalendarService
and the new PublicHolidayService
:
@code {
...
private async Task LoadData()
{
var calendarEntries = CalendarService.List();
var holidays = await PublicHolidayService.ListForYear(2023);
entries = calendarEntries.Concat(holidays);
}
}
This fetches the entries from our original service as well a holidays from our new Public Holiday Service then uses the handy Concat
method to combine the two lists together.
We’ll now see public holidays in our schedule, but what about that requirement to make the public holidays read only? At the moment they can be edited just like any other entry.
Let’s update our model class for the schedule (CalendarEntry
) to have a ReadOnly
property:
public class CalendarEntry
{
... other properties
public bool ReadOnly { get; set; }
}
With this we can update PublicHolidayService
to set ReadOnly
to true
for all its entries:
PublicHolidayService.cs
public async Task<IEnumerable<CalendarEntry>> ListForYear(int year)
{
var holidays = await _httpClient.GetFromJsonAsync<NagerHoliday[]>($"https://date.nager.at/api/v3/publicholidays/{year}/GB");
return holidays.Select(x=>new CalendarEntry
{
Title = $"{x.Name} ({x.CountryCode})",
IsAllDay = true,
Start = x.Date,
End = x.Date,
Description = x.LocalName,
ReadOnly = true
});
}
Our final job is to make TelerikScheduler
react to this ReadOnly
flag, and prevent the user from editing items if it’s set to true
.
TelerikScheduler
has a handy OnEdit
method which is called just before the Edit UI is shown (when the user attempts to edit an item).
This is the perfect place to cancel editing if the event is marked as ReadOnly.
<TelerikScheduler ... OnEdit="OnEdit">
...
</TelerikScheduler>
@code {
...
private void OnEdit(SchedulerEditEventArgs args)
{
if (((CalendarEntry)args.Item).ReadOnly)
{
args.IsCancelled = true;
}
}
}
By setting IsCancelled
to true, we signal to Scheduler
that it should abandon the edit in this case. If a user attempts to edit (or delete) one of our public holidays, the edit will
be canceled, the edit UI won’t be shown, and the user can continue about their business.
Telerik Scheduler is a powerful, quick and easy way to show calendar/schedule data in your Blazor app.
Here we’ve seen how we can bind the scheduler to a list of events, then enable users to quickly add new items, edit and delete existing items.
Plus, with a little help from the OnEdit
callback, we can tackle advanced scenarios like canceling editing for “read only” items.
Want to start taking advantage of Telerik Scheduler, or any of the other 100+ ready-made components, like the Grid or Charts? Start a free trial today and experience for yourself that building rich interactive applications for half the time is just a click away.
Feel free to share your experience and ideas in the comments section below or by visiting the Telerik UI for Blazor Feedback Portal. Your input makes a difference.
Jon spends his days building applications using Microsoft technologies (plus, whisper it quietly, a little bit of JavaScript) and his spare time helping developers level up their skills and knowledge via his blog, courses and books. He's especially passionate about enabling developers to build better web applications by mastering the tools available to them. Follow him on Twitter here.