Multi- User/ Resource Web Diary System With Repeat Events

Introduction

 
I previously wrote an article about using the open source jQuery plugin 'Full Calendar' to create a diary in .net MVC. That article covered the basics of using the plugin, and demonstrated the usual front and back-end functionality you need in an appointment diary system. This included creating an appointment/event, edit it, show different views etc. 
 
The content in the article remains valid and useful. This article improves on the last by showing how to use some of the new features offered to give multi-user, multi-resource calendar/diary and recurring/repeat events/appointment features to your diary/appointment/calendar app. I have attached an MVC project to the article that demonstrates the concepts discussed here - download it to see the solution in action.
 
Here are the images that show what we are going to build,
 
 
 
 
Background
 
When we manage our own appointments and diary entries, we normally do it only for ourselves, and thats fine. When we start to organise our lives and times around other people however, we need to consider their schedule as well as ours. This is especially important in organisations who need to manage the time of multiple individuals (think Doctors, Mechanics, Trainers), and also those who need to manage resources and equipment (think meeting rooms, portable/shared office equipment). Full Calendar version 2 introduced a new add-on for displaying events and resources using a grouped view known as Scheduler.
 
This add-on operates under a multi-license, allowing users to use it free of charge under GPL, get a limited license under creative commons, and also via a commercial version. Frankly, the commercial license charge is very reasonable for the value given. I have tried and used most of the commercial Calendar systems available, and this is now my go to choice every time. It is lightweight, fast and I find more flexible than anything else on the market at this time.
 
This article will build on the previous article and demonstrate the basics that you need to know to provide a very functional multi-user, multi-resource calendar/diary solution to your users.
 

FullCalendar resources

 
Overview
 
In a personal diary, like Outlook diary or Google Calendar, by default we see our own individual schedules. In a multi user environment, we need to see, and be able to manage, many users schedules at once. For a personal diary, there only one place we an be, but in a multi-user situation, users may be viewed as being grouped in different ways. In FullCalendar, these groupings are referred to as 'Resources'.
 
Some examples of how we might group users together,
  • by office or location
  • by department
  • by project team
  • by status
In addition to being grouped by some overall theme, we might also find that users share a set of things. We might want to view these either individually, or as a group. Here are some examples of things users might share,
  • equipment
  • meeting rooms
  • remote support login account
In FullCalendar, the items that appear as groupings, are called 'Resources'. The image below shows how they can appear in both horizontal and vertical view.
 
View 1
 
 
View 2
 
 
If we take things a step further, we could consider that a user or item they share may be involved in some kind of relationship, like for example certain meeting rooms are constrained by being in certain offices, or users or their equipment may be available only in certain locations. Here are some examples of how these things might be grouped,
  • Office has many meeting rooms
  • Users are only available in certain locations
In order to achieve the flexibility described above, where we can have 'resources within resources', the approach FullCalendar takes is to allow the creation of a parent/child relationship between resource groupings. Note that we are not restricted to having the same items or relationships from one resource node to the next - if we want to have a top level resource node with no children, the next with three, the next with five, the next with eight, and each of these with multiple child nodes as well, its all possible.
 
 
The key to working with FullCalendar in this way is manipulation of these resources, and how they get grouped together. Lets look now at how it can be done.
 

Code setup

 
Generally, I would expect to drive a diary system from a database of some sort. In order to keep this article and its demo code database agnostic, I decided to put together a simple test harness. This relies on creating a series of classes, creating/populating them with sample data, and using this to simulate a database environment. 
 

Test Harness

 
The harness consists of a number of classes which represent different things I want to demonstrate in the article. These are lists of users, equipment, offices, what users work out of which offices, and schedule/diary events. The harness defines the classes, and then there is an initialisation section that seeds the harness with test data. When the user (thats you!) runs the demo code, the application checks if a serialised (xml) representation of the harness exists in the user/temp folder, and if it does, it loads that, if not, it initialises itself and creates the test data. I'm only going to touch on the highlights of the setup code in this article, as the main focus is on how to use the resource functionality to enhance the Diary. If you want to go through the setup and other code in more detail, please download the code!
 
