Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Web Diary System for jQuery and C# MVC

DZone's Guide to

Web Diary System for jQuery and C# MVC

What should a coder do when they can't keep organized? Make a calendar app! Learn how to use a few libraries, Twitter Boostrap, and C# MVC to do this.

· Web Dev Zone ·
Free Resource

Learn how error monitoring with Sentry closes the gap between the product team and your customers. With Sentry, you can focus on what you do best: building and scaling software that makes your users’ lives better.

Introduction

This article describes using the very fine open source jQuery plugin, “FullCalendar,” by Adam Shaw to develop an appointment booking system. I will demonstrate numerous ways to use the plugin and show how it can integrate with an SQL Backend with Entity-Framework. The aim of this article is to give you almost everything you need, that you can tweak immediately, to bring diary/appointment functionality to your MVC application. This is a complete walk-through including setup of a linked EF database in SQL. I am going to use the Twitter Bootstrap framework to help speed things along. I am also going into a lot of detail to help those wishing to use the plugin to get started as fast as possible. The attached project is written in C# MVC 4. 

Image title

Background

I was recently looking for a good reliable web-based diary plugin that would allow me to give a solid appointment management functionality to my apps. Apart from some commercial offerings, the most suitable solution open source plugin I found was FullCalendar. Get it here. The demo scenario will have the following functionality:

  • For each day, manage appointments.
  • In month view, see an overview/summary of information on all appointments/events.

Gotchas

Some of the gotchas included date format (this plugin uses a Unix timestamp) and having to work with multiple plugin data sources as I wanted different kinds of data queried depending on the view of the calendar being seen (day, week, month, etc.). I also had some issues with trying to get the events rendering exactly how I wanted - the DOH! moment came when I realized I was referencing some CSS in the wrong order/sequence. For drag/drop functionality, the plugin depends on jQueryUI to be included, and finally, in order to stop the plugin from triggering before I wanted it to, I had to control the flow of loading of data carefully. 

Setting Things Up

To get started:

Image title

Image title

You will also note I have added the jQuery-UI.js file to my bundle - this is needed to support drag/drop.

<div id='calendar' style="width:65%"></div> 


Note that the inline style is simply to force the width for screenshots inside the CodeProject publishing size guidelines!

Create a new C# MVC project and clear out the default view templates. Download FullCalendar. Then, download the Twitter Bootstrap (I am using the stable version 2.3.2 until v3 is out of RC). For both libraries, unpack and place the CSS file in your /Content folder, and the JS scripts in the /Scripts folder. In your MVC project, add the bootstrap files to your script bundles. This is in BundleConfig.cs and is located in your App_Start folder. We will do our work in the default index.cshml page, so in there, place a DIV to hold the plugin. Finally, to prove its at least loading and rendering correctly, we will add some basic JS when the document is ready and loaded:

Image title

The initialization settings tell the plugin what to show in the header, the default view (in this case "agenda day"), and to have a default time slot of 15 minutes. When we run the project, as expected, the plugin appears:

Image title


Using the Code

Rather than use dummy client-side data, I wanted to show how we might store and manipulate data in a working environment, so I put together an SQL database to store data and linked to it using Entity Framework. 

The SQL table is called "AppointmentDiary" and contains these fields:

Image title

The ID is an auto-increment, Title gets displayed within the Calendar control as an event, SomeImportantKey represents a referential link to some other data table, DateTimeScheduled is the diary date/time, AppointmentLength is an integer that represents the number of minutes the appointment lasts, and finally StatusENUM is an integer that links to an ENUM value to say if the appointment is an enquiry, a booking, confirmed, etc.

We add a new Entity Data Model to the project and point it to our new database and table. This creates an EDMX file that in our case we call "Diary" and reference as "DiaryContainer."

As this example is date based, I didn't want to pre-populate it with data, because any events I stored would be out of date the day after publishing! For this reason, I put together a quick method that initialises the database. The method is called by pressing a button in the browser that sends an Ajax call to the application (let's minimize full server round-trips when we can!).

The button starts life as a link:

