FullCalendar
FullCalendar is a full-size event calendar. It wraps the open-source FullCalendar JavaScript library, version 3.9.0, and the optional Scheduler 1.9.4 plug-in for resource timelines. It shows events in month, week, day and list views. Users can drag and resize events, and every change comes back to the server as a typed .NET event.
Use it for appointment books, room and equipment bookings, shift plans, or anywhere a user needs to see and move events on a calendar.
Installation
Add the Wisej-4-FullCalendar package to your Wisej.NET project, either with the NuGet Package Manager or from the command line:
dotnet add package Wisej-4-FullCalendar
The package targets .NET Framework 4.8 and .NET 8 or later (including the -windows, Android, iOS and Mac Catalyst targets). After a rebuild, FullCalendar appears in the Visual Studio toolbox.
Everything the calendar needs on the client is embedded in the assembly: FullCalendar 3.9.0, jQuery 3.1.1, moment.js 2.17.1 with its locales and, when you enable it, the Scheduler plug-in. There's nothing to load from a CDN.
The core calendar is MIT-licensed. The Scheduler plug-in, which provides the four Timeline* views and the resources, is a separately licensed FullCalendar product. To enable it, set SchedulerLicenseKey:
"GPL-My-Project-Is-Open-Source"for projects released under the GPL;"CC-Attribution-NonCommercial-NoDerivatives"for non-commercial projects;- the key of a commercial license, bought from fullcalendar.io/scheduler, for everything else.
A Wisej.NET license doesn't cover the Scheduler plug-in.
Getting started
This example creates a calendar in the agenda week view, adds a timed event and an all-day event, and shows the title of an event when the user clicks it.
- C#
- VB.NET
using Wisej.Web.Ext.FullCalendar;
var calendar = new FullCalendar
{
Dock = DockStyle.Fill,
View = ViewType.AgendaWeek
};
// A one-hour event today at 9:00.
calendar.Events.Add(new Event(DateTime.Today.AddHours(9), TimeSpan.FromHours(1)) { Title = "Stand-up" });
// An all-day event tomorrow. Add() returns the new event.
calendar.Events.Add(DateTime.Today.AddDays(1)).Title = "Company holiday";
calendar.EventClick += (s, e) => AlertBox.Show(e.Event.Title);
this.Controls.Add(calendar);
Imports Wisej.Web.Ext.FullCalendar
Dim calendar As New FullCalendar With {
.Dock = DockStyle.Fill,
.View = ViewType.AgendaWeek
}
' A one-hour event today at 9:00.
calendar.Events.Add(New [Event](DateTime.Today.AddHours(9), TimeSpan.FromHours(1)) With {.Title = "Stand-up"})
' An all-day event tomorrow. Add() returns the new event.
calendar.Events.Add(DateTime.Today.AddDays(1)).Title = "Company holiday"
AddHandler calendar.EventClick, Sub(s, e) AlertBox.Show(e.Event.Title)
Me.Controls.Add(calendar)
The calendar doesn't show FullCalendar's own header toolbar with the title and the prev / next / today buttons. Build the toolbar you want with regular Wisej.NET controls and call the navigation methods described in Views and navigation. That way the toolbar matches the rest of your application and can be localized and themed with it.
Views and navigation
View selects one of the ViewType values. The default is Month. Changing the view after the calendar is created switches it on the client without rebuilding it.
| ViewType | What it shows |
|---|---|
Month | A month grid, one cell per day. |
BasicWeek / BasicDay | A week or a day as simple day cells, without time slots. |
AgendaWeek / AgendaDay | A week or a day with an all-day row and vertical time slots. |
ListYear / ListMonth / ListWeek / ListDay | A flat list of the events in the period. NoEventsMessage is shown when there are none. |
TimelineDay / TimelineWeek / TimelineMonth / TimelineYear | A horizontal time axis with one row per resource. Requires the Scheduler plug-in. |
To move through time, call Previous() and Next() (one month, week or day, depending on the view), PreviousYear() and NextYear(), Today() and GotoDate(), or set CurrentDate. Each move fires CurrentDateChanged, which is the place to update a title label.
- C#
- VB.NET
private void buttonPrev_Click(object sender, EventArgs e) => this.fullCalendar1.Previous();
private void buttonNext_Click(object sender, EventArgs e) => this.fullCalendar1.Next();
private void buttonToday_Click(object sender, EventArgs e) => this.fullCalendar1.Today();
private void buttonWeek_Click(object sender, EventArgs e) => this.fullCalendar1.View = ViewType.AgendaWeek;
private void fullCalendar1_CurrentDateChanged(object sender, EventArgs e)
{
this.labelTitle.Text = this.fullCalendar1.CurrentDate.ToString("MMMM yyyy");
}
Private Sub buttonPrev_Click(sender As Object, e As EventArgs) Handles buttonPrev.Click
Me.FullCalendar1.Previous()
End Sub
Private Sub buttonNext_Click(sender As Object, e As EventArgs) Handles buttonNext.Click
Me.FullCalendar1.Next()
End Sub
Private Sub buttonToday_Click(sender As Object, e As EventArgs) Handles buttonToday.Click
Me.FullCalendar1.Today()
End Sub
Private Sub buttonWeek_Click(sender As Object, e As EventArgs) Handles buttonWeek.Click
Me.FullCalendar1.View = ViewType.AgendaWeek
End Sub
Private Sub FullCalendar1_CurrentDateChanged(sender As Object, e As EventArgs) Handles FullCalendar1.CurrentDateChanged
Me.labelTitle.Text = Me.FullCalendar1.CurrentDate.ToString("MMMM yyyy")
End Sub
Previous(), Next(), Today() and the other navigation methods run on the client. CurrentDate changes only when the client reports the new date, so it still holds the old value right after the call returns. Read it in CurrentDateChanged, not in the next line of code.
TodayDate is the date the calendar treats as "today": the highlighted day, the current-time marker (ShowCurrentTime) and the target of Today(). Until you set it, it's DateTime.Now on the server.
If the server and the users aren't in the same time zone, set TodayDate to the user's local time. Otherwise "today" and the current-time marker follow the server's clock.
Events
The calendar's Events collection holds Event objects. Adding, removing or changing an event reloads the visible range on the client after the current request, so you can change many events in one go without calling anything else.
EventCollection.Add has overloads for the usual cases, and each returns the new event: a single day (an all-day event), a start and an end, or a start and a TimeSpan. Each can also take an ID first. An event without an ID gets one automatically. Adding an event with the ID of an existing one replaces it, and the string indexer finds it again:
- C#
- VB.NET
this.fullCalendar1.Events.Add("meeting1", new DateTime(2026, 10, 5, 10, 0, 0), TimeSpan.FromMinutes(90)).Title = "Design review";
this.fullCalendar1.Events.Add(new Event("vacation", new DateTime(2026, 10, 12)) { Title = "Vacation", BackgroundColor = Color.SeaGreen });
var ev = this.fullCalendar1.Events["meeting1"];
if (ev != null)
ev.Start = ev.Start.AddHours(1);
Me.FullCalendar1.Events.Add("meeting1", New DateTime(2026, 10, 5, 10, 0, 0), TimeSpan.FromMinutes(90)).Title = "Design review"
Me.FullCalendar1.Events.Add(New [Event]("vacation", New DateTime(2026, 10, 12)) With {.Title = "Vacation", .BackgroundColor = Color.SeaGreen})
Dim ev = Me.FullCalendar1.Events("meeting1")
If ev IsNot Nothing Then ev.Start = ev.Start.AddHours(1)
Event properties
| Property | What it does |
|---|---|
Id | Unique key within the calendar. Assigned automatically when empty. |
Title | The text shown on the event. |
Start / End | When the event starts and ends. End is exclusive: an all-day event on October 5 and 6 ends on October 7. Both are updated when the user drags or resizes the event. |
AllDay | Whether the event covers whole days. The single-day constructors create all-day events. |
Editable | Overrides the calendar's Editable for this event. |
BackgroundColor / BorderColor / TextColor | Colors for this event. They override the calendar-wide EventBackgroundColor, EventBorderColor and EventTextColor. |
ClassName | One or more CSS classes added to the event's element. Define them in a style sheet your application loads. |
ToolTipText | The tooltip, shown when ShowEventToolTips is true. It falls back to Title. |
ResourceId | The scheduler resource the event belongs to. |
UserData | A dynamic object for your own data, such as the database key of the appointment. |
Store your record's key in UserData (for example ev.UserData.AppointmentId = 1234) and read it back in EventClick or EventChanged. You don't have to encode it in Id or look the event up by title.
Drag, resize and EventChanged
When Editable is true (the default), users can move events and change their duration. The calendar updates the event's Start, End and AllDay, then fires EventChanged with the EventValueEventArgs: the event, plus its OldStartDate and OldEndDate. That's where you save the change, or reject it:
- C#
- VB.NET
private void fullCalendar1_EventChanged(object sender, EventValueEventArgs e)
{
// TryMove is your own booking logic.
if (!this.bookings.TryMove(e.Event.UserData.BookingId, e.Event.Start, e.Event.End))
{
// Put the event back where it was.
e.Event.Start = e.OldStartDate;
e.Event.End = e.OldEndDate;
AlertBox.Show("That slot is already taken.", MessageBoxIcon.Warning);
}
}
Private Sub FullCalendar1_EventChanged(sender As Object, e As EventValueEventArgs) Handles FullCalendar1.EventChanged
' TryMove is your own booking logic.
If Not Me.bookings.TryMove(e.Event.UserData.BookingId, e.Event.Start, e.Event.End) Then
' Put the event back where it was.
e.Event.Start = e.OldStartDate
e.Event.End = e.OldEndDate
AlertBox.Show("That slot is already taken.", MessageBoxIcon.Warning)
End If
End Sub
EventChanged also fires when your code changes Start or End of an event in the collection, including the two assignments in the example above. Guard against re-entering your save logic if that matters.
Loading events on demand
For a large calendar, don't fill Events with every appointment you have. Set VirtualMode to true and supply only the events of the visible range when the calendar asks for them:
VirtualEventsNeededfires each time the visible range changes. Its arguments giveStartDateandEndDate, and you setEventsto the events in that range.RetrieveVirtualEventfires when the calendar needs one event by ID or by index, which happens whenever the user clicks, drags or resizes an event. Sete.Event.
- C#
- VB.NET
this.fullCalendar1.VirtualMode = true;
this.fullCalendar1.VirtualEventsNeeded += (s, e) =>
{
// LoadAppointments is your own data access method.
e.Events = LoadAppointments(e.StartDate, e.EndDate)
.Select(a => new Event(a.Id.ToString(), a.Start, a.End) { Title = a.Subject });
};
this.fullCalendar1.RetrieveVirtualEvent += (s, e) =>
{
var a = LoadAppointment(e.EventID);
e.Event = new Event(a.Id.ToString(), a.Start, a.End) { Title = a.Subject };
};
Me.FullCalendar1.VirtualMode = True
AddHandler Me.FullCalendar1.VirtualEventsNeeded,
Sub(s, e)
' LoadAppointments is your own data access method.
e.Events = LoadAppointments(e.StartDate, e.EndDate).
Select(Function(a) New [Event](a.Id.ToString(), a.Start, a.End) With {.Title = a.Subject})
End Sub
AddHandler Me.FullCalendar1.RetrieveVirtualEvent,
Sub(s, e)
Dim a = LoadAppointment(e.EventID)
e.Event = New [Event](a.Id.ToString(), a.Start, a.End) With {.Title = a.Subject}
End Sub
In virtual mode the Events collection stores nothing and can't be modified, and turning VirtualMode on clears it. If RetrieveVirtualEvent isn't handled, clicking or dragging an event has no event to work with. Set VirtualSize if you also read Events.Count or use the index-based indexer.
Time slots and layout
The views with time slots (agenda and timeline) are shaped by these properties. Most of them rebuild the calendar on the client when changed, so set them in the designer or before the page is shown.
| Property | Default | What it does |
|---|---|---|
MinTime / MaxTime | 00:00 / 24:00 | The first and last (exclusive) time slot shown each day. |
ScrollTime | 06:00 | The time the view is initially scrolled to. |
SlotDuration | 30 minutes | The length of one slot. |
SlotLabelInterval | 1 hour | How often the time axis is labeled. |
SlotLabelFormat / TimeFormat | locale | moment.js format strings for the axis labels and for the time on each event, for example "HH:mm". |
HeaderFormats | locale | moment.js formats for the column headers of each view. |
FirstDayOfWeek | Default | The first column of the week views. |
BusinessHours | none | Time ranges to emphasize, as an array of BusinessHours, each with Start, End and Days. |
AllDaySlot / AllDayText | true / "All Day" | Whether the agenda views show the all-day row, and its title. |
SlotEventOverlap | true | Whether overlapping timed events overlap visually or sit side by side. |
EventLimit | 0 (no limit) | The maximum number of events per day. Extra events collapse into a "+2 more" link that opens a popover. |
NextDayThreshold | 09:00 | How late an event must end on the next day to be drawn on that day. |
- C#
- VB.NET
// Working hours only, 15-minute slots, Monday to Friday 8:00–18:00 emphasized.
this.fullCalendar1.MinTime = new TimeSpan(7, 0, 0);
this.fullCalendar1.MaxTime = new TimeSpan(19, 0, 0);
this.fullCalendar1.ScrollTime = new TimeSpan(8, 0, 0);
this.fullCalendar1.SlotDuration = TimeSpan.FromMinutes(15);
this.fullCalendar1.SlotLabelFormat = "HH:mm";
this.fullCalendar1.BusinessHours = new[] {
new BusinessHours {
Start = new TimeSpan(8, 0, 0),
End = new TimeSpan(18, 0, 0),
Days = new[] { DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday }
}
};
' Working hours only, 15-minute slots, Monday to Friday 8:00–18:00 emphasized.
Me.FullCalendar1.MinTime = New TimeSpan(7, 0, 0)
Me.FullCalendar1.MaxTime = New TimeSpan(19, 0, 0)
Me.FullCalendar1.ScrollTime = New TimeSpan(8, 0, 0)
Me.FullCalendar1.SlotDuration = TimeSpan.FromMinutes(15)
Me.FullCalendar1.SlotLabelFormat = "HH:mm"
Me.FullCalendar1.BusinessHours = {
New BusinessHours With {
.Start = New TimeSpan(8, 0, 0),
.End = New TimeSpan(18, 0, 0),
.Days = {DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday}
}
}
BusinessHours and Resources are arrays. Assign a new array to apply a change: editing an item of the current array doesn't update the calendar.
Day and month names follow the Wisej.NET application's current language, and the calendar is mirrored when RightToLeft is set to Yes.
Scheduler resources
With the Scheduler plug-in enabled, Resources defines the rows of the timeline views: rooms, people, machines. Each SchedulerResource has an Id and a Title, default colors for its events, and optional Children for grouped resources. An event goes on a resource's row through its ResourceId.
- C#
- VB.NET
this.fullCalendar1.SchedulerLicenseKey = "GPL-My-Project-Is-Open-Source";
this.fullCalendar1.ResourceLabelText = "Rooms";
this.fullCalendar1.Resources = new[] {
new SchedulerResource("a") { Title = "Auditorium A", EventBackgroundColor = Color.SteelBlue },
new SchedulerResource("b") { Title = "Auditorium B", EventBackgroundColor = Color.IndianRed }
};
this.fullCalendar1.View = ViewType.TimelineDay;
this.fullCalendar1.Events.Add(DateTime.Today.AddHours(10), TimeSpan.FromHours(2)).ResourceId = "a";
Me.FullCalendar1.SchedulerLicenseKey = "GPL-My-Project-Is-Open-Source"
Me.FullCalendar1.ResourceLabelText = "Rooms"
Me.FullCalendar1.Resources = {
New SchedulerResource("a") With {.Title = "Auditorium A", .EventBackgroundColor = Color.SteelBlue},
New SchedulerResource("b") With {.Title = "Auditorium B", .EventBackgroundColor = Color.IndianRed}
}
Me.FullCalendar1.View = ViewType.TimelineDay
Me.FullCalendar1.Events.Add(DateTime.Today.AddHours(10), TimeSpan.FromHours(2)).ResourceId = "a"
The Scheduler scripts are added to the calendar's client packages the first time the packages are built. Set SchedulerLicenseKey in the designer, or in code before the calendar is created on the client, and before you switch View to a timeline view. Setting it later doesn't load the plug-in.
ResourceAreaWidth (a CSS width, "30%" by default) and ResourceLabelText shape the resource column. Changing a resource's properties later fires ResourceChanged but doesn't redraw the calendar.
Dropping controls on the calendar
ItemDrop fires when the user drops a Wisej.NET control on the calendar, for example an entry of a list of unscheduled tasks. ItemDropEventArgs gives the dropped control as Item, the date and time under the pointer as Day and, in a timeline view, the ResourceId of the row. It uses the standard Wisej.NET drag and drop: set AllowDrag on the source and AllowDrop on the calendar. See Drag & Drop.
Events of the control
| Event | When |
|---|---|
EventClick / EventDoubleClick | The user clicks or double-clicks an event. e.Event is the event. |
DayClick / DayDoubleClick | The user clicks an empty day or time slot. e.Day is the date and time, which is handy for "new appointment here". |
EventChanged | An event was moved or resized, by the user or by code. |
EventMouseEnter / EventMouseLeave | The pointer enters or leaves an event. |
CurrentDateChanged | The displayed date range changed. |
ItemDrop | A Wisej.NET control was dropped on the calendar. |
VirtualEventsNeeded / RetrieveVirtualEvent | Virtual mode needs events. See Loading events on demand. |
ResourceChanged | A scheduler resource changed. |
Styling
- Calendar-wide event colors:
EventBackgroundColor,EventBorderColorandEventTextColor, overridden per resource and per event. - CSS classes:
Event.ClassNameandSchedulerResource.EventClassName, for rules in your own style sheet. ThemeSystem:Standard(the default) renders FullCalendar's own look.Bootstrap3andjQueryUIuse a Bootstrap 3 or jQuery UI theme instead, but you must load that CSS file into the page yourself.
The extension's FullCalendar.mixin.theme only gives the control the standard widget appearance. The calendar's inner look comes from FullCalendar's CSS.
API reference
FullCalendar, which derives fromWidgetEventandEventCollectionSchedulerResourceandBusinessHoursViewType,ThemeSystemandColumnHeaderFormats- The whole
Wisej.Web.Ext.FullCalendarnamespace