Implement jQuery FullCalendar Plugin in MVC 3

Tags: MVC, FullCalendar, jQuery

Let's implement Shaw's awesome FullCalendar jQuery plugin with an MVC 3 project. I'll assume you've downloaded the bits and have included the fullcalendar.min.js script in your project. Here I want to illustrate two things: (1) populating the calendar with events from the database; and (2) handling the calendar's dayClick event to create a new event.

But first we need a model to represent an event:

public class EventModel
{
    public int ID { get; set; }
    public string Title { get; set; }
    public DateTime StartDateTime { get; set; }
    public DateTime EndDateTime { get; set; }
    public bool IsAllDay { get; set; }
}

Just drop that in your Models folder or the project where your domain model lives. Next create a CalendarController that knows about the EventModel and passes it to your views. I've done that below. And to populate the calendar I'll add a GetEvents action method that will be called asynchronously.

public class CalendarController : Controller
{
    private readonly EventModel _eventModel;
        
    public CalendarController()
    {
        _eventModel = new EventModel();
    }
 
    public ActionResult Index()
    {
        return View();
    }
 
    public JsonResult GetEvents(double start, double end)
    {
        var startDateTime = FromUnixTimestamp(start);
        var endDateTime = FromUnixTimestamp(end);
 
        var repository = new EventRepository();
        var events = repository.GetEvents(startDateTime, endDateTime);
 
        var clientList = new List<object>();
 
        foreach (var e in events)
        {
            clientList.Add(
                new
                {
                    id = e.ID,
                    title = e.Title,
                    description = e.Description,
                    start = e.StartUnixTimestamp,
                    end = e.EndUnixTimestamp,
                    allDay = e.IsAllDay,
                    url = e.Url
                });
        }
        return Json(clientList.ToArray(), JsonRequestBehavior.AllowGet);
    }
 
    private static DateTime FromUnixTimestamp(double timestamp)
    {
        var origin = new DateTime(1970, 1, 1, 0, 0, 0, 0);
        return origin.AddSeconds(timestamp);
    }
}

I won't go into the repository since that's just standard data access plumbing. You might use LINQ against EF 4 or NHibernate against MySQL. It doesn't matter. For our purposes the call to our repository returns an IQueryable collection called events. We then enumerate over this collection and transform it into an object notation (JSON) that the calendar will understand. And since FullCalendar works with Unix timestamps we have a little helper method in there to convert the double into a .NET DateTime.

Right click on the Index action method and create a bare bones view. For this walkthrough don't bother creating a strongly-typed view, a partial view, or a layout/master page view. Just a bare bones Index.cshtml to host the calendar plugin is sufficient. In the header reference the scripts per FullCalendar's specification. Here's the whole Index.cshtml page in all its glory:

 
@{
    Layout = null;
}
 
<!DOCTYPE html>
 
<html>
    <head>
        <title>Calendar Demo</title>
        <link href="@Url.Content("~/Content/fullcalendar.css")" rel="stylesheet" type="text/css" />
        <script src="@Url.Content("~/Scripts/jquery-1.5.min.js")" type="text/javascript"></script>
        <script src="@Url.Content("~/Scripts/jquery-ui-1.8.9.custom.min.js")" type="text/javascript"></script>
        <script src="@Url.Content("~/Scripts/fullcalendar.min.js")" type="text/javascript"></script>
    
        <script type='text/javascript'>
 
            function padDigits(n, totalDigits) {
                n = n.toString();
                var pd = '';
                if (totalDigits > n.length) {
                    for (i = 0; i < (totalDigits - n.length); i++) {
                        pd += '0';
                    }
                }
                return pd + n.toString();
            }
 
            function createEvent(date, allDay, jsEvent, View) {
                var eventDate = padDigits(date.getMonth() + 1, 2) + '-' + 
                    padDigits(date.getDate(), 2) + '-' + 
                    padDigits(date.getFullYear(), 4);
                window.location.href = "/Calendar/Create/" + eventDate;               
            }  
 
            $(document).ready(function () {
                $('#calendar').fullCalendar({
                    editable: true,
                    events: "/Calendar/GetEvents",
                    eventClick: function (calEvent, jsEvent, view) {
                        alert('Event: ' + calEvent.title);
                    },
                    dayClick: function (date, allDay, jsEvent, view) {
                        createEvent(date, allDay, jsEvent, view);
                    }
                });
            });
        </script>
 
 
        <style type='text/css'>
            
            body {
        margin-top: 40px;
        text-align: center;
        font-size: 14px;
        font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif;
        }
 
        #calendar {
        width: 900px;
        margin: 0 auto;
        }
        
        </style>
 
    </head>
    
    <body>
        <div id="calendar"></div>
    </body>
</html>

Pretty straightforward stuff. The calendar plugin automatically calls its events function to populate the calendar and will pass in two Unix timestamp values "start" and "end" to the route we specify (in this case "/Calendar/GetEvents"). Our CalendarController must convert those values to .NET DateTime types to call the repository and get the events in that date range. So we're set on populating the calendar.

Now we want to implement the creation of new events. To do that I recommend handling the FullCalendar dayClick event. Notice that the first argument "date" is the date of the selected cell. That's all I care about. What we want to do is pass it along to our own createEvent function and use the padDigits helper to format it in the form mm-dd-yyyy. Why? Because we can leverage the MVC framework, which will automatically cast it to a .NET DateTime for us. Here's where the javascript is going to call our controller:

window.location.href = "/Calendar/Create/" + eventDate;

Of course in MVC we have to have a route added to our map to support that extensionless url. So go into Global.asax and add it before the Default route:

routes.MapRoute(
    "CreateNewCalendarEvent",
    "Calendar/{action}/{eventDate}",
    new { controller = "Calendar", action = "Create" }
);

Notice that "eventDate" is spelled exactly like the javascript variable. This is the querystring name that javascript will use. If you don't use an identical name then the Controller base class will not find the value in the Request.QueryString and pass a null to your parameter in the action method instead. Anyway, time to create the action method in the CalendarController class:

public ActionResult Create(DateTime eventDate)
{
    _eventModel.StartDateTime = eventDate;
    _eventModel.EndDateTime = eventDate;
    return View(_eventModel);
}

That's it! Here's where the magic happens. The MVC framework is smart enough to convert the querystring parameter "eventDate" into a DateTime. If it fails then you'll get a runtime exception. But we're passing it in the format mm-dd-yyyy which MVC can easily convert into a .NET DateTime type. That's the day on the calendar that the user clicked on so we've already got one property set. I won't show it here but the obvious next step is to pass the EventModel instance to a view scaffolded with the Create template so that the end user can provide a title as well as a begin and end time for the event. That will get posted back as a FormCollection like any other MVC create action method where you'd first save it to the database and then redirect the user back to the calendar page. The new event shows up on the day along with the others and you're good to go.

I also won't go into handling the FullCalendar eventClick event. You'll notice above I just stubbed it out with a simple alert. If you want to allow users to edit existing events then you'll want to replace that functionality with something similar to what I've described here.

Add a Comment