<a href="#" id="btnInit" class="btn btn-secondary">Initialise database!</a>  

But through the wonder of Bootstrap, the addition of the class "btn" turns the link into a button, and "btn-secondary" gives it a gray color.

Image title

The script to call the initialized code is basic enough:


$('#btnInit').click(function () {
  $.ajax({
        type: 'POST',
        url: "/Home/Init",
        success: function (response) {
            if (response == 'True') {
                $('#calendar').fullCalendar('refetchEvents');
                alert('Database populated! ');
            }
            else {
                alert('Error, could not populate database!');
            }
        }
    }); 
}); 

Server-side, we have a controller, "/Home/Init," that calls a method in a shared file called Utils called, "InitialiseDiary." The objective of this method is to generate a series of test diary appointments that center around the current date. It creates some items for the current date and others before and after the date.


public static bool InitialiseDiary() { 
    // init connection to database
    DiaryContainer ent = new DiaryContainer();
    try
    { 
        for(int i= 0; i<30; i++){
        AppointmentDiary item = new AppointmentDiary();
        // record ID is auto generated
        item.Title = "Appt: " + i.ToString();
        item.SomeImportantKey = i;
        item.StatusENUM = GetRandomValue(0,3); // random is exclusive - we have three status enums
        if (i <= 5) // create ten appointments for todays date
        {
            item.DateTimeScheduled = GetRandomAppointmentTime(false, true);
        }
        else {  // rest of appointments on previous and future dates
            if (i % 2 == 0)
                item.DateTimeScheduled = GetRandomAppointmentTime(true, false);
                // flip/flop between date ahead of today and behind today
            else item.DateTimeScheduled = GetRandomAppointmentTime(false, false);
        }
        item.AppointmentLength = GetRandomValue(1,5) * 15;
        // appoiment length always less than an hour in this demo in blocks of fifteen minutes

        ent.AppointmentDiary.Add(item);
        ent.SaveChanges();
    }
    }
    catch (Exception)
    {
        return false;
    }

    return ent.AppointmentDiary.Count() > 0;         
}


This method calls two other supporting methods, one which generates a random number, the other that generates a random date/time.

/// <summary>
/// sends back a date/time +/- 15 days from todays date
/// </summary>
public static DateTime GetRandomAppointmentTime(bool GoBackwards, bool Today) {
    Random rnd = new Random(Environment.TickCount); // set a new random seed each call
    var baseDate = DateTime.Today;
    if (Today)
        return new DateTime(baseDate.Year, baseDate.Month, 
                baseDate.Day, rnd.Next(9, 18), rnd.Next(1, 6)*5, 0);
    else
    {
        int rndDays = rnd.Next(1, 15);
        if (GoBackwards)
            rndDays = rndDays * -1; // make into negative number
        return new DateTime(baseDate.Year, baseDate.Month, 
          baseDate.Day, rnd.Next(9, 18), rnd.Next(1, 6)*5, 0).AddDays(rndDays);             
    }
} 


Now we have that, we can generate sample data by running the application and clicking the button (made amazing and wonderful by the magic of the twittering bootstrap). However, before we click that object of wonder, lets put in a controller and method to send our sample data back to the plugin.

The full calendar can create diary "events" to render in a number of different ways. One of the more common is to send in data as a JSON list. 

"Events" need at a minimum the following information:

  • ID: A unique identifier for the diary item.
  • Title: Some text to render on the screen.
  • Start: Starting date/time of the event.
  • End: Ending date/time of the event.

You can also send some other information back, such as the color you would like the event to be on the screen, or a CSS class name if you wish to render the event in a particular way. You can also send back any other information you might need to handle client-side, for example, key fields to related data tables.

To hook into the data table, I created a model called DiaryEvent, and gave it some fields that map to the Entity model.


public class DiaryEvent
{
   public int ID;
   public string Title;
   public int SomeImportantKeyID; 
   public string StartDateString;
   public string EndDateString;
   public string StatusString;
   public string StatusColor;
   public string ClassName;
   ...
}


In addition, I added some methods to extract information and save it back. The first method we are interested in takes as its parameters a start and end date, and returns a list of DiaryEvents:

