I am trying to use Telerik Blazor Scheduler to display a Google Calendar. Need to be able to add, edit, delete events from Scheduler. Need to be able to manage recurrence events. I am new to Telerik Scheduler and running into a few issues that might be easily spotted to someone else.
The code will connect to the Google Calendar API and access the data. There is issues with recurrence event as they do not stop when the "UNTIL" directs the event to stop. They go on forever. Also issues when it comes when updating and adding new events that need to be set as recurring.
- There is one recurring event in the Google Calendar that is set for Wed Dec 4 2024 1:15PM to 2:15PM. Set to repeat on Wednesdays and set to stop on 12/12/24. So it is really only 2 Wednesdays 12/4 and 12/11. Scheduler is showing this event every Wednesday forever.
- Also, based on the Console output, it looks like it is pulling the information twice upon page load.
- In Postman, the api seems to show the output correctly.
- If you try to create a new event or update an event with recurrence settings, you get an error. You can add/update a non-recurring event just fine.
Below is the code. (You may see some trial code and debuging items as I have been trying to figure out why)
Any help you can provide would be greatly appriciated.
RAZOR PAGE:
@page "/gscheduler"
@rendermode RenderMode.InteractiveServer
@using Telerik.Blazor
@using Telerik.Blazor.Components
@using Telerik.Blazor.Components.Scheduler
@inject GSchedulerService GSchedulerService
<TelerikRootComponent>
<TelerikScheduler Height="600px"
@bind-Date="@StartDate"
@bind-View="@CurrView"
Data="@GSchedulerService.Appointments"
OnUpdate="OnUpdateHandler"
OnCreate="OnCreateHandler"
OnDelete="OnDeleteHandler"
AllowCreate="true"
AllowDelete="true"
AllowUpdate="true"
IdField="@(nameof(GSchedulerDto.Id))"
RecurrenceRuleField="@(nameof(GSchedulerDto.RecurrenceRule))"
RecurrenceExceptionsField="@(nameof(GSchedulerDto.RecurrenceExceptions))"
RecurrenceIdField="@(nameof(GSchedulerDto.RecurrenceId))">
<SchedulerViews>
<SchedulerDayView StartTime="@DayStart" />
<SchedulerWeekView StartTime="@DayStart" />
<SchedulerMultiDayView StartTime="@DayStart" />
<SchedulerMonthView></SchedulerMonthView>
<SchedulerTimelineView StartTime="@DayStart" />
<SchedulerAgendaView></SchedulerAgendaView>
</SchedulerViews>
</TelerikScheduler>
</TelerikRootComponent>
@code {
private DateTime StartDate { get; set; } = DateTime.Today;
private SchedulerView CurrView { get; set; } = SchedulerView.Week;
private DateTime DayStart { get; set; } = new DateTime(2000, 1, 1, 7, 0, 0);
// Initialize Scheduler Data
protected override async Task OnInitializedAsync()
{
await GSchedulerService.InitializeAppointmentsAsync();
}
// Create Event Handler
private async Task OnCreateHandler(SchedulerCreateEventArgs args)
{
if (args?.Item is GSchedulerDto newItem)
{
await GSchedulerService.CreateEventAsync(newItem);
}
}
// Update Event Handler
private async Task OnUpdateHandler(SchedulerUpdateEventArgs args)
{
if (args?.Item is GSchedulerDto updatedItem)
{
await GSchedulerService.UpdateEventAsync(updatedItem);
}
}
// Delete Event Handler
private async Task OnDeleteHandler(SchedulerDeleteEventArgs args)
{
if (args?.Item is GSchedulerDto itemToDelete && await GSchedulerService.ConfirmDeletionAsync())
{
await GSchedulerService.DeleteEventAsync(itemToDelete.Id);
}
}
}
CS file:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Google.Apis.Auth.OAuth2;
using Google.Apis.Calendar.v3;
using Google.Apis.Calendar.v3.Data;
using Google.Apis.Services;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Configuration;
using Microsoft.JSInterop;
using Telerik.SvgIcons;
public class GSchedulerService
{
private readonly IConfiguration _configuration;
private readonly IJSRuntime _jsRuntime;
public List<GSchedulerDto> Appointments { get; private set; } = new();
public GSchedulerService(IConfiguration configuration, IJSRuntime jsRuntime)
{
_configuration = configuration;
_jsRuntime = jsRuntime;
}
public async Task InitializeAppointmentsAsync()
{
await LoadEventsAsync();
// await PrintEventsAsync();
}
public async Task LoadEventsAsync()
{
var service = await GetCalendarServiceAsync();
var events = await service.Events.List(_configuration["Google:CalendarId"]).ExecuteAsync();
foreach (var e in events.Items)
{
Console.WriteLine($"Event: {e.Summary}");
Console.WriteLine($"Start: {e.Start.DateTime ?? DateTime.Parse(e.Start.Date)}");
Console.WriteLine($"End: {e.End.DateTime ?? DateTime.Parse(e.End.Date)}");
Console.WriteLine($"Recurrence Rule: {e.Recurrence?.FirstOrDefault()}");
Console.WriteLine($"Recurrence ID: {e.RecurringEventId}");
Console.WriteLine($"Description: {e.Description}");
Console.WriteLine(new string('-', 40));
}
Appointments = events.Items.Select(e => new GSchedulerDto
{
Id = e.Id,
Title = e.Summary,
Description = e.Description,
Start = e.Start.DateTime ?? DateTime.Parse(e.Start.Date),
End = e.End.DateTime ?? DateTime.Parse(e.End.Date),
RecurrenceRule = FormatRecurrenceRuleForDisplay(e.Recurrence?.FirstOrDefault()),
RecurrenceId = e.RecurringEventId // Make sure to assign RecurrenceId
}).ToList();
}
public async Task PrintEventsAsync()
{
var service = await GetCalendarServiceAsync();
var events = await service.Events.List(_configuration["Google:CalendarId"]).ExecuteAsync();
foreach (var e in events.Items)
{
Console.WriteLine($"Event: {e.Summary}");
Console.WriteLine($"Start: {e.Start.DateTime ?? DateTime.Parse(e.Start.Date)}");
Console.WriteLine($"End: {e.End.DateTime ?? DateTime.Parse(e.End.Date)}");
Console.WriteLine($"Recurrence Rule: {e.Recurrence?.FirstOrDefault()}");
Console.WriteLine($"Recurrence ID: {e.RecurringEventId}");
Console.WriteLine($"Description: {e.Description}");
Console.WriteLine(new string('-', 40));
}
}
public async Task CreateEventAsync(GSchedulerDto appointment)
{
var service = await GetCalendarServiceAsync();
var newEvent = new Event
{
Summary = appointment.Title,
Description = appointment.Description,
Start = new EventDateTime
{
DateTime = appointment.Start,
TimeZone = "UTC"
},
End = new EventDateTime
{
DateTime = appointment.End,
TimeZone = "UTC"
},
Recurrence = !string.IsNullOrEmpty(appointment.RecurrenceRule)
? new List<string> { SanitizeRecurrenceRule(appointment.RecurrenceRule) }
: null
};
Console.WriteLine($"Creating event with recurrence rule: {newEvent.Recurrence?.FirstOrDefault()}");
await service.Events.Insert(newEvent, _configuration["Google:CalendarId"]).ExecuteAsync();
await LoadEventsAsync();
}
public async Task UpdateEventAsync(GSchedulerDto appointment)
{
var service = await GetCalendarServiceAsync();
var existingEvent = await service.Events.Get(_configuration["Google:CalendarId"], appointment.Id).ExecuteAsync();
existingEvent.Summary = appointment.Title;
existingEvent.Description = appointment.Description;
existingEvent.Start = new EventDateTime
{
DateTime = appointment.Start,
TimeZone = "UTC"
};
existingEvent.End = new EventDateTime
{
DateTime = appointment.End,
TimeZone = "UTC"
};
existingEvent.Recurrence = !string.IsNullOrEmpty(appointment.RecurrenceRule)
? new List<string> { SanitizeRecurrenceRule(appointment.RecurrenceRule) }
: null;
Console.WriteLine($"Updating event with recurrence rule: {existingEvent.Recurrence?.FirstOrDefault()}");
await service.Events.Update(existingEvent, _configuration["Google:CalendarId"], appointment.Id).ExecuteAsync();
await LoadEventsAsync();
}
public async Task DeleteEventAsync(string eventId)
{
var service = await GetCalendarServiceAsync();
await service.Events.Delete(_configuration["Google:CalendarId"], eventId).ExecuteAsync();
await LoadEventsAsync();
}
public async Task<bool> ConfirmDeletionAsync()
{
return await _jsRuntime.InvokeAsync<bool>("confirm", "Are you sure you want to delete this event?");
}
private async Task<CalendarService> GetCalendarServiceAsync()
{
var initializer = new ClientSecrets
{
ClientId = _configuration["Google:ClientId"],
ClientSecret = _configuration["Google:ClientSecret"]
};
var credential = await GoogleWebAuthorizationBroker.AuthorizeAsync(
initializer,
new[] { CalendarService.Scope.Calendar },
_configuration["Google:User"],
CancellationToken.None);
return new CalendarService(new BaseClientService.Initializer
{
HttpClientInitializer = credential,
ApplicationName = _configuration["Google:ApplicationName"]
});
}
private string SanitizeRecurrenceRule(string rule)
{
if (string.IsNullOrEmpty(rule))
return null;
// Normalize case and remove extra spaces
rule = rule.ToUpperInvariant().Trim();
rule = Regex.Replace(rule, @"\s+", " "); // Remove extra spaces
// Ensure the rule starts with "RRULE:"
if (!rule.StartsWith("RRULE:"))
rule = "RRULE:" + rule;
// Handle UNTIL formatting
var untilMatch = Regex.Match(rule, @"UNTIL=(\d{8}T\d{6}Z)");
if (untilMatch.Success)
{
// If UNTIL exists, ensure it is correctly formatted
var untilValue = untilMatch.Groups[1].Value;
if (!DateTime.TryParseExact(untilValue, "yyyyMMdd'T'HHmmss'Z'", null, System.Globalization.DateTimeStyles.AdjustToUniversal, out _))
{
throw new FormatException($"Invalid UNTIL format: {untilValue}. Expected format is 'YYYYMMDDTHHMMSSZ'.");
}
}
else if (!rule.Contains("COUNT=")) // If neither UNTIL nor COUNT is present, add a default UNTIL
{
var defaultUntil = DateTime.UtcNow.AddYears(1).ToString("yyyyMMdd'T'HHmmss'Z'");
rule += $";UNTIL={defaultUntil}";
}
// Optionally validate additional fields like FREQ, INTERVAL, COUNT, etc.
return rule;
}
private string FormatRecurrenceRuleForDisplay(string rule)
{
if (string.IsNullOrEmpty(rule))
return string.Empty;
// Remove the "RRULE:" prefix for display purposes
rule = rule.StartsWith("RRULE:") ? rule.Substring(6) : rule;
// Convert the UNTIL field to a human-readable format
var untilMatch = Regex.Match(rule, @"UNTIL=(\d{8}T\d{6}Z)");
if (untilMatch.Success)
{
var untilValue = untilMatch.Groups[1].Value;
if (DateTime.TryParseExact(untilValue, "yyyyMMdd'T'HHmmss'Z'", null, System.Globalization.DateTimeStyles.AdjustToUniversal, out var untilDate))
{
// Replace the UNTIL field with a readable format
rule = rule.Replace($"UNTIL={untilValue}", $"UNTIL={untilDate:yyyyMMddTHHmmssZ}");
}
}
Console.WriteLine($"Formatted Recurrence Rule: {rule}");
return rule;
}
}
public class GSchedulerDto
{
public string 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 string RecurrenceId { get; set; }
public string RecurrenceExceptions { get; set; }
// public List<DateTime>? RecurrenceExceptions { get; set; }
public string RecurrenceType { get; set; } // Recurrence type: Daily, Weekly, Monthly, Yearly
public int Interval { get; set; } // Interval for recurrence (e.g., every X days/weeks)
public int? Count { get; set; } // Number of occurrences (optional)
public DateTime? Until { get; set; } // End date for recurrence (optional)
public string ByDay { get; set; } // Optional: Specific days (e.g., "MO,WE,FR")
public int? ByMonthDay { get; set; } // Optional: Specific day of the month
public int? ByMonth { get; set; } // Optional: Specific month
}
CONSOLE OUTPUT:
Start: 12/4/2024 1:15:00 PM
End: 12/4/2024 2:15:00 PM
Recurrence Rule: RRULE:FREQ=WEEKLY;WKST=SU;UNTIL=20241212T045959Z;BYDAY=WE
Recurrence ID:
Description:
----------------------------------------
Formatted Recurrence Rule: FREQ=WEEKLY;WKST=SU;UNTIL=20241212T045959Z;BYDAY=WE
Event: test
Start: 12/4/2024 1:15:00 PM
End: 12/4/2024 2:15:00 PM
Recurrence Rule: RRULE:FREQ=WEEKLY;WKST=SU;UNTIL=20241212T045959Z;BYDAY=WE
Recurrence ID:
Description:
----------------------------------------
Formatted Recurrence Rule: FREQ=WEEKLY;WKST=SU;UNTIL=20241212T045959Z;BYDAY=WE