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 2013 Solution
  • C# source code
  • VB.NET source code
  • SQL Server 2014 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. Buy a license.

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:

asp.net-mvc-5-scheduler-recurrence-sql-schema.png

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

asp.net-mvc-5-recurrence-ask-dialog.png

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

asp.net-mvc-5-scheduler-recurring-event-edit.png

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

asp.net-mvc-5-recurring-event-icon.png

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