public static List<DiaryEvent> LoadAllAppointmentsInDateRange(double start, double end)
{
    var fromDate = ConvertFromUnixTimestamp(start);
    var toDate = ConvertFromUnixTimestamp(end);
    using (DiaryContainer ent = new DiaryContainer())
    {
        var rslt = ent.AppointmentDiary.Where(s => s.DateTimeScheduled >= 
            fromDate && System.Data.Objects.EntityFunctions.AddMinutes(
            s.DateTimeScheduled, s.AppointmentLength) <= toDate);
        List<DiaryEvent> result = new List<DiaryEvent>();
        foreach (var item in rslt)
        {
            DiaryEvent rec = new DiaryEvent();
            rec.ID = item.ID;
            rec.SomeImportantKeyID = item.SomeImportantKey;
            rec.StartDateString = item.DateTimeScheduled.ToString("s");
            // "s" is a preset format that outputs as: "2009-02-27T12:12:22"

            rec.EndDateString = item.DateTimeScheduled.AddMinutes(item.AppointmentLength).ToString("s");
            // field AppointmentLength is in minutes

            rec.Title = item.Title + " - " + item.AppointmentLength.ToString() + " mins";
            rec.StatusString = Enums.GetName<AppointmentStatus>((AppointmentStatus)item.StatusENUM);
            rec.StatusColor = Enums.GetEnumDescription<AppointmentStatus>(rec.StatusString);
            string ColorCode = rec.StatusColor.Substring(0, rec.StatusColor.IndexOf(":"));
            rec.ClassName = rec.StatusColor.Substring(rec.StatusColor.IndexOf(":")+1, 
                            rec.StatusColor.Length - ColorCode.Length-1);
            rec.StatusColor = ColorCode;                   
            result.Add(rec);
        }
        return result;
    }


The code is quite simple:

  1. We connect to the database using Entity Framework, run a LINQ query that extracts all appointment events between the start and end dates (using "EntityFunctions.AddMinutes" to create an end-date on the fly from the StartDateTime + AppointmentLength).
  2. For each event returned, we create a DiaryEvent item and add the data table record information to it ready to send back.

Some things to note - first, FullCalander deals in dates in a UNIX format, therefore we had to run a conversion for this before querying. Second, I stored the color attribute of the "Status" of the event in the "DESCRIPTION ANNOTATION" of the StatusENUM and then used a method to extract the description, color code, etc. The color code includes a CSS "class name" we will use later in the article to make the event item look a wee bit fancier.

CSS:

.ENQUIRY {

    background-color: #FF9933;
    border-color: #C0C0C0; 
    color: White;
    background-position: 1px 1px;
    background-repeat: no-repeat;
    background-image: url('Bubble.png');
    padding-left: 50px;
}

.BOOKED {
    background-color: #33CCFF;
    border-color: #C0C0C0;
    color: White;
    background-position: 1px 1px;
    background-repeat: no-repeat;
    background-image: url('ok.png');
    padding-left: 50px;
... etc...   


StatusENUM: 

Image title

Method to extract CSS class/color from Description attribute of StatusENUM:

public static string GetEnumDescription<T>(string value)
{
    Type type = typeof(T);
    var name = Enum.GetNames(type).Where(f => f.Equals(value, 
        StringComparison.CurrentCultureIgnoreCase)).Select(d => d).FirstOrDefault();
    if (name == null)
    {
        return string.Empty;
    }
    var field = type.GetField(name);
    var customAttribute = field.GetCustomAttributes(typeof(DescriptionAttribute), false);
    return customAttribute.Length > 0 ? ((DescriptionAttribute)customAttribute[0]).Description : name;
}


Ok, almost there, we need to take the list and put that into JSON format to send back to the Plugin. This is done in a controller.

public JsonResult GetDiaryEvents(double start, double end)
{
    var ApptListForDate = DiaryEvent.LoadAllAppointmentsInDateRange(start, end);
    var eventList = from e in ApptListForDate
                    select new
                    {
                        id = e.ID,
                        title = e.Title,
                        start = e.StartDateString,
                        end = e.EndDateString,
                        color = e.StatusColor,
                        someKey = e.SomeImportantKeyID,
                        allDay = false
                    };
    var rows = eventList.ToArray();
    return Json(rows, JsonRequestBehavior.AllowGet); 
}


Finally (!), let's go back to our INDEX page, and add one line that tells the FullCalendar plugin where to go to get its JSON data.

Image title

Now when we run the application, we can click our INIT button, which will populate the database, and see the data coming through.

Image title

Looking good.


Image title

You will recall that earlier I talked about color, and ClassName - the plugin that allows us to pass in a CSS class name, and it will use this to help render the event. In the CSS I showed earlier, I added an icon image to the CSS decoration to make the event look visually better. To make this pass through, we add in the  ClassName  string:

Image title


And this is how it renders:

Image title

That's superb! Now let's look at client-side user functionality - how can the user interact with our diary?

Typically, one would expect to be able to select an event and get information on it, edit it, move it around, resize it, etc. Let's see how we could make that happen.

First, let's examine the information we sent along with the event. We can do this by adding a callback function when we initialize the plugin:

eventClick: function (calEvent, jsEvent, view) {
    alert('You clicked on event id: ' + calEvent.id
        + "\nSpecial ID: " + calEvent.someKey 
        + "\nAnd the title is: " + calEvent.title); 


The important param here is the first one, calEvent - using it, we can access any of the data we sent through when we sent back the JSON records.

Here's how it looks:

Image title

Using this information, you can hook in and use it to pop up an edit form, redirect to a details window, etc. - your code chops are your magic wand.

To move an event around the plugin, we hook eventDrop.

eventDrop: function (event, dayDelta, minuteDelta, allDay, revertFunc) {
    if (confirm("Confirm move?")) {
        UpdateEvent(event.id, event.start);
    }
    else {
        revertFunc();
    }  
}


The parameters dayDelta and minuteDelta are useful, as they tell us the amount of days or minutes the event has been moved by. We could use this to adjust the backend database, but there is another method I used.

I decided, in this case, to take the event object and use its information. When we create the event initially, it is set with the start/end date and time we give it. When the item is moved/dropped, that date and time change, however, the ID and the extra information we saved along with the event do not. Therefore, my strategy, in this case, was to take the ID and the new start and end time and use these to update the database.

FullCalendar provides a "revertFunc" method that resets the event move to its previous state if the user decides not to confirm the event move.

After the event move, I call a local script function called "UpdateEvent." This takes the relevant data, and sends it by Ajax to a controller back on the server:

function UpdateEvent(EventID, EventStart, EventEnd) {
    var dataRow = {
        'ID': EventID,
        'NewEventStart': EventStart,
        'NewEventEnd': EventEnd
    }
    $.ajax({
        type: 'POST',
        url: "/Home/UpdateEvent",
        dataType: "json",
        contentType: "application/json",
        data: JSON.stringify(dataRow)
    }); 
}


Controller:

public void UpdateEvent(int id, string NewEventStart, string NewEventEnd)
{
    DiaryEvent.UpdateDiaryEvent(id, NewEventStart, NewEventEnd); 
} 

Method called from the controller:

public static void UpdateDiaryEvent(int id, string NewEventStart, string NewEventEnd) 
{
    // EventStart comes ISO 8601 format, eg:  "2000-01-10T10:00:00Z" - need to convert to DateTime
    using (DiaryContainer ent = new DiaryContainer()) {
        var rec = ent.AppointmentDiary.FirstOrDefault(s => s.ID == id);
        if (rec != null)
        {
            DateTime DateTimeStart = DateTime.Parse(NewEventStart, null, 
               DateTimeStyles.RoundtripKind).ToLocalTime(); // and convert offset to localtime
            rec.DateTimeScheduled = DateTimeStart;
            if (!String.IsNullOrEmpty(NewEventEnd)) { 
                TimeSpan span = DateTime.Parse(NewEventEnd, null, 
                   DateTimeStyles.RoundtripKind).ToLocalTime() - DateTimeStart;
                rec.AppointmentLength = Convert.ToInt32(span.TotalMinutes);
                }
            ent.SaveChanges();
        }
    }
} 


The important thing to note here is that the date format is sent in IS8601 format, so we need to convert it. I also used a Timespan to calculate the new appointment length, if any exist.

Event resizing, and updating the database appointment length server-side, is done in a similar fashion. First, hook the event:

eventResize: function (event, dayDelta, minuteDelta, revertFunc) {
    if (confirm("Confirm change appointment length?")) {
        UpdateEvent(event.id, event.start, event.end);
    }
    else {
        revertFunc();
    } 
},  


You will notice that I have used the same controller and method to update the database - all in an effort to reuse code wherever possible! What happens is that if the update method sees that a new end date/time has been sent in, it assumes a RESIZE has happened and adjusts the "AppointmentLength" value accordingly, or else it just updates the new start/date time.

if (!String.IsNullOrEmpty(NewEventEnd)) { 
    TimeSpan span = DateTime.Parse(NewEventEnd, null, DateTimeStyles.RoundtripKind).ToLocalTime() - DateTimeStart;
    rec.AppointmentLength = Convert.ToInt32(span.TotalMinutes); 
}


Now, that's great, and we have a lovely warm fuzzy feeling about making it happen until we turn to look at the MONTH view....


Image title

Oh dear .... it renders every single event and makes our calendar look rather messy... it would be better, for month view, if the control just showed a summary of appointment/diary events for each date. There is an elegant solution to this issue in "data sources." 

FullCalendar can store an array of event SOURCES. All we need to do is hook into the "viewRender" event, query what kind of view is active (day, week, month), and based on this, change the source and tell the plugin to refresh the data.

First, let's go back to our index.cshtml plugin init section and remove the reference to the data path as we will replace it with something else.

Image title

To facilitate the new functionality we require, I created two variables to hold the URL path of each view. 

var sourceSummaryView = { url: '/Home/GetDiarySummary/' };
var sourceFullView = { url: '/Home/GetDiaryEvents/' }; 

What we are aiming to do is hook into the Click/change event of the view button group.
Image title


The way to do this is to drop in code that examines when the user clicks the buttons to change the view - this is "viewRender." Two parameters are passed, the first is "view" and that tells us what was just clicked: 

Image title

So depending on which view was clicked, we remove any data sources from the source array (by name), remove any events from the plugin, and finally assign one of the string variables we created earlier as the new data source which then gets immediately loaded.  

As our summary view holds slightly different information, we need to create a different method to extract data from the server side. This is almost the same as our previous query, with the exception that we need to extend our LINQ query to use GROUP and COUNT. Our objective is to group by date and get a count of diary events for each day. Let's have a quick look at that method:

public static List<DiaryEvent> LoadAppointmentSummaryInDateRange(double start, double end)
{
    var fromDate = ConvertFromUnixTimestamp(start);
    var toDate = ConvertFromUnixTimestamp(end);
    using (DiaryContainer ent = new DiaryContainer())
    {
        var rslt = ent.AppointmentDiary.Where(
           s => s.DateTimeScheduled >= fromDate && 
           System.Data.Objects.EntityFunctions.AddMinutes(s.DateTimeScheduled, s.AppointmentLength) <= toDate)
           .GroupBy(s => System.Data.Objects.EntityFunctions.TruncateTime(s.DateTimeScheduled))
           .Select(x => new { DateTimeScheduled = x.Key, Count = x.Count() });
        List<DiaryEvent> result = new List<DiaryEvent>();
        int i = 0;
        foreach (var item in rslt)
        {
            DiaryEvent rec = new DiaryEvent();
            rec.ID = i; //we dont link this back to anything as its a group summary
            // but the fullcalendar needs unique IDs for each event item (unless its a repeating event)

            rec.SomeImportantKeyID = -1;  
            string StringDate = string.Format("{0:yyyy-MM-dd}", item.DateTimeScheduled);
            rec.StartDateString = StringDate + "T00:00:00"; //ISO 8601 format
            rec.EndDateString = StringDate +"T23:59:59";
            rec.Title = "Booked: " + item.Count.ToString();
            result.Add(rec);
            i++;
        }
        return result;
    }
}

As before, we need to convert the incoming unix format date. Next, because we are grouping by date, but we are storing a dateTIME, we need to remove the time part from the query - for this, we use the EntityFunctions.TruncateTime method, and chain the result of this into a sub-select which gives us back a key field of date and a value of count.

Note that as this is summary information we don't have a key-id-field to link back to, but this is a problem for FullCalendar as it needs a unique ID for keeping track of events, so we assign an arbitrary one, in this case, of a simple counting variable ("i"). Also, because we stripped out the time part of the result to enable the grouping, we need to put it back in using the ISO format.

Once the code is implemented, everything looks far better!

Image title


The last thing we are going to do is add a new event when a blank space on the calendar is clicked.

We will construct a quick bootstrap modal popup to capture an event title, date/time, and appointment length: 

<div id="popupEventForm" class="modal hide" style="display: none;">
   <div class="modal-header"><h3>Add new event</h3></div>
  <div class="modal-body">
    <form id="EventForm" class="well">
        <input type="hidden" id="eventID">
        <label>Event title</label>
        <input type="text" id="eventTitle" placeholder="Title here"><br />
        <label>Scheduled date</label>
        <input type="text" id="eventDate"><br />
        <label>Scheduled time</label>
        <input type="text" id="eventTime"><br />
        <label>Appointment length (minutes)</label>
        <input type="text" id="eventDuration" placeholder="15"><br />
    </form>
</div>
  <div class="modal-footer">
    <button type="button" id="btnPopupCancel" data-dismiss="modal" class="btn">Cancel</button>
    <button type="button" id="btnPopupSave" data-dismiss="modal" class="btn btn-primary">Save event</button>
  </div>
</div> 


Next, we will add a hook to our friendly plugin to show this modal popup when a day slot is clicked.

dayClick: function (date, allDay, jsEvent, view) {
    $('#eventTitle').val("");
    $('#eventDate').val($.fullCalendar.formatDate(date, 'dd/MM/yyyy'));
    $('#eventTime').val($.fullCalendar.formatDate(date, 'HH:mm'));
    ShowEventPopup(date); 
},


A small script method initializes the popup form, first clearing any input values hanging around and then setting focus to the first input box. 

function ShowEventPopup(date) {
    ClearPopupFormValues();
    $('#popupEventForm').show();
    $('#eventTitle').focus(); 
}  

Image title

Finally, we have the script attached to the "save event" button to send the data back to the server via Ajax.

$('#btnPopupSave').click(function () {
    $('#popupEventForm').hide();
    var dataRow = {
        'Title':$('#eventTitle').val(),
        'NewEventDate': $('#eventDate').val(),
        'NewEventTime': $('#eventTime').val(),
        'NewEventDuration': $('#eventDuration').val()
    }
    ClearPopupFormValues();
    $.ajax({
        type: 'POST',
        url: "/Home/SaveEvent",
        data: dataRow,
        success: function (response) {
            if (response == 'True') {
                $('#calendar').fullCalendar('refetchEvents');
                alert('New event saved!');
            }
            else {
                alert('Error, could not save event!');
            }
        }
    });
}); 

If the server saves the record successfully, it sends back 'True' and we tell the plugin to refetch its events.

The very last thing I want to address is the double loading of data. When we are changing data sources using the viewRender callback, what happens is that on initial page load, the viewRender gets kicked off twice. To fix this, I added in a variable var CalLoading = true; at the top of the main script before the plugin is initialized, examine that when rendering the view, and reset it after the document is ready.  

Image title

So that's it! There's enough detail in this article to allow you, with very little effort, to include a quite useful diary functionality into your MVC application. 

What’s the best way to boost the efficiency of your product team and ship with confidence? Check out this ebook to learn how Sentry's real-time error monitoring helps developers stay in their workflow to fix bugs before the user even knows there’s a problem.

Topics:
jquery ,c# ,mvc ,web dev ,twitter bootstrap

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}