Setting up the TestHarness
  1. public class TestHarness    
  2. {    
  3.     public List < BranchOfficeVM > Branches {    
  4.         get;    
  5.         set;    
  6.     }    
  7.     public List < ClientVM > Clients {    
  8.         get;    
  9.         set;    
  10.     }    
  11.     public List < EquipmentVM > Equipment {    
  12.         get;    
  13.         set;    
  14.     }    
  15.     public List < EmployeeVM > Employees {    
  16.         get;    
  17.         set;    
  18.     }    
  19.     public List < ScheduleEventVM > ScheduleEvents {    
  20.         get;    
  21.         set;    
  22.     }    
  23.     public List < ScheduleEventVM > UnassignedEvents {    
  24.             get;    
  25.             set;    
  26.         } // constructor public TestHarness() { Branches = new List<BranchOfficeVM>(); Equipment = new List
  27. <EquipmentVM>(); Employees = new List<EmployeeVM>(); ScheduleEvents = new List<ScheduleEventVM>();
     
  28. UnassignedEvents = new List<ScheduleEventVM>(); Clients = new List<ClientVM>(); } ... <etc>   
Initialising lists to take data
  1. // initial setup if none already exists to load public void Setup()    
  2. {     
  3.   initClients();     
  4.   initUnAssignedTasks();     
  5.   initBranches();     
  6.   initEmployees();     
  7.   linkEmployeesToBranches();     
  8.   initEquipment();     
  9.   initEvents();    
  10. }   
Populating some of the properties with data
 
In this one we create a list of branch offices to play with.
  1. public void initBranches() {    
  2.     var b1 = new BranchOfficeVM();    
  3.     b1.BranchOfficeID = Guid.NewGuid().ToString();    
  4.     b1.Name = "New York";    
  5.     Branches.Add(b1);    
  6.     var b2 = new BranchOfficeVM();    
  7.     b2.BranchOfficeID = Guid.NewGuid().ToString();    
  8.     b2.Name = "London";    
  9.     Branches.Add(b2);    
  10. }   
