Features
This tutorial shows how to create, display and edit recurring events using DayPilot ASP.NET MVC Scheduler control.
ASP.NET MVC 5
Recurrence rules: daily, weekly, monthly, yearly
Rule exceptions (an occurrence has a different text, start date, or duration; occurrence doesn't happen)
Stores the recurrence data in a single database field
Sample recurrence definition UI (modal dialog with rule options)
Allows editing the series or a specific occurrence
Visual Studio Solution
C# source code
VB.NET source code
SQL Server database (LocalDB)
Includes DayPilot Pro for ASP.NET MVC Trial version
This tutorial shows advanced features related to handling recurrence. For an introductory ASP.NET MVC Scheduler tutorial please see Scheduler for ASP.NET MVC 4 Razor (C#, VB.NET, SQL Server).
License
Licensed for testing and evaluation purposes. You can use the source code of the tutorial if you are a licensed user of DayPilot Pro for ASP.NET MVC.
Storing the Recurrence Rule in the Database
DayPilot ASP.NET MVC Scheduler control includes support for recurring events. It lets you save the recurrence information in a single string (VARCHAR) database field.
We will store the recurrence information in a "recurrence" field:
This is the DLL for our sample database table that stores event records:
CREATE TABLE [dbo].[event] (
[id] INT IDENTITY (1, 1) NOT NULL,
[name] VARCHAR (50) NULL,
[eventstart] DATETIME NOT NULL,
[eventend] DATETIME NOT NULL,
[resource] INT NOT NULL,
[recurrence] VARCHAR (300) NULL,
CONSTRAINT [PK_event] PRIMARY KEY CLUSTERED ([id] ASC)
);
The Scheduler control will load the recurrence information from the field specified using DataRecurrenceField property:
C#
protected override void OnFinish()
{
if (UpdateType == CallBackUpdateType.None)
{
return;
}
Events = new EventManager().FilteredData(StartDate, StartDate.AddDays(Days)).AsEnumerable();
DataIdField = "id";
DataTextField = "name";
DataStartField = "eventstart";
DataEndField = "eventend";
DataResourceField = "resource";
DataRecurrenceField = "recurrence";
}
VB.NET
Protected Overrides Sub OnFinish()
If UpdateType = CallBackUpdateType.None Then
Return
End If
Events = (New EventManager()).FilteredData(StartDate, StartDate.AddDays(Days)).AsEnumerable()
DataIdField = "id"
DataTextField = "name"
DataStartField = "eventstart"
DataEndField = "eventend"
DataResourceField = "resource"
DataRecurrenceField = "recurrence"
End Sub
Creating the Recurrence Rule
The recurrence rule string can be created using RecurrenceRule class. It will let you define the recurrence period (e.g. "weekly"), specify the period items on which the event occurs (e.g. "Monday, Tuesday") and the end rule (e.g. "Indefinitely").
Example 1:
Repeat this event weekly with no end date specified.
C#
string id = "123"; // id of the master event
DateTime start = new DateTime(2014, 12, 10, 10, 0, 0);
RecurrenceRule rule - RecurrenceRule.FromDateTime(id, start).Weekly().Indefinitely();
string encoded = rule.Encode();
VB.NET
Dim id As String = "123" ' id of the master event
Dim start As New Date(2014, 12, 10, 10, 0, 0)
RecurrenceRule rule - RecurrenceRule.FromDateTime(id, start).Weekly().Indefinitely()
Dim encoded As String = rule.Encode()
Example 2:
Repeat this event every day, generate 15 occurences:
C#
string id = "123"; // id of the master event
DateTime start = new DateTime(2014, 12, 10, 10, 0, 0);
RecurrenceRule rule - RecurrenceRule.FromDateTime(id, start).Daily().Times(15);
string encoded = rule.Encode();
VB
Dim id As String = "123" ' id of the master event
Dim start As New Date(2014, 12, 10, 10, 0, 0)
RecurrenceRule rule - RecurrenceRule.FromDateTime(id, start).Daily().Times(15)
Dim encoded As String = rule.Encode()
Event Editing
The event edit dialog is open when the user clicks an event.
C#
@Html.DayPilotScheduler("dps", new DayPilotSchedulerConfig
{
// ...
EventClickHandling = EventClickHandlingType.JavaScript,
EventClickJavaScript = "ask(e);",
})
VB.NET
@Html.DayPilotScheduler("dps", New DayPilotSchedulerConfig With
{
' ...
.EventClickHandling = EventClickHandlingType.JavaScript,
.EventClickJavaScript = "ask(e);",
})
If this event is a recurring event we need to ask whether to edit the series or an individual occurrence:
function ask(e) {
// it's a normal event
if (!e.recurrent()) {
edit(e);
return;
}
// it's a recurrent event but it's an exception from the series
if (e.value() !== null) {
edit(e);
return;
}
var modal = new DayPilot.Modal();
modal.width = 300;
modal.height = 150;
modal.closed = function () {
if (this.result != "cancel") {
edit(e, this.result);
}
};
modal.showUrl('@Url.Action("RecurrentEditMode", "Event")');
}
Depending on the event type we will open the edit dialog. The event type will be determined by the query string parameters.
Recurring events will have ?master specified
When editing a specific occurrence, ?start will hold the occurrence start
function edit(e, mode) {
var modal = new DayPilot.Modal();
modal.closed = function () {
if (this.result === "OK") {
dps.commandCallBack('refresh');
}
};
var url = "@Url.Action("Edit", "Event")";
if (e.id() !== null) {
url += "/" + e.id();
}
url += "?";
if (e.recurrentMasterId()) {
url += "master=" + e.recurrentMasterId() + "&";
}
if (mode == "this") {
url += "start=" + e.start().toStringSortable();
}
modal.showUrl(url);
}
Recurrence Edit Dialog
The edit dialog uses sample UI for editing the recurrence rule:
1. Use the section marked using <!-- Recurrence section --> and <!-- Recurrence section ends here -->
2. Add a hidden field that will store the serialized rule:
@Html.Hidden("Recurrence").
The id of this field is passed to the recurrence.js helper:
r.jsonHiddenId = "Recurrence";
3. Specify the save button id so it can be hooked by the recurrence rule serializer:
r.saveButtonId = "ButtonSave";
4. Load the existing rule from the controller:
C#
r.config = @ViewData["RecurrenceJson"];
VB.NET
r.config = @ViewData("RecurrenceJson");
The controller saves the serialized recurrence to ViewData object:
C#
public ActionResult Edit(string id)
{
var master = new EventManager().Get(masterId) ?? new EventManager.Event();
RecurrenceRule _rule = RecurrenceRule.Decode(master.Recurrence);
// ...
ViewData["RecurrenceJson"] = new HtmlString(_rule.ToJson());
return View(e);
}
VB.NET
Public Function Edit(ByVal id As String) As ActionResult
Dim master = If((New EventManager()).Get(masterId), New EventManager.Event())
Dim _rule As RecurrenceRule = RecurrenceRule.Decode(master.Recurrence)
' ...
ViewData("RecurrenceJson") = New HtmlString(_rule.ToJson())
Return View(e)
End Function
Full MVC view source code (Edit.cshtml)
@{
Layout = null;
}
<!DOCTYPE>
<html>
<head runat="server">
<script type="text/javascript" src="@Url.Content("~/Scripts/jquery-1.4.1.min.js")"></script>
<link href="@Url.Content("~/Media/layout.css")" rel="stylesheet" type="text/css" />
<style>
p, body, td { font-family: Tahoma, Arial, Sans-Serif; font-size: 10pt; }
</style>
<title></title>
</head>
<body style="padding:10px">
<form id="f" method="post" action="@Url.Action("Edit")">
<h2>Edit Event</h2>
<div style="margin-top:20px">
<div>Event text</div>
@Html.TextBox("Text") @Html.Hidden("Id")
</div>
<div>Start</div>
<div>@Html.TextBox("Start")</div>
<div>End</div>
<div>@Html.TextBox("End")</div>
<div>Resource</div>
<div>@Html.DropDownList("Resource")</div>
<div style="border-bottom: 2px solid #d0d0d0; margin-top:10px; margin-bottom: 10px;"></div>
<!-- Recurrence section -->
<script type="text/javascript" src="@Url.Content("~/Scripts/DayPilot/recurrence.js?v=3")"></script>
Repeat:
<select id="repeat">
<option value="norepeat">Does not repeat</option>
<option value="daily">Daily</option>
<option value="weekly">Weekly</option>
<option value="monthly">Monthly</option>
<option value="annually">Annually</option>
</select>
<div id="select_norepeat" style="display:none">
</div>
<div id="select_daily" style="display:none">
Repeat every <input id="daily_every" style="width: 20px;" value="1" /> day(s).
</div>
<div id="select_weekly" style="display:none">
Repeat every <input id="weekly_every" style="width: 20px;" value="1" /> week(s).
<div id="select_weekly_error" style="display:none; margin-top: 5px; padding: 2px; background-color: #FFF1A8;">Please select at least one day:</div>
<br />
<table>
<tr>
<td>On days: </td>
<td><label for="weekly_0"><input type="checkbox" id="weekly_0" />Sun</label></td>
<td><label for="weekly_1"><input type="checkbox" id="weekly_1" />Mon</label></td>
<td><label for="weekly_2"><input type="checkbox" id="weekly_2" />Tue</label></td>
<td><label for="weekly_3"><input type="checkbox" id="weekly_3" />Wed</label></td>
<td><label for="weekly_4"><input type="checkbox" id="weekly_4" />Thu</label></td>
<td><label for="weekly_5"><input type="checkbox" id="weekly_5" />Fri</label></td>
<td><label for="weekly_6"><input type="checkbox" id="weekly_6" />Sat</label></td>
</tr>
</table>
</div>
<div id="select_monthly" style="display:none">
Repeat every <input id="monthly_every" style="width: 20px;" value="1" /> month(s).
<div id="select_monthly_error" style="display:none; margin-top: 5px; padding: 2px; background-color: #FFF1A8;">Please select at least one day:</div>
<br />
<label for="monthly_1"><input type="checkbox" id="monthly_1" />1</label>
<label for="monthly_2"><input type="checkbox" id="monthly_2" />2</label>
<label for="monthly_3"><input type="checkbox" id="monthly_3" />3</label>
<label for="monthly_4"><input type="checkbox" id="monthly_4" />4</label>
<label for="monthly_5"><input type="checkbox" id="monthly_5" />5</label>
<label for="monthly_6"><input type="checkbox" id="monthly_6" />6</label>
<label for="monthly_7"><input type="checkbox" id="monthly_7" />7</label>
<label for="monthly_8"><input type="checkbox" id="monthly_8" />8</label>
<label for="monthly_9"><input type="checkbox" id="monthly_9" />9</label>
<label for="monthly_10"><input type="checkbox" id="monthly_10" />10</label>
<label for="monthly_11"><input type="checkbox" id="monthly_11" />11</label>
<label for="monthly_12"><input type="checkbox" id="monthly_12" />12</label>
<label for="monthly_13"><input type="checkbox" id="monthly_13" />13</label>
<label for="monthly_14"><input type="checkbox" id="monthly_14" />14</label>
<label for="monthly_15"><input type="checkbox" id="monthly_15" />15</label>
<label for="monthly_16"><input type="checkbox" id="monthly_16" />16</label>
<label for="monthly_17"><input type="checkbox" id="monthly_17" />17</label>
<label for="monthly_18"><input type="checkbox" id="monthly_18" />18</label>
<label for="monthly_19"><input type="checkbox" id="monthly_19" />19</label>
<label for="monthly_20"><input type="checkbox" id="monthly_20" />20</label>
<label for="monthly_21"><input type="checkbox" id="monthly_21" />21</label>
<label for="monthly_22"><input type="checkbox" id="monthly_22" />22</label>
<label for="monthly_23"><input type="checkbox" id="monthly_23" />23</label>
<label for="monthly_24"><input type="checkbox" id="monthly_24" />24</label>
<label for="monthly_25"><input type="checkbox" id="monthly_25" />25</label>
<label for="monthly_26"><input type="checkbox" id="monthly_26" />26</label>
<label for="monthly_27"><input type="checkbox" id="monthly_27" />27</label>
<label for="monthly_28"><input type="checkbox" id="monthly_28" />28</label>
<label for="monthly_29"><input type="checkbox" id="monthly_29" />29</label>
<label for="monthly_30"><input type="checkbox" id="monthly_30" />30</label>
<label for="monthly_31"><input type="checkbox" id="monthly_31" />31</label>
</div>
<div id="select_annually" style="display:none">
</div>
<div id="range" style="display:none">
<div style="border-bottom: 2px solid #d0d0d0; margin-top:10px; margin-bottom: 10px;"></div>
<label for="repeat_indefinitely"><input type="radio" name="repeat_range" id="repeat_indefinitely" />Repeat indefinitely</label><br />
<label for="repeat_until"><input type="radio" name="repeat_range" id="repeat_until" />Repeat until: </label><input id="repeat_until_value" style="width: 150px;" value="12/31/2099" /><br />
<label for="repeat_times"><input type="radio" name="repeat_range" id="repeat_times" />Repeat </label><input id="repeat_times_value" style="width: 20px;" /> time(s).<br />
</div>
<script type="text/javascript">
var r = new DayPilot.Recurrence();
r.saveButtonId = "ButtonSave";
r.jsonHiddenId = "Recurrence";
r.config = @ViewData["RecurrenceJson"];
r.onChange = function(args) {
var last = parent.DayPilot.ModalStatic.list.length - 1;
parent.DayPilot.ModalStatic.list[last].stretch();
};
r.onError = function(args) {
var last = parent.DayPilot.ModalStatic.list.length - 1;
parent.DayPilot.ModalStatic.list[last].stretch();
};
r.Init();
</script>
<!-- Recurrence section ends here -->
Mode: @ViewData["Mode"]
<div style="margin-top:20px">
@Html.Hidden("Recurrence")
<input type="submit" id="ButtonSave" value="Save" />
<a href="javascript:close()">Cancel</a>
</div>
</form>
<script type="text/javascript">
function close(result) {
if (parent && parent.DayPilot && parent.DayPilot.ModalStatic) {
parent.DayPilot.ModalStatic.close(result);
}
}
$("#f").submit(function () {
var f = $("#f");
$.post(f.action, f.serialize(), function (result) {
close(eval(result));
});
return false;
});
$(document).ready(function () {
$("#Text").focus();
});
</script>
</body>
</html>
Recurring Event Icon
We will add a special icon the event box indicating that it shows a recurring event or an exception. We will use event active areas to create the icons.
C#
protected override void OnBeforeEventRender(BeforeEventRenderArgs e)
{
if (e.Recurrent)
{
if (e.RecurrentException)
{
e.Areas.Add(new Area().Right(5).Top(5).Visible().CssClass("area_recurring_ex"));
}
else
{
e.Areas.Add(new Area().Right(5).Top(5).Visible().CssClass("area_recurring"));
}
}
}
VB.NET
Protected Overrides Sub OnBeforeEventRender(ByVal e As BeforeEventRenderArgs)
If e.Recurrent Then
If e.RecurrentException Then
e.Areas.Add((New Area()).Right(5).Top(5).Visible().CssClass("area_recurring_ex"))
Else
e.Areas.Add((New Area()).Right(5).Top(5).Visible().CssClass("area_recurring"))
End If
End If
End Sub
CSS
/* active area */
.area_recurring
{
height: 16px;
width: 16px;
background: url("repeat16.png");
}
.area_recurring_ex
{
height: 16px;
width: 16px;
background: url("repeat_exception16.png");
}
Recurrence Rule Exceptions
Exception from the recurrence rule are stored as special records in the database.
The recurrence field specifies the original occurrence encoded using RecurrenceRule.EncodeExceptionModified() or RecurrenceRuleEncodeExceptionDeleted().
The standard fields (text, start, end) will be used for the exception.
The existence of exceptions from the recurrence rule requires careful updating of the records in all types of operations (editing, moving, resizing, etc.).
This is a sample drag and drop moving event handler. It checks whether the event is recurring:
If it is a regular event -> update the record
If it is an exception -> update the exception record
If it isn't an exception -> create a new record (a new exception)
C#
protected override void OnEventMove(EventMoveArgs e)
{
if (e.Recurrent && !e.RecurrentException)
{
new EventManager().EventCreateException(e.NewStart, e.NewEnd, e.Text, e.NewResource, RecurrenceRule.EncodeExceptionModified(e.RecurrentMasterId, e.OldStart));
UpdateWithMessage("Recurrence exception was created.");
}
else
{
new EventManager().EventMove(e.Id, e.NewStart, e.NewEnd, e.NewResource);
UpdateWithMessage("The event was moved.");
}
}
VB.NET
Protected Overrides Sub OnEventMove(ByVal e As EventMoveArgs)
If e.Recurrent AndAlso (Not e.RecurrentException) Then
CType(New EventManager(), EventManager).EventCreateException(e.NewStart, e.NewEnd, e.Text, e.NewResource, RecurrenceRule.EncodeExceptionModified(e.RecurrentMasterId, e.OldStart))
UpdateWithMessage("Recurrence exception was created.")
Else
CType(New EventManager(), EventManager).EventMove(e.Id, e.NewStart, e.NewEnd, e.NewResource)
UpdateWithMessage("The event was moved.")
End If
End Sub