Click here to Skip to main content
14,137,520 members
Click here to Skip to main content
Add your own
alternative version

Stats

26.7K views
1.4K downloads
19 bookmarked
Posted 3 Sep 2016
Licenced CPOL

Multi-user/Resource Web Diary in C# MVC with Repeat Events

, 4 Sep 2016
Rate this:
Please Sign up or sign in to vote.
Adding multi user and resource capabilities to Full Calendar in .NET MVC

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 backend functionality you need in an appointment diary system. This included creating an appointment/event, editing it, showing 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 that's 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 that 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 it 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 is only one place we can 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 FullCalander, 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 FullCalander 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, it's all possible.

The key to working with FullCalendar in this way is manipulation of these resources, and how they get grouped together. Let's 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 (that's 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...

public class TestHarness
{
    public List<BranchOfficeVM> Branches { get; set; }
    public List<ClientVM> Clients { get; set; }
    public List<EquipmentVM> Equipment { get; set; }
    public List<EmployeeVM> Employees { get; set; }
    public List<ScheduleEventVM> ScheduleEvents { get; set; }
    public List<ScheduleEventVM> UnassignedEvents { get; set; }

    // constructor
    public TestHarness()
    {
        Branches = new List<BranchOfficeVM>();
        Equipment = new List<EquipmentVM>();
        Employees = new List<EmployeeVM>();
        ScheduleEvents = new List<ScheduleEventVM>();
        UnassignedEvents = new List<ScheduleEventVM>();
        Clients = new List<ClientVM>();
    }
  ... <etc>

Initialising lists to take data...

// initial setup if none already exists to load
public void Setup()
{
    initClients();
    initUnAssignedTasks();
    initBranches();
    initEmployees();
    linkEmployeesToBranches();
    initEquipment();
    initEvents();
}
Populating Some of the Properties with Data...

In this one, we create a List of branch offices to play with.

public void initBranches()
{
    var b1 = new BranchOfficeVM();
    b1.BranchOfficeID = Guid.NewGuid().ToString();
    b1.Name = "New York";
    Branches.Add(b1);
    var b2 = new BranchOfficeVM();
    b2.BranchOfficeID = Guid.NewGuid().ToString();
    b2.Name = "London";
    Branches.Add(b2);
}

We create some test employees and clients...

