Hello,
The fun that one can have with dates, time and time zones seems almost infinite. I just stumbled across an issue with the Scheduler component where deleting a single occurrence of an appointment series did not work correctly sometimes. Specifically, it doesn't work in my use case for daily and hourly recurrences, but works fine for the others. When it does not work, the problem is that the recurrence exceptions do not match the actual start time of the event that they are supposed to "except". For compatibility reasons with other libraries, we have to feed the scheduler with events that have all of their dates and times represented as DateTime with DateTime.Kind == DateTimeKind.Unspecified .
I traced the issue to the RecurrenceCalculator where for both the GetNextDailyOccurrence and GetNextHourlyOccurence, the DateTime of the exception is created from the original event's start date, but with the DateTimeKind explicitly set to UTC. For example, if I have a daily recurring event from 2025-08-11 13:00 to 14:00 (with unspecified DateTimeKind) and then I delete the occurrence for 2025-08-12 in the scheduler, then the exception that is added excludes "2025-08-12T14:00:00.000Z" (with UTC DateTimeKind) which is clearly not correct. It is also not consistent with the other recurrence frequencies.
In the code of the RecurrenceCalculator
new DateTime(year, month, day, date.Hour, date.Minute, date.Second, date.Millisecond, DateTimeKind.Utc)
creates a new UTC DateTime regardless of whether the original date had DateTimeKind UTC, local or unspecified. Therefore I'm pretty sure this qualifies as a bug. There is an argument to be had about which "way" would be correct as the rest of the code creates new DateTime instance with DateTimeKind.Unspecified instead by not specifying the kind at all.
I personally would argue that for all occurrences, new DateTime instances should always be created with the same kind as the original DateTime.
Also, the events that we are displaying definitely don't happen in the UTC time zone and converting everything to UTC is really not an option as the events would be displayed incorrectly. I also don't want to "reinterpret" all the times as UTC without actually converting the time because that would end up being extremely confusing.
Regards,
// Lukas Angerer
I have a scheduler, and am using a custom Edit handler. I need to support recurrence, and editing recurring events. When the use double-clicks on a recurring event, there is a dialog that asks whether they would like to edit the occurrence or the entire series.
I want to be able to capture the results of this dialog. `SchedulerEditEventArgs` doesn't include any attributes that track this.
===========
ADMIN EDIT
===========
The implementation of this enhancement could be covered by either including the corresponding attributes in the SchedulerEditEventArgs or by exposing a Template for the RecurrenceDialog.
Note: You may also check the Ability to directly edit an occurence or the series, without the prompt asking you to choose feature request as the implementation of both features will most likely be covered in one release.
The following date in the Scheduler RecurrenceRule cannot be parsed and is ignored:
RecurrenceRule = "FREQ=DAILY;UNTIL=20210722T000000"
According to the RFC5545 specification, this should be a valid date format.
These formats will work:
RecurrenceRule = "FREQ=DAILY;UNTIL=2021-07-22T00:00:00"
RecurrenceRule = "FREQ=DAILY;UNTIL=2021-07-22T00:00:00.000Z"
EDIT:
This is my work-around. It captures the date portion of the UNTIL clause, converts it into the date string style that Telerik can understand, then reassembles the rule stringprivate string TransformRecurrenceRule()
{
const string untilSeparator = "UNTIL=";
var ruleParts = RecurrenceRule.Split(untilSeparator, StringSplitOptions.RemoveEmptyEntries);
if (ruleParts.Length <= 1)
{
// There was no Until clause to worry about
return RecurrenceRule;
}
// Save the first part of the rule
var ruleBeginning = ruleParts[0];
// Split the date part of the until clause from any following clauses
var remainingClauses = ruleParts[1].Split(';', 2, StringSplitOptions.RemoveEmptyEntries);
//Save the date part of the until clause
var untilDate = remainingClauses[0];
// Save any following clauses with the `;` replaced
var ruleEnding = "";
if (remainingClauses.Length == 2)
{
ruleEnding = $";{remainingClauses[1]}";
}
// Convert the until date into .net parsable format
const string format = "yyyyMMddTHHmmss";
var date = DateTime.ParseExact(untilDate, format, CultureInfo.InvariantCulture);
var dateStr = date.ToString("yyyy-MM-ddTHH:mm:ss");
// recombine rule components
var newRuleParts = new[] {ruleBeginning, untilSeparator, dateStr, ruleEnding};
var newRule = string.Join("",newRuleParts);
return newRule;
}
The behavior is reproducible only in the Material and Bootstrap themes - it can be observed in the online demo.
When I scroll to a point where an appointment is not fully visible and click it, the page scrolls up. This does not happen in the other themes.
Reproduction video: https://www.loom.com/share/b8303675096d42ea8582a1638f37571f.
Once a user has clicked an appointment to select it, I'd like the ability to clear this selection. For example, if you show one set of appointments, then click a radio button on the page to switch to a different set of appointments, I'd like to clear the selection so that no appointment is selected. The way it works now, the component appears to store the index of the selected appointment and then re-apply that when it's rendered with new appointments.
More context here: https://www.telerik.com/forums/how-to-clear-the-selected-slot-in-the-scheduler
In the scheduler for Asp.net Ajax, the month view would put a k-other-month class on the cells from other months outside of the current one and those cells would have a different style (grayed day numbers or background, etc.). In the Blazor scheduler component it doesn't look like that class is being put on those cells anymore. The Telerik SCSS themes still allow for that class, so is there a way to add them or is there a workaround so that those cells can be re-styled similar to the way the Ajax Scheduler functions?
Thanks,
Mike
Hi there
The scheduler is an amazing component for Blazor, however with our designs we'd need to hijack one of the existing views and make a custom page from that which we don't really want to do if we can get this feature.
Custom Views would work just like any other view, but expose a <ViewTemplate> tag that would allow us to create our own custom view that can leverage on the power of the scheduler without doing anything hacky to achieve the same effect.
For instance a code example can be like:
<SchedulerViews>
<SchedulerMonthView />
<SchedulerDayView StartTime="@CurrentDate"></SchedulerDayView>
<SchedulerCustomView>
<ViewTemplate Context="view">
<h1>This is a custom view for the @view.month</h1>
<TelerikCalendar Date="@item.date" />
</ViewTemplate>
</SchedulerCustomView>
</SchedulerViews>
I want to easily print the Scheduler calendar - month view, week view and more.
===
ADMIN EDIT
===
A possible approach for the time being is to implement printing functionality for the Scheduler in a similar fashion to how the Grid is printed here https://github.com/telerik/blazor-ui/tree/master/grid/print.
The approach relies on using JavaScript to invoke the browser print() method. Using CSS and @media print, you can customize the page and specify which elements should be visible when printing. For example, you may hide the toolbar and footer of the component, so only the calendar is visible.
Here is a runnable sample that demonstrates the approach: https://blazorrepl.telerik.com/wfkbbBca54BdZnUk24
Now that we can have more than 2 events in a day in Month View, we need to add an AppointmentHeight parameter to be able to make the appointments taller in height to accommodate longer titles.
ADMIN EDIT
An alternative for the time being is to show the longer content by using the appointment templates and the Tooltip component. For example:
https://github.com/telerik/blazor-ui/tree/master/scheduler/appointment-tooltips
When SchedulerGroupOrientation is set to vertical, increasing the height of the `SchedulerResourceGroupHeader` breaks the rendering of the Scheduler cells, which causes misalignment of the appointments. In my scenario, I want to have a Button and an Icon in the group header cell.
Reproduction example: https://blazorrepl.telerik.com/wJElGYvF02XSsN6j42
At the moment start and end times effectively "round" to the nearest half an hour. This can give the impression of events overlapping when they do not
e.g.
Admin edit: This feature would be similar to the Exact Time Rendering in our WebForms suite: https://demos.telerik.com/aspnet-ajax/scheduler/examples/exacttimerendering/defaultcs.aspx
I'm in the same situation as the OP in this post: https://www.telerik.com/forums/how-do-you-always-edit-series-when-editing-a-recurring-appointment
In my case, I'm working with with the Blazor Scheduler Component. How can I disable the prompt and automatically select "Edit the series"? Or at least "disable" the "Edit this occurrence" button when updating/deleting an appointment?
---
ADMIN EDIT
Note: You may also check the Differentiate "edit current occurrence" and "edit the series" in the RecurrenceDialog feature request as the implementation of both features will most likely be covered in one release.
Please comment with suggestions on how you would like to see this feature exposed. Ideally it might have to be dynamic so you can toggle it based on conditions, so perhaps it could be a parameter (not flexible), an event argument to OnEdit, or a separate event/setting.
Here is a sample code you can try to achieve this - comments in the code explain how the templates are used to capture the edit action and some JS is used to fake-click the button to simulate a user choice:
@inject IJSRuntime _jsInterop
@* this errors suppression is a hack to make this sample succinct
move this script to a proper place for a real app*@
<script suppress-error="BL9992">
function clickSchedulerPromptButton(btnIndex) {
setTimeout(function () {
var buttons = document.querySelectorAll(".k-window-content .text-right button");
if (buttons && buttons.length >= btnIndex) {
var chosenButton = buttons[btnIndex];
chosenButton.click();
}
}, 50);
}
</script>
@* appearance setting for the template - make the custom template tall as the appointment to capture all clicks *@
<style>
.tallAppt {
height: 100%;
}
</style>
<TelerikScheduler Data="@Appointments"
OnUpdate="@UpdateAppointment"
OnCreate="@AddAppointment"
OnDelete="@DeleteAppointment"
AllowCreate="true" AllowDelete="true" AllowUpdate="true"
@bind-Date="@StartDate" Height="600px" @bind-View="@CurrView">
<ItemTemplate>
@{
SchedulerAppointment appt = context as SchedulerAppointment;
}
<div title="@appt.Start - @appt.End" class="tallAppt" @ondblclick="@( () => ChooseEditMode(appt) )"><div class="k-event-template">@appt.Title</div></div>
</ItemTemplate>
<AllDayItemTemplate>
@{
SchedulerAppointment appt = context as SchedulerAppointment;
}
<div title="@appt.Start.ToShortDateString() - @appt.Title" class="tallAppt" @ondblclick="@( () => ChooseEditMode(appt) )"><div class="k-event-template">@appt.Title</div></div>
</AllDayItemTemplate>
<SchedulerViews>
<SchedulerDayView StartTime="@DayStart" />
<SchedulerWeekView StartTime="@DayStart" />
<SchedulerMultiDayView StartTime="@DayStart" NumberOfDays="10" />
</SchedulerViews>
</TelerikScheduler>
@code {
//async void so we don't block the execution
//we will have a small timeout in the script to let it wait for the popup
async void ChooseEditMode(SchedulerAppointment appt)
{
// check if we have a recurring appointment or a member of one
if (appt.RecurrenceId != null || !string.IsNullOrEmpty(appt.RecurrenceRule))
{
int btnIndexToClick = 0;//the first button - edit instance
// make it 1 for the second button - the series
await _jsInterop.InvokeVoidAsync("clickSchedulerPromptButton", btnIndexToClick);
}
}
//the rest is sample data and sample CUD operations handling
// sample data and scheduler settings
public SchedulerView CurrView { get; set; } = SchedulerView.Week;
public DateTime StartDate { get; set; } = new DateTime(2019, 12, 2);
public DateTime DayStart { get; set; } = new DateTime(2000, 1, 1, 8, 0, 0); //the time portion is important
List<SchedulerAppointment> Appointments { get; set; }
async Task UpdateAppointment(SchedulerUpdateEventArgs args)
{
SchedulerAppointment item = (SchedulerAppointment)args.Item;
await MyService.Update(item);
await GetSchedulerData();
}
async Task AddAppointment(SchedulerCreateEventArgs args)
{
SchedulerAppointment item = args.Item as SchedulerAppointment;
await MyService.Create(item);
await GetSchedulerData();
}
async Task DeleteAppointment(SchedulerDeleteEventArgs args)
{
SchedulerAppointment item = (SchedulerAppointment)args.Item;
await MyService.Delete(item);
await GetSchedulerData();
}
public class SchedulerAppointment
{
public Guid Id { get; set; }
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 string RecurrenceRule { get; set; }
public List<DateTime> RecurrenceExceptions { get; set; }
public Guid? RecurrenceId { get; set; }
public SchedulerAppointment()
{
Id = Guid.NewGuid();
}
}
async Task GetSchedulerData()
{
Appointments = await MyService.Read();
}
protected override async Task OnInitializedAsync()
{
await GetSchedulerData();
}
// the following static class mimics an actual data service that handles the actual data source
// replace it with your actual service through the DI, this only mimics how the API can look like and works for this standalone page
public static class MyService
{
private static List<SchedulerAppointment> _data { get; set; } = new List<SchedulerAppointment>()
{
new SchedulerAppointment
{
Title = "Board meeting",
Description = "Q4 is coming to a close, review the details.",
Start = new DateTime(2019, 12, 5, 10, 00, 0),
End = new DateTime(2019, 12, 5, 11, 30, 0)
},
new SchedulerAppointment
{
Title = "Vet visit",
Description = "The cat needs vaccinations and her teeth checked.",
Start = new DateTime(2019, 12, 2, 11, 30, 0),
End = new DateTime(2019, 12, 2, 12, 0, 0)
},
new SchedulerAppointment
{
Title = "Planning meeting",
Description = "Kick off the new project.",
Start = new DateTime(2019, 12, 6, 9, 30, 0),
End = new DateTime(2019, 12, 6, 12, 45, 0)
},
new SchedulerAppointment
{
Title = "Trip to Hawaii",
Description = "An unforgettable holiday!",
IsAllDay = true,
Start = new DateTime(2019, 11, 27),
End = new DateTime(2019, 12, 05)
},
new SchedulerAppointment
{
Title = "Morning run",
Description = "Some time to clear the head and exercise.",
Start = new DateTime(2019, 11, 27, 9, 0, 0),
End = new DateTime(2019, 11, 27, 9, 30, 0),
RecurrenceRule = "FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR"
}
};
public static async Task Create(SchedulerAppointment itemToInsert)
{
itemToInsert.Id = Guid.NewGuid();
_data.Insert(0, itemToInsert);
}
public static async Task<List<SchedulerAppointment>> Read()
{
return await Task.FromResult(_data);
}
public static async Task Update(SchedulerAppointment itemToUpdate)
{
var index = _data.FindIndex(i => i.Id == itemToUpdate.Id);
if (index != -1)
{
_data[index] = itemToUpdate;
}
}
public static async Task Delete(SchedulerAppointment itemToDelete)
{
if (itemToDelete.RecurrenceId != null)
{
// a recurrence exception was deleted, you may want to update
// the rest of the data source - find an item where theItem.Id == itemToDelete.RecurrenceId
// and remove the current exception date from the list of its RecurrenceExceptions
}
if (!string.IsNullOrEmpty(itemToDelete.RecurrenceRule) && itemToDelete.RecurrenceExceptions?.Count > 0)
{
// a recurring appointment was deleted that had exceptions, you may want to
// delete or update any exceptions from the data source - look for
// items where theItem.RecurrenceId == itemToDelete.Id
}
_data.Remove(itemToDelete);
}
}
}
---
I'd like to have a data virtualization feature (similar to the OnRead event), so I can load only small chunks of data (appointments) specifically for the current page/view.
===
ADMIN EDIT
===
Currently, you can achieve similar functionality by handling the DateChanged and ViewChanged events of the Scheduler to load only the relevant appointments for the selected period.
You can find an example here: Load Scheduler Appointments on Demand.