We create some test employees and clients
  1. public void initEmployees() {    
  2.         var v1 = new EmployeeVM();    
  3.         v1.EmployeeID = Guid.NewGuid().ToString();    
  4.         v1.FirstName = "Paul";    
  5.         v1.LastName = "Smith";    
  6.         Employees.Add(v1);    
  7.         var v2 = new EmployeeVM();    
  8.         v2.EmployeeID = Guid.NewGuid().ToString();    
  9.         v2.FirstName = "Max";    
  10.         v2.LastName = "Brophy";    
  11.         Employees.Add(v2);    
  12.         var v3 = new EmployeeVM();    
  13.         v3.EmployeeID = Guid.NewGuid().ToString();    
  14.         v3.FirstName = "Rajeet";    
  15.         v3.LastName = "Kumar";    
  16.         Employees.Add(v3);... < etc >   
  1. public void initClients()    
  2. {    
  3.     Clients.Add(new ClientVM("Big Company A""New York"));    
  4.     Clients.Add(new ClientVM("Small Company X""London"));    
  5.     Clients.Add(new ClientVM("Big Company B""London"));    
  6.     Clients.Add(new ClientVM("Big Company C""Mumbai"));    
  7.     Clients.Add(new ClientVM("Small Company Y""Berlin"));    
  8.     Clients.Add(new ClientVM("Small Company Z""Dublin"));    
  9. }   
Create relationships between the various bits of test data
  1.     public void linkEmployeesToBranches()    
  2.     {    
  3.             var EmployeeUtil = new EmployeeVM();    
  4.             Branches[0].Employees.Add(EmployeeUtil.EmployeeByName(Employees, "Paul"));    
  5.             Branches[0].Employees.Add(EmployeeUtil.EmployeeByName(Employees, "Max"));    
  6.             Branches[0].Employees.Add(EmployeeUtil.EmployeeByName(Employees, "Rajeet"));    
  7.             Branches[1].Employees.Add(EmployeeUtil.EmployeeByName(Employees, "Philippe"));    
  8.             Branches[1].Employees.Add(EmployeeUtil.EmployeeByName(Employees, "Samara"));...   
  9.     < etc >   
Having finished with the supporting data, we then put in some sample data for diary events themselves.
 
(As an aside, to reiterate again, this article focuses on the resources and repeat functionality of the diary implementation. A complete explanation of the important fields, usage functions etc are all discussed in my previous introductory article to Full Calendar. If you are new to creating a diary using FullCalendar, you should start with that article, and then read this one!)
  1. public void initEvents()    
  2. {    
  3.         var utilBranch = new BranchOfficeVM();    
  4.         var EmployeeUtil = new EmployeeVM();    
  5.         var s1 = new ScheduleEventVM();    
  6.         s1.BranchOfficeID = utilBranch.GetBranchByName(Branches, "New York").BranchOfficeID;    
  7.         var c1 = utils.GetClientByName(Clients, "Big Company A");    
  8.         s1.clientId = c1.ClientID;    
  9.         s1.clientName = c1.Name;    
  10.         s1.clientAddress = c1.Address;    
  11.         s1.title = "Event 2 - Big Company A";    
  12.         s1.statusString = Constants.statusBooked;    
  13.         var v1 = EmployeeUtil.EmployeeByName(Employees, "Paul");    
  14.         s1.EmployeeId = v1.EmployeeID;    
  15.         s1.EmployeeName = v1.FullName;    
  16.         s1.DateTimeScheduled = new DateTime(DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day, 11, 15, 0);    
  17.         s1.durationMinutes = 120;    
  18.         s1.duration = s1.durationMinutes.ToString();    
  19.         s1.DateTimeScheduledEnd = s1.DateTimeScheduled.AddMinutes(s1.durationMinutes);    
  20.         ScheduleEvents.Add(s1);... < etc >   
We also create some sample 'unscheduled/unassigned diary events'. This will be used to demonstrate some functionality particular to the resource/scheduler add-on that are different to the main diary control. The main difference between a standard event event and one that is not yet assigned to a diary event.
  1. public void initUnAssignedTasks()     
  2. {    
  3.         var uaItem1 = new ScheduleEventVM();    
  4.         var cli1 = utils.GetClientByName(Clients, "Big Company A");    
  5.         uaItem1.clientId = cli1.ClientID;    
  6.         uaItem1.clientName = cli1.Name;    
  7.         uaItem1.clientAddress = cli1.Address;    
  8.         uaItem1.title = cli1.Name + " - " + cli1.Address;    
  9.         uaItem1.durationMinutes = 30;    
  10.         uaItem1.duration = uaItem1.durationMinutes.ToString();    
  11.         uaItem1.DateTimeScheduled = DateTime.Now.AddDays(14);    
  12.         uaItem1.DateTimeScheduledEnd = uaItem1.DateTimeScheduled.AddMinutes(uaItem1.durationMinutes);    
  13.         uaItem1.notes = "Test notes 1";... < etc >   
Full Calendar Javascript configuration
 
To setup the plugin and its resources, we need to initialize it when the browser loads. A full description of the general options and properties are discussed in my other article. So I will show the code for javascript setup here, and discuss how it pertains to resources. Please refer back to the other article if you need detail on the overall diary setup.
 
First, the start of the general setup
  1. // Main code to initialise/setup and show the calendar itself.     
  2. function ShowCalendar()     
  3. {     
  4.   $('#calendar').fullCalendar    
  5.   ({     
  6.     schedulerLicenseKey: 'GPL-My-Project-Is-Open-Source',     
  7.     // change depending on license type     
  8.     theme: false,     
  9.     resourceAreaWidth: 230,     
  10.     groupByDateAndResource: false,     
  11.     editable: true,     
  12.     aspectRatio: 1.8,     
  13.     scrollTime: '08:00',     
  14.     timezone: 'local',     
  15.     droppable: true,     
  16.     drop: function     
  17.     ...<snip> ...   
Then the important part for the resources
  1. // this is where the resource loading for laying out the page is triggered from resourceLabelText: "@Model.ResourceTitle",     
  2. // set server-side resources:     
  3. { url: '/Home/GetResources', data: {resourceView : "@Model.DefaultView"},     
  4.   type: 'POST', ...<snip> ...   
In this case, I have told it to get its feed for resources from a server-side ajax controller '/Home/GetResources'. There are multiple options for setting up the resources, you can use inline arrays, ajax/json feed or functions.
 
Here is an array example
  1. $('#calendar').fullCalendar({    
  2.     resources: [{    
  3.         id: 'a',    
  4.         title: 'Room A'    
  5.     }, {    
  6.         id: 'b',    
  7.         title: 'Room B'    
  8.     }]    
  9. });   

Relationship between diary event objects and resources

 
Full Calendar displays event objects. Here is a basic construction that adds a single basic event,
  1. events: [ { id: '1', title: 'Meeting', start: '2015-02-14' }    
Lets say we have the following structure of resources,
  1. resources: [ { id: 'a', title: 'Room A' } ]   
The unique ID of the resource is 'a', so to link that to our event, and have it display in the appropriate resource column/row, we simply tell the event it is using that resource ID,
  1. $('#calendar').fullCalendar({    
  2.     resources: [{    
  3.         id: 'a',    
  4.         title: 'Room A'    
  5.     }],    
  6.     events: [{    
  7.         id: '1',    
  8.         resourceId: 'a',    
  9.         title: 'Meeting',    
  10.         start: '2015-02-14'    
  11.     }]    
  12. });   
We can also associate an event with multiple different resources - in this case, we separate the IDs using a comma, and use the plural 'resourceIds' to define the relationship, versus the singular used for one link.
  1. $('#calendar').fullCalendar    
  2. ({    
  3.     resources: [{    
  4.         id: 'a',    
  5.         title: 'Room A'    
  6.     }, {    
  7.         id: 'b',    
  8.         title: 'Room B'    
  9.     }],    
  10.     events: [{    
  11.         id: '1',    
  12.         resourceIds: ['a''b'],    
  13.         title: 'Meeting',    
  14.         start: '2015-02-14'    
  15.     }]    
  16. });   
Now, here's something to get your head around ... a resourceId is a moving target. What I mean by this is that depending on what kind of view you are using, the ID of it means something different. For example, if the current view is ‘timeline: equipment’, then the ResourceID refers to the EquipmentID. If the current view is ‘timeline: Employees’, then the ResourceID refers to the EmployeeID.
 
The reason for this is that the diary grid for timeline view shows date/time on top (columns), and the rows are reserved for the main ‘diary event’ in question, being the resource having the focus (employee, or equipment, etc).
 

Switching between resource views / loading data

 
Looking at code is one thing, but a picture tells a better story! ... here is a screenshot that shows the popup form the user can use to switch between the different resource views.
 
 
In the OnClick/close modal event of the selector form, Javascript code takes the value of the resource type the user wants to see, and posts this to the server calling 'setView'. I decided to use a post versus a get as I didn't want to make my URL ugly! The SetView controller sets the new default view in the session data, then redirects back to the index controller and the correct view is then rendered to the user.
  1. $('#btnUpdateView').click(function()    
  2. {    
  3.     var selectedView = $('input[name="rdoResourceView"]:checked').val();    
  4.     post('/Home/setView',     
  5.     {    
  6.         ResourceView: selectedView    
  7.     }, 'setView');    
  8. });   

Server-side code

 
The starting point for the diary on the server-side is the home/index controller. Note at the top of the controller the TestHarness class is declared. The Index takes one parameter ‘ResourceView’. This tells the controller what view to return - in our example, Branch/Employee view, or equipment view. By default, it returns Branch/Employee view. When the index loads (as all other controllers), it loads up the TestHarness - for bringing this to production, you will connect here to your database and load data as appropriate. In this example, if the TestHarness XML data does not exist, it creates it by calling the TestHarness.Setup() method.
 
The Index.cshtml page contains a model ‘FullCal.ViewModels.Resource’. This can carry any information you want. This example uses it to carry the ‘Default View’ to be shown to the user when the page reloads, and the title string for the resource column. In the controller, once we decide/set the view to use, we send back the ‘ResourceView’ as a string in the view model. The last thing to note about ‘ResourceView’ is that it is stored as a ‘Session value’. See controller method ‘setView’ (below) for the implementation. 
  1. Index controller
    1. public class HomeController: Controller    
    2. {    
    3.   TestHarness testHarness;     
    4.   // used to store temp data repository public ActionResult     
    5.   Index(string ResourceView)     
    6.   {     
    7.     // create test harness if it does not exist     
    8.     // this harness represents interaction you may replace with database calls.     
    9.     if (Session["ResourceView"]!=null)     
    10.       // refers to whatever default view you may have,     
    11.       // for example Offices/Users/Shared Equipment/etc     
    12.       // you can create any amount and combination     
    13.       // of different view types you need.     
    14.       ResourceView = Session["ResourceView"].ToString();     
    15.     if (!System.IO.File.Exists(utils.GetTestFileLocation()))     
    16.     {     
    17.       testHarness = new TestHarness();     
    18.       testHarness.Setup();     
    19.       utils.Save(utils.GetTestFileLocation(), testHarness);    
    20.     }     
    21.     if (ResourceView == null) ResourceView = "";     
    22.     ResourceView = ResourceView.ToLower().Trim();     
    23.     var DiaryResourceView = new Resource();     
    24.     if (ResourceView == "" || ResourceView == "employees")     
    25.       // set the default     
    26.     {    
    27.       DiaryResourceView.DefaultView = "employees";     
    28.       DiaryResourceView.ResourceTitle = "Branch offices";     
    29.     }     
    30.     else if    
    31.       (    
    32.         ResourceView == "equipment")     
    33.     {     
    34.       DiaryResourceView.DefaultView = ResourceView;     
    35.       DiaryResourceView.ResourceTitle = "Equipment list";    
    36.     }     
    37.     return View(DiaryResourceView);     
    38.   }  
  2. SetView controller
    1. // this method, called from the index page, sets a session variable for    
    2. // the user that gets looped back to the index page to tell it what view     
    3. // to display. branch/employee or Equipment.     
    4. public ActionResult setView(string ResourceView)     
    5. {     
    6.   Session["ResourceView"] = ResourceView; return RedirectToAction("Index");     
    7. }   

Useful functionality

 
Managing resource cell navigation - a minor gotcha....
 
When we have a single resource view with no child resources, and we click on a cell to create a new event, it is clear we are clicking on a cell that represents a single resource.... 
 
 
When we have a parent/child relationship however, its a different story, we have to keep track of where we are, and implement rules depending on where the user clicks...
 
 
To help manage this situation, we keep an in-memory list of our resources and their associations, and then use the OnClick/Select events of FullCalendar to preform a lookup against the cell selected and decide if we need to implement a rule.
  1. // use this function to get a local list of employees/branches/equipment etc    
  2. // and populate arrays as appropriate for checking business rules etc.     
  3. function GetLocationsAndEmployees()     
  4. {     
  5.   $.ajax    
  6.   ({     
  7.     url: '/home/GetSetupInfo',     
  8.     cache: false,     
  9.     success: function (resultData)     
  10.     {     
  11.       ClearLists(); EmployeeList = resultData.Employees.slice(0);     
  12.       BranchList = resultData.Branches.slice(0);     
  13.       EquipmentList = resultData.Equipment.slice(0);     
  14.       ClientList = resultData.Clients.slice(0);     
  15.     }     
  16.   });   
In this example, we don't allow users to click on "office" rows, so we raise an alert if the user makes a mistake...
 
 
  1. var employeeResource = EmployeeList.find    
  2. (     
  3.   // if the row clicked on is NOT in the known array of employeeID,     
  4.   // then drop out (and alert...)     
  5.   function (employee)     
  6.   {     
  7.     return employee.EmployeeID == resourceObj.id;     
  8.   }     
  9. )   

Drag and drop

 
Having the capability to be able to drag/drop events onto a diary is useful. However, we need to be able to tell the diary on the drop event, something about the event being dropped. For this, we attach information to the object being dropped in a particular manner.
 
To identify items to be dropped, we mark them with a class 'draggable'. To attach data to them, we call a function that iterates through everything marked with this class, and assign data as follows,
  1. // set up for drag/drop of unassigned tasks into scheduler    
  2. // *example only - if using a large data feed from a table* function     
  3. InitDragDrop()     
  4. {     
  5.   $('.draggable').each(function ()     
  6.                        {     
  7.     // create an Event Object     
  8.     // ref: (http://arshaw.com/fullcalendar/docs/event_data/Event_Object/)     
  9.     // it doesn't need to have a start or end     
  10.     var table = $('#UnScheduledEvents').DataTable();     
  11.     var eventObject = {     
  12.       id: $(table.row(this).data()[0]).selector,     
  13.       clientId: $(table.row(this).data()[1]).selector,     
  14.       start: $(table.row(this).data()[2]).selector,     
  15.       end: $(table.row(this).data()[3]).selector,     
  16.       title: $(table.row(this).data()[4]).selector,     
  17.       duration: $(table.row(this).data()[5]).selector,     
  18.       notes: $(table.row(this).data()[6]).selector,     
  19.       color: 'tomato'     
  20.     }     
  21.     // gotcha: MUST be named "event", for *external dropped objects* and     
  22.     // some rules: http://fullcalendar.io/docs/dropping/eventReceive/ $(this).data('event', eventObject);     
  23.     // make the event draggable using jQuery UI     
  24.     $(this).draggable    
  25.     ({     
  26.       activeClass: "ui-state-hover",     
  27.       hoverClass: "ui-state-active",     
  28.       zIndex: 999, revert: true,     
  29.       // will cause the event to go back to its revertDuration: 0     
  30.       // original position after the drag     
  31.     });     
  32.   });     
  33. };   
When the item is dropped, it is hooked by the FullCalendar 'eventReceive' method. In our example, we ask the user to confirm before proceeding,
  1. eventReceive: function(event)     
  2. {    
  3.         var confirmDlg = confirm('Are you sure you wish to assign this event?');    
  4.         if (confirmDlg == true)    
  5.         {    
  6.             var eventDrag =    
  7.              {    
  8.                 title: event.title,    
  9.                 start: new Date(event.start),    
  10.                 resourceId: event.resourceId,    
  11.                 clientId: null,    
  12.                 duration: 30,    
  13.                 equipmentId: null,    
  14.                 BranchID: null,    
  15.                 statusString: "",    
  16.                 notes: "",    
  17.             }    
  18.             UpdateEventMove(eventDrag, null);    
  19.         }   
Now, heres a minor gotcha to note.... you will recall earlier in the article we discussed the 'resourceId' property of an event object being a 'moving target'... well here it is in action. Depending on the view the user is looking at (say employee or equipment), then the 'resourceId' value in the associated event, will *either* refer to the ID of an employee OR a piece of equipment. Due to this, here we example the view type before proceeding and sending the data to the server. Of course this is my example implementation, there are many ways to work the logic - the point of this is to point out that you need to take it into consideration.
  1. function UpdateEventMove(event, view)     
  2. {     
  3.   // determine the view and from this set the correct EmployeeID or ResourceID     
  4.   // before sending down to server     
  5.   if (ResourceView == 'employees') event.employeeId = event.resourceId;     
  6.   else     
  7.   {     
  8.     event.employeeId = $('#newcboEmployees').val();     
  9.   }     
  10.   var dataRow =     
  11.       {     
  12.         'Event': event     
  13.       }     
  14.   $.ajax    
  15.   ({     
  16.     type: 'POST', url: "/Home/PushEvent", dataType: "json", contentType: "application/json", data: JSON.stringify(dataRow)    
  17.   });     
  18. }   

Repeat / recurring calendar events

 
The final thing I want to demonstrate is how we can put repeat or recurring events into our solution. I have done this using a combination of two very useful open course libraries. One that operates browser-side, one server-side. Before we look at the code and components we use, it would be good to understand how repeats work and how we can represent recurring events in our solution.
 

Understanding CRON format

 
A CRON JOB (from chronological), is well known in the UNIX operating system as a command to tell the system to execute a job at a particular time. CRON has a syntax that when formulated, both a computer and human can read, it describes the exact date/time a job should be triggered and is very flexible. A CRON string is not restricted to describing a single date and time (example: 1st Jan 2016), it can also be used to describe recurring time patterns (example: The third of each month at 9am). There are a few different implementations of the CRON syntax, and you can expand to it yourself if you need to. For example, if a basic CRON descriptor only allowed for repeat events, you might decide to add limitations of a 'between X and Y date' to your business logic (ie: 'The third of each month at 9am, but only if thats a Tuesday and between the 1st of June 2016 and the 30th of August 2016).
 
Standard CRON consists of five fields, each separated by a space, minute hour day-of-month month-of-year day-of-week.
 
Each field can contain one or more characters that describe the contents of the field, 
  • * (asterisk) means all values. For example, if we had '*' in the minute field, it would mean 'execute the job once per minute'. If it was in the month field, it would mean 'execute the job once per month'.
     
  • commas in a field are used to separate values (eg: 2,4,6 .. on the 2nd, 4th and 6th minute)
     
  • hyphens in a field are used to specify a range (eg: 4-9 .. between the 4th and 9th minute)
     
  • after an asterisk or hyphen-range, we can use the forward slash '/' to indicate that values are repeated over and over with a particular interval between the values. (eg: 0-18/2 indicates execute the job every two hours between the time 0 hours and 18 hours)
Here are some examples showing CRON in action,
 
 
* * * * * * Each minute 45 17 7 6 * * Every year, on June 7th at 17:45 * 0-11 * * * Each minute before midday 0 0 * * * * Daily at midnight 0 0 * * 3 * Each Wednesday at midnight
 
You can find out more detailed information on CRON here (where some examples came from) and here.
 

JQueryUI Cron Builder

 
Whenever possible, I try not to reinvent the wheel. I came across the really useful JQuery-Cron builder from Shawn Chin a few years back and have used it in multiple projects since very successfully. Instead of forcing users to enter cryptic expressions to specify a cron expression, this JQuery plugin allows users to select recurring times from an easy to use GUI. It is designed as a series of drop-boxes that the users chooses values from. Depending on the initial selections, the interface changes to offer appropriate follow-on values. Once the user has set the repeat/recurring time they require, you can call a function that gives you back the CRON expression for the visual values the user selected.
 
Using the Cron builder is the usual JQuery style. We declare a div, then call the plugin against it,
  1. <div id="repeatCRON"></div> $('#repeatCRON').cron();  
Here are some examples of it rendered in the browser,
 
When we save the diary event, we query the CronBuilder plugin and get the CRON expression - once we have this, we can then save it as a string into a field in our database .. in my case I named this field 'Repeat'.
  1. $('#submitButton').on('click'function(e)    
  2. {    
  3.     e.preventDefault();    
  4.     SelectedEvent.title = $('#title').val();    
  5.     SelectedEvent.duration = $('#duration').val();    
  6.     SelectedEvent.equipmentId = $('#cboEquipment').val();    
  7.     SelectedEvent.branchId = $('#branch').val();    
  8.     SelectedEvent.clientId = $('#cboClient').val();    
  9.     SelectedEvent.notes = $('#notes').val();    
  10.     SelectedEvent.resourceId = $('#cboEmployees').val();    
  11.     SelectedEvent.statusString = $('#cboStatus').val();    
  12.     SelectedEvent.repeat = $('#repeatCRONEdit').cron("value") UpdateEventMove(SelectedEvent, null);    
  13.     doSubmit();    
  14. });   
We can also do the reverse - take a CRON string, and pass it into the JQuery builder, and it will display the UI that represents the CRON expression.
  1. if (SelectedEvent.repeat != null) $('#repeatCRONEdit').cron("value", SelectedEvent.repeat);    
  2. else $('#repeatCRONEdit').cron("value""* * * * *");   

NCronTab

 
Ok, so we have have the building and storage of the repeat/recurring event information and the UI taken care of - now lets look at what we can do to decide WHEN/IF to display these recurring events in our diary.
 
In UNIX, a CronTab is a file that contains a list of CRON expressions, and a command for the system to execute once that CRON time gets triggered. From a Windows perspective, the equivalent is the Windows task scheduler service.
 
NCrontab is a library written in C# 6.0 that provides the following facilities, 
  • Parsing of crontab expressions
  • Formatting of crontab expressions
  • Calculation of occurrences of time based on a crontab schedule
This library does not provide any scheduler or is not a scheduling facility like cron from Unix platforms. What it provides is parsing, formatting and an algorithm to produce occurrences of time based on a give schedule expressed in the crontab format. (src: NCrontab)
 
In this example project, I am using a NCrontab library method to examine a stored CRON expression string, against the date range that the user has selected in the FullCalendar, and determine if any of my stored repeat values occur within that date range.
 
Lets look at how the code works for this,
  1. the user decides to refresh the diary to show events in a particular date range. Note the params sent in are the start and end date plus the resourceView required.
    1. public JsonResult GetScheduleEvents(string start, string end, string resourceView) .. <etc>    
  2. we query all scheduled events for any event that has a 'repeat' value stored.
    1. repeatEvents = testHarness.ScheduleEvents.Where(s => (s.repeat != null));  
  3. we then examine each repeat event (ie: the CRON string expression), and PARSE it with NCronTab.CrontabSchedule, passing the result of the parse into a method that says 'between this start and end date given, give me a list of valid date/times the CRON string represents.
    1. if (repeatEvents != null) foreach(var rptEvnt in repeatEvents)    
    2. {    
    3.     var schedule = CrontabSchedule.Parse(rptEvnt.repeat);    
    4.     var nextSchdule = schedule.GetNextOccurrences(Start, End);    
    5.     foreach(var startDate in nextSchdule) {    
    6.         ScheduleEvent itm = new ScheduleEvent();    
    7.         itm.id = rptEvnt.EventID;    
    8.         if (rptEvnt.title.Trim() == "") itm.title = rptEvnt.clientName;    
    9.         else itm.title = rptEvnt.title;    
    10.         itm.start = startDate.ToString("s");    
    11.         itm.end = startDate.AddMinutes(30).ToString("s");    
    12.         itm.duration = rptEvnt.duration.ToString();    
    13.         itm.notes = rptEvnt.notes;    
    14.         itm.statusId = rptEvnt.statusId;    
    15.         itm.statusString = rptEvnt.statusString;    
    16.         itm.allDay = false;    
    17.         itm.EmployeeId = rptEvnt.EmployeeId;    
    18.         itm.clientId = rptEvnt.clientId;    
    19.         itm.clientName = rptEvnt.clientName;    
    20.         itm.equipmentId = rptEvnt.equipmentID;    
    21.         itm.EmployeeName = rptEvnt.EmployeeName;    
    22.         itm.repeat = rptEvnt.repeat;    
    23.         itm.color = rptEvnt.statusString;    
    24.         if (resourceView == "employees") itm.resourceId = rptEvnt.EmployeeId;    
    25.         else itm.resourceId = rptEvnt.equipmentID;    
    26.         EventItems.Add(itm);    
    27.     }    
    28. }   
The final thing to note in the above code ar the last few lines - again, we go back to our 'moving target' issue. Depending on the view that the user has selected, we need to set the correct resourceId value so ti will render correctly once it is returned to the browser.
 

Conclusion

 
That pretty much wraps up the article. If you need to implement a fully loaded diary/calendar/appointment solution that incorporates multi resources in a very powerful way, you should strongly consider the combination of FullCalendar, JQueryCron, and NCronTab. I have attached a working example with the article that shows all of the functionality we have discussed working - please download and play with it.
 
Please note, this is not a standalone project - it demonstrates specific functionality. You should use it in conjunction with my other article explaining FullCalendar (and its accompanying code) to implement your own particular working solution.