Ever had a need to create a recurring event? Or how about repeat any page type a specific number of times? It's pretty common and this post will give an example of how to go about this.
On a project I recently worked on I had a request to create recurring (daily, weekly, monthly) events. Out of the box, Kentico does not offer this. So I figured, why not take the challenge on and give it a shot. After a bit of planning and API reading I came up with this:
Create the UI to support the selection
I created my own custom Page Type, but you could extend the out of the box Event page type (cms.event) if you'd like. For consistency and demo purposes, lets create a new Page Type called custom.Event.
Add the following fields
in addition to the standard ones you need like EventName, EventText, EventCategory, etc.
- StartDate (Date, calendar control)
- EndDate (Date, calendar control)
- Recurrence (Text - 20, Dropdown list)
- RecurrenceDay (Text - 30, Multiple choice) These are the days of the week the event occurs on when Weekly is selected as the recurrence
- RecurrenceMonth (Integer number, Dropdown list) These are the days of the month the event occurs on when Monthly is selected as the recurrence.
On the Recurrence field add the following list of options:
;-- select one --
Daily
Weekly
Monthly
And checked the "Has depending fields" option.
On the RecurrenceDay multiple choice field I wanted to dynamically generate a list of day names so I used SQL query:
with wkday as
(
select 1 as dnum
union all
select 1 + dnum from wkday where dnum<8
)
select datename(WEEKDAY,DATEADD(day,dnum-1,'010101')), datename(WEEKDAY,DATEADD(day,dnum-1,'010101')) from wkday
I also set the visibility condition to Recurrence.Value == "weekly" so when "weekly" was selected in the Recurrence dropdown, this box would show. Don't forget to check the Depends on another field for this too!
For the RecurrenceMonth field once again, I wanted a dynamic list of month names and numbers so I used a SQL query:
with mday as
(
select 1 as dnum
union all
select 1 + dnum from mday where dnum<(SELECT DAY(DATEADD(mm,DATEDIFF(mm, -1, CAST('1/1/2010' AS DATETIME)),0) -1))
)
select datename(day,DATEADD(day,dnum-1,'010101')) AS [Value], datename(day,DATEADD(day,dnum-1,'010101')) AS [Name] from mday
I also set the visibility on this field to Recurrence.Value == "monthly" and checked the Depends on another field box.
When adding a new page your UI will look like so:
And when you change the recurrence field to Weely or Monthly you will see this:
Create the code to support the database work
Now that you have the UI worked out, lets write a little code. The code will consist of 2 files both located in the /App_Code/<SiteCodeName> directory (assuming you're using website vs. web project in Visual Studio):
- EventDocument.cs
- EventClassLoader.cs
The
EventDocument.cs file will contain this very simple code. Note it's a partial class from CMSModuleLoader and inerhits the TreeNode class.
using CMS.DocumentEngine;
using CMS.Helpers;
using System;
using System.Collections.Generic;
using System.Linq;
[DocumentType(EventDocument.CLASS_NAME, typeof(EventDocument))]
public partial class CMSModuleLoader
{
}
public partial class EventDocument : TreeNode
{
public const string CLASS_NAME = "custom.event";
public int EventID
{
get
{
return GetIntegerValue("EventID", 0);
}
protected set
{
SetValue("EventID", value);
}
}
public string EventTitle
{
get
{
return GetStringValue("EventTitle", "");
}
set
{
SetValue("EventTitle", value);
}
}
public DateTime StartDate
{
get
{
return GetDateTimeValue("StartDate", DateTimeHelper.ZERO_TIME);
}
set
{
SetValue("StartDate", value);
}
}
public DateTime EndDate
{
get
{
return GetDateTimeValue("EndDate", DateTimeHelper.ZERO_TIME);
}
set
{
SetValue("EndDate", value);
}
}
public string Recurrence
{
get
{
return GetStringValue("Recurrence", "");
}
set
{
SetValue("Recurrence", value);
}
}
public List RecurrenceDay
{
get
{
return GetStringValue("RecurrenceDay", "").Split('|').ToList();
}
set
{
SetValue("RecurrenceDay", value.Join("|"));
}
}
public int RecurrenceMonthDay
{
get
{
return GetIntegerValue("RecurrenceMonth", 0);
}
set
{
SetValue("RecurrenceMonth", value);
}
}
The EventClassLoader.cs file is the global event handler that extends the CMSModuleLoader and inherits the CMSLoaderAttrubute class. It only handles insert events for the "custom.event" Page Type we created. The best part about a global handler is you have one place to maintain code.
using CMS.Base;
using CMS.DocumentEngine;
using CMS.Helpers;
using System;
[EventClassLoader]
public partial class CMSModuleLoader
{
/// <summary>
/// Summary description for EventClassLoader
/// </summary>
public class EventClassLoaderAttribute : CMSLoaderAttribute
{
public override void Init()
{
DocumentEvents.Insert.After += Insert_After;
}
/// <summary>
/// We only want to create the recurring events
/// AFTER the initial event has been created so we
/// have that objects inserted information
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Insert_After(object sender, DocumentEventArgs e)
{
if (e.Node != null)
{
switch (e.Node.ClassName.ToLower())
{
case "custom.event":
CreateRecurringEvents(e);
break;
}
}
}
/// <summary>
/// Performs the necessary logic to create the events
/// </summary>
/// <param name="docEvents"></param>
private void CreateRecurringEvents(DocumentEventArgs docEvents)
{
var node = TreeNode.New<EventDocument>("custom.event", docEvents.Node, docEvents.TreeProvider);
// create a timespan object to do some math operations on easier
TimeSpan span = node.EndDate - node.StartDate;
// difference, in days, between start and end dates
double diff = 0;
bool weekdayInsert = false;
switch (node.Recurrence.ToLower())
{
case "daily":
diff = ValidationHelper.GetDouble(span.TotalDays, 0);
break;
case "weekly":
diff = ValidationHelper.GetDouble(span.TotalDays, 0);
break;
case "monthly":
diff = ValidationHelper.GetDouble(span.TotalDays / 30, 0);
break;
default:
break;
}
if (diff > 0)
{
DateTime currentDate = node.StartDate;
int c = 1;
while (c <= diff)
{
currentDate = currentDate.AddDays(1);
// create a new instance of the EventDocument class and set the values
var newEvent = TreeNode.New<EventDocument>("custom.event", docEvents.Node, docEvents.TreeProvider);
newEvent.NodeGUID = Guid.NewGuid();
// null out the recurrence values so we don't create an endless loop
newEvent.Recurrence = "";
newEvent.RecurrenceDay = null;
newEvent.RecurrenceMonthDay = 0;
switch (node.Recurrence.ToLower())
{
case "daily":
newEvent.StartDate = node.StartDate.AddDays(c);
newEvent.EndDate = node.StartDate.AddDays(c);
break;
case "weekly":
newEvent.StartDate = currentDate;
newEvent.EndDate = currentDate;
weekdayInsert = true;
break;
case "monthly":
DateTime newDate = new DateTime(node.StartDate.Year, node.StartDate.Month + c, node.RecurrenceMonthDay);
newEvent.StartDate = newDate;
newEvent.EndDate = newDate;
break;
default:
return;
}
if (weekdayInsert)
{
// check if the day being inserted exists in the check box list on the parent object
if (node.RecurrenceDay.Contains(currentDate.DayOfWeek.ToString()))
{
newEvent.Insert(node.NodeParentID);
newEvent.Publish();
}
}
else
{
newEvent.Insert(node.NodeParentID);
newEvent.Publish();
}
// increment the counter
c++;
}
}
}
}
}
Build your site, navigate to the website and add a new recurring event. This should create the events you're looking for and publish them.
There is a flaw!
This design has no way to do a mass delete on the recurring events you created. To resolve this you could create another field (that wouldn't be displayed) and store a unique ID in it, something to simply group all the items together that you created at one time. Then create a global event handler to perform a mass delete. That's another post I'll work I'll share later.
Be sure to check Part 2:
Recurring Events in a Multi-Tenancy Kentico Environment
Best of luck and Happy Coding!