   public void initEmployees()
   {
       var v1 = new EmployeeVM();
       v1.EmployeeID = Guid.NewGuid().ToString();
       v1.FirstName = "Paul";
       v1.LastName = "Smith";
       Employees.Add(v1);

       var v2 = new EmployeeVM();
       v2.EmployeeID = Guid.NewGuid().ToString();
       v2.FirstName = "Max";
       v2.LastName = "Brophy";

       Employees.Add(v2);
       var v3 = new EmployeeVM();
       v3.EmployeeID = Guid.NewGuid().ToString();
       v3.FirstName = "Rajeet";
       v3.LastName = "Kumar";
       Employees.Add(v3);
... <etc>
public void initClients()
{
    Clients.Add(new ClientVM("Big Company A", "New York"));
    Clients.Add(new ClientVM("Small Company X", "London"));
    Clients.Add(new ClientVM("Big Company B", "London"));
    Clients.Add(new ClientVM("Big Company C", "Mumbai"));
    Clients.Add(new ClientVM("Small Company Y", "Berlin"));
    Clients.Add(new ClientVM("Small Company Z", "Dublin"));
}

Create relationships between the various bits of test data...

 public void linkEmployeesToBranches()
 {
     var EmployeeUtil = new EmployeeVM();

     Branches[0].Employees.Add(EmployeeUtil.EmployeeByName(Employees, "Paul"));
     Branches[0].Employees.Add(EmployeeUtil.EmployeeByName(Employees, "Max"));
     Branches[0].Employees.Add(EmployeeUtil.EmployeeByName(Employees, "Rajeet"));
     Branches[1].Employees.Add(EmployeeUtil.EmployeeByName(Employees, "Philippe"));
     Branches[1].Employees.Add(EmployeeUtil.EmployeeByName(Employees, "Samara"));
... <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!)

  public void initEvents()
  {
      var utilBranch = new BranchOfficeVM();
      var EmployeeUtil = new EmployeeVM();

      var s1 = new ScheduleEventVM();
      s1.BranchOfficeID = utilBranch.GetBranchByName(Branches, "New York").BranchOfficeID;
      var c1 = utils.GetClientByName(Clients, "Big Company A");
      s1.clientId = c1.ClientID;
      s1.clientName = c1.Name;
      s1.clientAddress = c1.Address;
      s1.title = "Event 2 - Big Company A";
      s1.statusString = Constants.statusBooked;

      var v1 = EmployeeUtil.EmployeeByName(Employees, "Paul");
      s1.EmployeeId = v1.EmployeeID;
      s1.EmployeeName = v1.FullName;
      s1.DateTimeScheduled = new DateTime(DateTime.Now.Year, DateTime.Now.Month,
                             DateTime.Now.Day, 11, 15, 0);
      s1.durationMinutes = 120;
      s1.duration = s1.durationMinutes.ToString();
      s1.DateTimeScheduledEnd = s1.DateTimeScheduled.AddMinutes(s1.durationMinutes);
      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 and one that is not yet assigned to a diary event.

  public void initUnAssignedTasks()
  {
      var uaItem1 = new ScheduleEventVM();
      var cli1 = utils.GetClientByName(Clients, "Big Company A");
      uaItem1.clientId = cli1.ClientID;
      uaItem1.clientName = cli1.Name;
      uaItem1.clientAddress = cli1.Address;
      uaItem1.title = cli1.Name + " - " + cli1.Address;
      uaItem1.durationMinutes = 30;
      uaItem1.duration = uaItem1.durationMinutes.ToString();
      uaItem1.DateTimeScheduled = DateTime.Now.AddDays(14);
      uaItem1.DateTimeScheduledEnd = uaItem1.DateTimeScheduled.AddMinutes
                                             (uaItem1.durationMinutes);
      uaItem1.notes = "Test notes 1";
... <etc>

Full Calendar JavaScript configuration

To setup the plugin, and its resources, we need to initialise 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 details on the overall diary setup.

First, the start of the general setup...

// Main code to initialise/setup and show the calendar itself.
  function ShowCalendar() {
     $('#calendar').fullCalendar({
       schedulerLicenseKey: 'GPL-My-Project-Is-Open-Source', // change depending on license type
       theme: false,
       resourceAreaWidth: 230,
       groupByDateAndResource: false,
       editable: true,
       aspectRatio: 1.8,
       scrollTime: '08:00',
       timezone: 'local',
       droppable: true,
       drop: function
        ...<snip> ...

Then the important part for the resources....

// this is where the resource loading for laying out the page is triggered from

resourceLabelText: "@Model.ResourceTitle",  // set server-side
resources:
{
url: '/Home/GetResources',
data: {resourceView : "@Model.DefaultView"},
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:

$('#calendar').fullCalendar({
    resources: [
        {
            id: 'a',
            title: 'Room A'
        },
        {
           id: 'b',
            title: 'Room B'
        }
    ]
});

Relationship Between Diary Event Objects and Resources

Full Calendar displays event objects. Here is a basic construction that adds a single basic event:​​​

events: [
        {
            id: '1',
            title: 'Meeting',
            start: '2015-02-14'
        }

Let's say we have the following structure of resources:

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:

$('#calendar').fullCalendar({
    resources: [
        {
            id: 'a',
            title: 'Room A'
        }
    ],
    events: [
        {
            id: '1',
            resourceId: 'a',
            title: 'Meeting',
            start: '2015-02-14'
        }
    ]
});

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.

$('#calendar').fullCalendar({
    resources: [
        {
            id: 'a',
            title: 'Room A'
        },
        {
            id: 'b',
            title: 'Room B'
        }
    ],
    events: [
        {
            id: '1',
            resourceIds: ['a', 'b'],
            title: 'Meeting',
            start: '2015-02-14'
        }
    ]
});

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.

$('#btnUpdateView').click(function () {
    var selectedView = $('input[name="rdoResourceView"]:checked').val();
    post('/Home/setView', { ResourceView: selectedView }, 'setView');
});

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
    public class HomeController : Controller
     {
         TestHarness testHarness; // used to store temp data repository
    
         public ActionResult Index(string ResourceView)
         {
             // create test harness if it does not exist
             // this harness represents interaction you may replace with database calls.
             if (Session["ResourceView"]!=null) // refers to whatever default view you may have,
                                                // for example Offices/Users/Shared Equipment/etc.
                                                // you can create any amount and combination 
                                                // of different view types you need.
                 ResourceView = Session["ResourceView"].ToString();
    
             if (!System.IO.File.Exists(utils.GetTestFileLocation()))
                 {
                 testHarness = new TestHarness();
                 testHarness.Setup();
                 utils.Save(utils.GetTestFileLocation(), testHarness);
                 }
    
             if (ResourceView == null)
                 ResourceView = "";
    
             ResourceView = ResourceView.ToLower().Trim();
    
             var DiaryResourceView = new Resource();
    
             if (ResourceView == "" || ResourceView == "employees") // set the default
                 {
                 DiaryResourceView.DefaultView = "employees";
                 DiaryResourceView.ResourceTitle = "Branch offices";
                 }
             else if (ResourceView == "equipment")
                 {
                 DiaryResourceView.DefaultView = ResourceView;
                 DiaryResourceView.ResourceTitle = "Equipment list";
                 }
    
             return View(DiaryResourceView);
         }
    
  2. SetView controller
            // this method, called from the index page, sets a session variable for 
            // the user that gets looped back to the index page to tell it what view 
            // to display. branch/employee or Equipment.
    
            public ActionResult setView(string ResourceView)
            {
              Session["ResourceView"] = ResourceView;
              return  RedirectToAction("Index");
            }

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, it's 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 perform a lookup against the cell selected and decide if we need to implement a rule.

// use this function to get a local list of employees/branches/equipment etc 
// and populate arrays as appropriate for checking business rules etc.

function GetLocationsAndEmployees() {
    $.ajax({
           url: '/home/GetSetupInfo',
           cache: false,
           success: function (resultData) {
                    ClearLists();
                    EmployeeList = resultData.Employees.slice(0);
                    BranchList = resultData.Branches.slice(0);
                    EquipmentList = resultData.Equipment.slice(0);
                    ClientList = resultData.Clients.slice(0); 
                }
         });

In this example, we don't allow users to click on "office" rows, so we raise an alert if the user makes a mistake...

var employeeResource =
       EmployeeList.find( // if the row clicked on is NOT in the known array of employeeID,
                          // then drop out (and alert...)
                         function (employee) 
                                { return employee.EmployeeID == resourceObj.id; }
                        )

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:

// set up for drag/drop of unassigned tasks into scheduler 
// *example only - if using a large data feed from a table*

        function InitDragDrop() {
            $('.draggable').each(function () {
                // create an Event Object 
                // ref: (http://arshaw.com/fullcalendar/docs/event_data/Event_Object/)
                // it doesn't need to have a start or end
                var table = $('#UnScheduledEvents').DataTable();

                var eventObject = {
                    id: $(table.row(this).data()[0]).selector,
                    clientId: $(table.row(this).data()[1]).selector,
                    start: $(table.row(this).data()[2]).selector,
                    end: $(table.row(this).data()[3]).selector,
                    title: $(table.row(this).data()[4]).selector,
                    duration: $(table.row(this).data()[5]).selector,
                    notes: $(table.row(this).data()[6]).selector,
                    color: 'tomato'
                }

                // gotcha: MUST be named "event", for *external dropped objects* and 
                // some rules:   http://fullcalendar.io/docs/dropping/eventReceive/
                $(this).data('event', eventObject);

                // make the event draggable using jQuery UI
                $(this).draggable({
                    activeClass: "ui-state-hover",
                    hoverClass: "ui-state-active",
                    zIndex: 999,
                    revert: true,      // will cause the event to go back to its
                    revertDuration: 0  //  original position after the drag
                });
            });
        };

When the item is dropped, it is hooked by the FullCalendar 'eventReceive' method. In our example, we ask the user to confirm before proceeding:

eventReceive: function (event) { 
var confirmDlg = confirm('Are you sure you wish to assign this event?');

if (confirmDlg == true) {
    var eventDrag = {
        title: event.title,
        start: new Date(event.start),
        resourceId: event.resourceId,
        clientId: null,
        duration: 30,
        equipmentId: null,
        BranchID: null,
        statusString: "",
        notes: "",
    }
    UpdateEventMove(eventDrag, null);
}

Now, here's 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.

function UpdateEventMove(event, view) {
    // determine the view and from this set the correct EmployeeID or ResourceID 
    // before sending down to server
    if (ResourceView == 'employees')
        event.employeeId = event.resourceId;
    else {
        event.employeeId = $('#newcboEmployees').val();
    }
    var dataRow = {
        'Event': event
    }

    $.ajax({
        type: 'POST',
        url: "/Home/PushEvent",
        dataType: "json",
        contentType: "application/json",
        data: JSON.stringify(dataRow)
    });
}

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 that's 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 (e.g.: 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. (e.g.: 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:

<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'.

$('#submitButton').on('click', function (e) {
    e.preventDefault();
    SelectedEvent.title = $('#title').val();
    SelectedEvent.duration = $('#duration').val();
    SelectedEvent.equipmentId = $('#cboEquipment').val();
    SelectedEvent.branchId = $('#branch').val();
    SelectedEvent.clientId = $('#cboClient').val();
    SelectedEvent.notes = $('#notes').val();
    SelectedEvent.resourceId = $('#cboEmployees').val();
    SelectedEvent.statusString = $('#cboStatus').val();
    SelectedEvent.repeat = $('#repeatCRONEdit').cron("value")
    UpdateEventMove(SelectedEvent, null);
    doSubmit();
});

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.

if (SelectedEvent.repeat != null)
    $('#repeatCRONEdit').cron("value", SelectedEvent.repeat);
else
    $('#repeatCRONEdit').cron("value", "* * * * *");

NCronTab

Ok, so we have the building and storage of the repeat/recurring event information and the UI taken care of - now let's 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.

Let's 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.
    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.
    repeatEvents = testHarness.ScheduleEvents.Where(s => (s.repeat != null));
  3. We then examine each repeat event (i.e.: 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.
    if (repeatEvents!=null)
    foreach (var rptEvnt in repeatEvents)
    {
        var schedule = CrontabSchedule.Parse(rptEvnt.repeat);
        var nextSchdule = schedule.GetNextOccurrences(Start, End);
        foreach (var startDate in nextSchdule)
        { 
            ScheduleEvent itm = new ScheduleEvent();
            itm.id = rptEvnt.EventID;
            if (rptEvnt.title.Trim() == "")
                itm.title = rptEvnt.clientName;
            else itm.title = rptEvnt.title;
            itm.start = startDate.ToString("s");
            itm.end = startDate.AddMinutes(30).ToString("s");
            itm.duration = rptEvnt.duration.ToString();
            itm.notes = rptEvnt.notes;
            itm.statusId = rptEvnt.statusId;
            itm.statusString = rptEvnt.statusString;
            itm.allDay = false;
            itm.EmployeeId = rptEvnt.EmployeeId;
            itm.clientId = rptEvnt.clientId;
            itm.clientName = rptEvnt.clientName;
            itm.equipmentId = rptEvnt.equipmentID;
            itm.EmployeeName = rptEvnt.EmployeeName;
            itm.repeat = rptEvnt.repeat;
            itm.color = rptEvnt.statusString;
            if (resourceView == "employees")
                itm.resourceId = rptEvnt.EmployeeId;
            else itm.resourceId = rptEvnt.equipmentID;
            EventItems.Add(itm);
        }
    }

The final thing to note in the above code are 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 it 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.

Finally, as always, please consider voting for this article if you liked it!

History

  • 3rd September, 2016: Version 1

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

AJSON
Engineer
United Kingdom United Kingdom
Allen is a consulting architect with a background in enterprise systems. His current obsessions are IoT, Big Data and Machine Learning. When not chained to his desk he can be found fixing broken things, playing music very badly or trying to shape things out of wood. He runs his own company specializing in systems architecture and scaling for big data and is involved in a number of technology startups.

You may also be interested in...

Comments and Discussions

 
QuestionReplace TestHarness with my database Pin
Member 1394106010-Jan-19 3:50
memberMember 1394106010-Jan-19 3:50 
PraiseMy vote of Five Pin
novreis23-Jul-18 8:57
membernovreis23-Jul-18 8:57 
QuestionHow to display 11:30am/12:30am/...... instead 11:00am/12:00am/...? Pin
novreis23-Jul-18 8:55
membernovreis23-Jul-18 8:55 
QuestionError in Full Calander Pin
rajendra sharma11-Jan-18 0:57
memberrajendra sharma11-Jan-18 0:57 
QuestionDisengaging the test harness Pin
Member 1325480913-Jun-17 1:17
memberMember 1325480913-Jun-17 1:17 
Questionwhere to get database for this Pin
Member 1239013723-May-17 3:54
memberMember 1239013723-May-17 3:54 
GeneralMy vote of 5 Pin
Tridip Bhattacharjee23-Feb-17 22:54
professionalTridip Bhattacharjee23-Feb-17 22:54 
GeneralRe: My vote of 5 Pin
AJSON26-Feb-17 5:47
mvaAJSON26-Feb-17 5:47 
QuestionHow to change the hours view Pin
LeBarros7-Feb-17 8:58
memberLeBarros7-Feb-17 8:58 
QuestionMy vote of 5 Pin
Ahmet Abdi4-Sep-16 8:13
memberAhmet Abdi4-Sep-16 8:13 
GeneralRe: My vote of 5 Pin
AJSON4-Sep-16 9:11
mvaAJSON4-Sep-16 9:11 
GeneralMy vote of 5 Pin
Shashangka Shekhar3-Sep-16 7:28
mvaShashangka Shekhar3-Sep-16 7:28 
GeneralRe: My vote of 5 Pin
AJSON3-Sep-16 10:02
mvaAJSON3-Sep-16 10:02 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Cookies | Terms of Use | Mobile
Web01 | 2.8.190518.1 | Last Updated 4 Sep 2016
Article Copyright 2016 by AJSON
Everything else Copyright © CodeProject, 1999-2019
Layout: fixed | fluid