How to patch the Ajax Control Toolkit Calendar Extender control to show a Quarter view

In this series of educational articles, I am demonstrating how you might enhance the Calendar Extender control found in the publicly available Ajax Control Toolkit. All code here is an extension of my previous work, which was to enhance the Ajax Control Toolkit Calendar Extender so that it could be better utilised to select a birthdate. It introduced the decade view, and also allowed the setting of a initial view of the control, which can be Day, Month, Year and Decade. That article is available here: https://tonesdotnetblog.wordpress.com/2009/04/15/how-to-patch-the-ajax-control-toolkit-calendarextender-to-add-decade-support-and-initialview-part-1/

You can download the code for the latest version (20820) of the Ajax Control Toolkit from here: http://www.codeplex.com/AjaxControlToolkit.

My enhanced decade and initial view can be found here: http://cid-5e237543fffb2891.skydrive.live.com/self.aspx/Public/DecadeView.zip  This enhancement is an extension of that control.

All code here is published under the Microsoft Public Licence, found here: http://ajaxcontroltoolkit.codeplex.com/license

What we are trying to achieve:

There are certain situations,  where you might want to select a Quarter of a year. The meaning of that quarter doesn’t really matter. It can be a calendar quarter or a financial quarter. Calendars are such general devices that it doesn’t really matter. You may also want to select a quarter of a year. This innovation enables you to do either, on top of the existing enhanced calendar extender control that shows a decade view and initial view.

For example, a simple quarter selection may be as follows:

Calendar - Quarter View

Calendar - Quarter View

 That’s ok if you just want a quarter, however if you want to be able to select a quarter in a particular year, then you may want the following:

Calendar - Quarter/Year/Decade view

Calendar - Quarter/Year/Decade view

The code:

Like the InitialView property added in the decade example, we need to add in a new property to set the new Calendar view. So firstly, make a copy of the InitialView class file created in the version of the Ajax Control Toolkit that I provided the enhancement for decade view. Paste it in the same location as the InitialView.cs file. Then rename it as CalendarView.cs, as follows:

Creation of the CalendarView.cs file

Creation of the CalendarView.cs file

Next, open the file and change the code to reflect the new name and also the views that we wish to support. The existing view is a DayMonthYearDecade view, and will be the default. It is not good to change existing behaviour (although granted that I did this with decade view!) Add to this the Quarter and QuarterYearDecade enumerations. If we decide to add further views later, this is where we add them. Note that I am surrounding new code with the start tag S3 and the end tag E3. Don’t confuse these tags with S1/E1 and S2/E2 as they are from previous enhancements.

//S3 - Add Calendar View so that you can choose the calendar view for your date selection.
//Choosing a calendar view will close the calendar at the bottom view and return the selected date/value
namespace AjaxControlToolkit
{
    /// <summary>
    /// The calendar view of the calendar
    /// </summary>
    public enum CalendarView
    {
        DayMonthYearDecade = 0, //the default view.
        Quarter = 1,            //shows Quarter selection but not Year or Decade.
        QuarterYearDecade = 2   //shows Quarter Year Decade.
    }
}
//E3

 Next, we want to make the javascript consistent with the new enumeration. So head to the bottom of the CalendarBehavior.js file, copy the existing InitialView enumeration code, paste it, and rename InitialView in that instance to CalendarView.

The new code should look like this:

//S3 Calendar View. Copy the segment of code from S2/E2, rename to CalendarView then make it consistent
//with the values in the CalendarView.cs file
AjaxControlToolkit.CalendarView = function() {
    /// <summary>
    /// View of the calendar opened when the user selects the calendar
    /// </summary>
    /// <field name="DayMonthYearDecade" type="Number" integer="true" />
    /// <field name="Quarter" type="Number" integer="true" />
    /// <field name="QuarterYearDecade" type="Number" integer="true" />
    throw Error.invalidOperation();
}
AjaxControlToolkit.CalendarView.prototype = {
    DayMonthYearDecade: 0,
    Quarter: 1,
    QuarterYearDecade: 2
}
AjaxControlToolkit.CalendarView.registerEnum('AjaxControlToolkit.CalendarView');
//E3

We now need to create a property in the server side code so that we can set the calendar view. In the CalendarExtender.cs file, add the following infrastucture code:

//S3 - Copy initial view and rename to calendar view, then set the default values
[DefaultValue(CalendarView.DayMonthYearDecade)]
[ExtenderControlProperty]
[ClientPropertyName("calendarView")]
public virtual CalendarView CalendarView
{
   get { return GetPropertyValue("CalendarView", CalendarView.DayMonthYearDecade); }
   set { SetPropertyValue("CalendarView", value); }
}
//E3

We now follow the same pattern that we did for the decade view. Find the code that created the decade view, copy it, and rename it to quarter.

Original Code (from the Decade patch):

//S1 - Decades variables
this._decades = null;
this._decadesTable = null;
this._decadesBody = null;
//E1

Modified Code:

//S1 - Decades variables
this._decades = null;
this._decadesTable = null;
this._decadesBody = null;
//E1
//S3 - Quarters variables, copied and renamed from decades
this._quarters = null;
this._quartersTable = null;
this._quartersBody = null;
//E3   

At the same time, only a few lines below this, we set the default value in Javascript (for when the property is not set).

Original code:

//S2 - set the initial default in javascript
this._initialView = AjaxControlToolkit.InitialView.Day;
//E2

Modified code:

//S2 - set the initial default in javascript
this._initialView = AjaxControlToolkit.InitialView.Day;
//E2
//S3 - set the initial default in javascript
this._calendarView = AjaxControlToolkit.CalendarView.DayMonthYearDecade;
//E3

Alter the modes to support the addition of “quarters”. Note that we already modified this to support decade, so the code in the previous mod needs to be commented out and this new code added:

Original code:

//S1 - Decade view
//    this._modes = {"days" : null, "months" : null, "years" : null};
//    this._modeOrder = {"days" : 0, "months" : 1, "years" : 2 };
this._modes = { "days": null, "months": null, "years": null, "decades": null };
this._modeOrder = { "days": 0, "months": 1, "years": 2, "decades": 3 };
//E1

Modified code:

//S3 alter modes to add quarter   
////S1 - Decade view
////    this._modes = {"days" : null, "months" : null, "years" : null};
////    this._modeOrder = {"days" : 0, "months" : 1, "years" : 2 };
//this._modes = { "days": null, "months": null, "years": null, "decades": null };
//this._modeOrder = { "days": 0, "months": 1, "years": 2, "decades": 3 };
////E1
this._modes = { "days": null, "months": null, "years": null, "decades": null, "quarters": null };
this._modeOrder = { "days": 0, "months": 1, "years": 2, "decades": 3, "quarters": 4 };
//E3   

Add the getters and setters in the javascript code, just below the getters and setters from initialView, as follows (if you’re having trouble with this, read my previous article):

//S3 - set and get variables for javascript calendarView, copied off initialView
get_calendarView: function() {
    /// <value type="AjaxControlToolkit.CalendarView">
    /// The calendar view of the calendar, DayMonthYearDecade, Quarter, QuarterYearDecade
    /// </value>
    return this._calendarView;
},
set_calendarView: function(value) {
    if (this._calendarView != value) {
       this._calendarView = value;
       this.invalidate();
       this.raisePropertyChanged("calendarView");
    }
},
//E3

Next, we need to add code to build the quarter view, just like the decade view. In this case, we only need 4 cells (td) in the view table. So in the _buildBody function, just below the _buildDecade call, create a new _buildQuarter call, as follows:

Original code:

_buildBody: function() {
   /// <summary>
   /// Builds the body region for the calendar
   /// </summary>
 

   this._body = $common.createElementFromTemplate({
      nodeName: "div",
      properties: { id: this.get_id() + "_body" },
      cssClasses: ["ajax__calendar_body"]
   }, this._popupDiv);

   this._buildDays();
   this._buildMonths();
   this._buildYears();
   //S1 - Create the html elements for the calendar decade view
   this._buildDecades();
   //E1
},

 Modified code:

_buildBody: function() {
   /// <summary>
   /// Builds the body region for the calendar
   /// </summary>
 

   this._body = $common.createElementFromTemplate({
      nodeName: "div",
      properties: { id: this.get_id() + "_body" },
      cssClasses: ["ajax__calendar_body"]
   }, this._popupDiv);

   this._buildDays();
   this._buildMonths();
   this._buildYears();
   //S1 - Create the html elements for the calendar decade view
   this._buildDecades();
   //E1
   //S3 - Create the html elements for the calendar quarter view
   this._buildQuarters();
   //E3
},

 Just below the previously created _buildDecades function, add a new function _buildQuarters. This is just a copy of the _buildYears function, replacing all references to years with quarters:

//S3
_buildQuarters: function() {
  /// <summary>
  /// Builds a "quarters" view for the calendar
  /// </summary>
 

  var id = this.get_id();

  this._quarters = $common.createElementFromTemplate({
   nodeName: "div",
   properties: { id: id + "_quarters" },
   cssClasses: ["ajax__calendar_quarters"],
   visible: false
  }, this._body);
  this._modes["quarters"] = this._quarters;

  this._quartersTable = $common.createElementFromTemplate({
   nodeName: "table",
   properties: {
    id: id + "_quartersTable",
    cellPadding: 0,
    cellSpacing: 0,
    border: 0,
    style: { margin: "auto" }
   }
  }, this._quarters);

  this._quartersBody = $common.createElementFromTemplate({ nodeName: "tbody", properties: { id: id + "_quartersBody"} }, this._quartersTable);

  //the following line generates 2 rows in the table
  for (var i = 0; i < 2; i++) {
   var quartersRow = $common.createElementFromTemplate({ nodeName: "tr" }, this._quartersBody);

   //the following line generates 2 columns in the table, for a maximum grid of 2x2 = 4 squares.
   for (var j = 0; j < 2; j++) {
    var quarterCell = $common.createElementFromTemplate({ nodeName: "td" }, quartersRow);
    var quarterDiv = $common.createElementFromTemplate({
     nodeName: "div",
     properties: {
      id: id + "_quarter_" + i + "_" + j,
      mode: "quarter",
      quarter: ((i * 2) + j) + 1
     },
     events: this._cell$delegates,
     cssClasses: &#91;"ajax__calendar_quarter"&#93;
    }, quarterCell);
   }
  }
 },
//E3
&#91;/sourcecode&#93;

Ok, so now we don't want the Title bar to be displayed if the calendar view is set to Quarter. Also, we don't want the Today bar at the bottom to be displayed if the mode is not Day.

So in the function _performLayout, add in the following code, which prevents the display of the title and today bar under various conditions:

Original code:

&#91;sourcecode language='javascript'&#93;
_performLayout: function() {
  /// <summmary>
  /// Updates the various views of the calendar to match the current selected and visible dates
  /// </summary>;
  var elt = this.get_element();
  if (!elt) return;
  if (!this.get_isInitialized()) return;
  if (!this._isOpen) return;
  var dtf = Sys.CultureInfo.CurrentCulture.dateTimeFormat;
  var selectedDate = this.get_selectedDate();
  var visibleDate = this._getEffectiveVisibleDate();
  var todaysDate = this.get_todaysDate();
  var todaysDate = this.get_todaysDate();

Modified code:

_performLayout: function() {
  /// <summmary>
  /// Updates the various views of the calendar to match the current selected and visible dates
  /// </summary>
  var elt = this.get_element();
  if (!elt) return;
  if (!this.get_isInitialized()) return;
  if (!this._isOpen) return;
  var dtf = Sys.CultureInfo.CurrentCulture.dateTimeFormat;
  var selectedDate = this.get_selectedDate();
  var visibleDate = this._getEffectiveVisibleDate();
  var todaysDate = this.get_todaysDate();
  var todaysDate = this.get_todaysDate();</pre>
  //S3 Where we don't want the title to display, we simply hide it
  //this is only the case if you only want to select a value at the current mode
  if (this._calendarView == AjaxControlToolkit.CalendarView.Quarter)
     this._header.style.display = 'none';
  //E3

  //S3 We don't want the Today section displaying if it's Quarter selection
  if (this._calendarView != AjaxControlToolkit.CalendarView.DayMonthYearDecade)
     this._today.style.display = 'none';
  //E3       

Still in the _performLayout function, we need to add the layout switch statement for quarters. In the switch statement, below the decades case, add the following code. Note that we set the date on the inner div element, utilising the day part to store the quarter value.

Modified code:

//S3 add the quarters mode. This is modelled after the month view.             
   case "quarters":
      for (var i = 0; i < this._quartersBody.rows.length; i++) {
         var row = this._quartersBody.rows&#91;i&#93;;
         for (var j = 0; j < row.cells.length; j++) {
            var cell = row.cells&#91;j&#93;.firstChild;
            //the following line of code sets the date for selecting a quarter. Here we are
            //overriding the day value, as it is not being used for quarter selection
            cell.date = new Date(visibleDate.getFullYear(), 0, cell.quarter, this._hourOffsetForDst);
            cell.title = "Q" + cell.quarter;
            if (!cell.firstChild) {
               cell.appendChild(document.createTextNode("Q" + cell.quarter));
            }
            $common.removeCssClasses(cell.parentNode, &#91;"ajax__calendar_other", "ajax__calendar_active"&#93;);
            Sys.UI.DomElement.addCssClass(cell.parentNode, this._getCssClass(cell.date, 'q'));
         }
      }

      if (this._title.firstChild) {
         this._title.removeChild(this._title.firstChild);
      }
      this._title.appendChild(document.createTextNode(visibleDate.localeFormat("yyyy")));
      this._title.date = visibleDate;
      this._prevArrow.date = new Date(visibleDate.getFullYear() - 1, 0, 1, this._hourOffsetForDst);
      this._nextArrow.date = new Date(visibleDate.getFullYear() + 1, 0, 1, this._hourOffsetForDst);

      break;
//E3   
  }
  if (this._today.firstChild) {
     this._today.removeChild(this._today.firstChild);
  }
  this._today.appendChild(document.createTextNode(String.format(AjaxControlToolkit.Resources.Calendar_Today, todaysDate.localeFormat("MMMM d, yyyy"))));
  this._today.date = todaysDate;
},           
&#91;/sourcecode&#93;

The code in the _isSelected function needs to be modified to support the new quarter selection. Note that I made a mistake in the original decade code because I forgot to create a selected item for decade as well. Don't worry, we'll fix that here.

Original code:

&#91;sourcecode language='javascript'&#93;
_isSelected: function(date, part) {
  /// <summary>
  /// Gets whether the supplied date is the currently selected date
  /// </summary>
  ///
<param name="date" type="Date">The date to match</param>
  ///
<param name="part" type="String">The most significant part of the date to test</param>
  /// <returns type="Boolean" />
  var value = this.get_selectedDate();
  if (!value) return false;
  switch (part) {
   case 'd'  
    if (date.getDate() != value.getDate()) return false;
    // goto case 'M';
   case 'M':
    if (date.getMonth() != value.getMonth()) return false;
    // goto case 'y';
   case 'y':
    if (date.getFullYear() != value.getFullYear()) return false;
    break;
  }
  return true;
},

New code:

_isSelected: function(date, part) {
  /// <summary>
  /// Gets whether the supplied date is the currently selected date
  /// </summary>
  ///
<param name="date" type="Date">The date to match</param>
  ///
<param name="part" type="String">The most significant part of the date to test</param>
  /// <returns type="Boolean" />
  var value = this.get_selectedDate();
  if (!value) return false;
  //S3 change to handle quarter and decade. Using q to signify quarter. Behaves the same as day.
  if (part == 'd' || part == 'q') {
   if (date.getDate() != value.getDate()) return false;
  }
  if (part == 'M') {
   if (date.getMonth() != value.getMonth()) return false;
  }
  // using x to signify decade. This behaves the same as year.
  if (part == 'y' || part == 'x') {
   if (date.getFullYear() != value.getFullYear()) return false;
  }
  //  switch (part) {
  //   case 'd'  
  //    if (date.getDate() != value.getDate()) return false;
  //    // goto case 'M';
  //   case 'M':
  //    if (date.getMonth() != value.getMonth()) return false;
  //    // goto case 'y';
  //   case 'y':
  //    if (date.getFullYear() != value.getFullYear()) return false;
  //    break;
  //  }
  //E3
  return true;
},

In the _cell_onclick function, we need to return if a quarter is selected. We also need to add code to switch to year view if the title bar is selected, and also to quarter view if a year is selected.

Original code:

_cell_onclick: function(e) {
    /// <summary>
    /// Handles the click event of a cell
    /// </summary>
    ///
<param name="e" type="Sys.UI.DomEvent">The arguments for the event</param>

    e.stopPropagation();
    e.preventDefault();

    if (!this._enabled) return;

    var target = e.target;
    var visibleDate = this._getEffectiveVisibleDate();
    Sys.UI.DomElement.removeCssClass(target.parentNode, "ajax__calendar_hover");

    switch (target.mode) {
        case "prev":
        case "next":
            this._switchMonth(target.date);
            break;
        case "title":
            switch (this._mode) {
                case "days": this._switchMode("months"); break;
                case "months": this._switchMode("years"); break;
                //S1 - enable the clicking on the title to change to a decade view               
                case "years": this._switchMode("decades"); break;
                //E1          
            }
            break;
        case "month":
            if (target.month != visibleDate.getMonth()) {
                this._visibleDate = target.date;
            }
            this._switchMode("days");
            break;
        case "year":
             if (target.date.getFullYear() != visibleDate.getFullYear()) {
                   this._visibleDate = target.date;
             }
             this._switchMode("months");
            break;
        //S1 - On clicking a decade, switch to year view                        
        case "decade":
            if (target.date.getFullYear() != visibleDate.getFullYear()) {
                this._visibleDate = target.date;
            }
            this._switchMode("years");
            break;
        //E1                                       
        case "day":
            this.set_selectedDate(target.date);
            this._switchMonth(target.date);
            this._blur.post(true);
            this.raiseDateSelectionChanged();
            break;
        case "today":
            this.set_selectedDate(target.date);
            this._switchMonth(target.date);
            this._blur.post(true);
            this.raiseDateSelectionChanged();
            break;
    }
},

Modified code:

_cell_onclick: function(e) {
    /// <summary>
    /// Handles the click event of a cell
    /// </summary>
    ///
<param name="e" type="Sys.UI.DomEvent">The arguments for the event</param>

    e.stopPropagation();
    e.preventDefault();

    if (!this._enabled) return;

    var target = e.target;
    var visibleDate = this._getEffectiveVisibleDate();
    Sys.UI.DomElement.removeCssClass(target.parentNode, "ajax__calendar_hover");

    //S3 - now to return the date. If the CalendarView is Quarter or QuarterYear and the mode returned is
    //quarter then select the date, close the calendar and return
    var calendarView = this.get_calendarView();
    if (target.mode == "quarter" && (calendarView == AjaxControlToolkit.CalendarView.Quarter
                                  || calendarView == AjaxControlToolkit.CalendarView.QuarterYearDecade)) {
        this.set_selectedDate(target.date);
        this._blur.post(true);
        this.raiseDateSelectionChanged();
        return;
    }
    //E3

    switch (target.mode) {
        case "prev":
        case "next":
            this._switchMonth(target.date);
            break;
        case "title":
            switch (this._mode) {
                case "days": this._switchMode("months"); break;
                case "months": this._switchMode("years"); break;
                //S1 - enable the clicking on the title to change to a decade view               
                case "years": this._switchMode("decades"); break;
                //E1         
                //S3 - enable the clicking on the title to change to a years view                
                case "quarters": this._switchMode("years"); break;
                //E3                                    
            }
            break;
        case "month":
            if (target.month != visibleDate.getMonth()) {
                this._visibleDate = target.date;
            }
            this._switchMode("days");
            break;
        case "year":
            //S3 - on selecting a year, we need to switch to quarter view if it's a quarter, otherwise, back to normal behaviour
            //                if (target.date.getFullYear() != visibleDate.getFullYear()) {
            //                    this._visibleDate = target.date;
            //                }
            //                this._switchMode("months");
            if (calendarView != AjaxControlToolkit.CalendarView.QuarterYearDecade
             && calendarView != AjaxControlToolkit.CalendarView.Quarter) {
                if (target.date.getFullYear() != visibleDate.getFullYear()) {
                    this._visibleDate = target.date;
                }
                this._switchMode("months");
            }
            else {
                if (target.date.getFullYear() != visibleDate.getFullYear()) {
                    this._visibleDate = target.date;
                }
                this._switchMode("quarters");
            }
            //E3
            break;
        //S1 - On clicking a decade, switch to year view                        
        case "decade":
            if (target.date.getFullYear() != visibleDate.getFullYear()) {
                this._visibleDate = target.date;
            }
            this._switchMode("years");
            break;
        //E1                                       
        case "day":
            this.set_selectedDate(target.date);
            this._switchMonth(target.date);
            this._blur.post(true);
            this.raiseDateSelectionChanged();
            break;
        case "today":
            this.set_selectedDate(target.date);
            this._switchMonth(target.date);
            this._blur.post(true);
            this.raiseDateSelectionChanged();
            break;
    }
},

Next, clean up your mess. In the dispose method, just below the clean up code for decade, add the following code:

//S3 - Clean up quarter variables       
if (this._quartersBody) {
   for (var i = 0; i < this._quartersBody.rows.length; i++) {
      var row = this._quartersBody.rows&#91;i&#93;;
      for (var j = 0; j < row.cells.length; j++) {
         $common.removeHandlers(row.cells&#91;j&#93;.firstChild, this._cell$delegates);
      }
   }
   this._quartersBody = null;
}
//E3       
&#91;/sourcecode&#93;

Finally, the styles. Open up the Calendar.css file, and add the following styles at the bottom of the file:

&#91;sourcecode language='css'&#93;
/*S3 add styles for quarter */
.ajax__calendar_quarter {height:67px;width:80px;line-height:40pt;vertical-align:middle;text-align:center;cursor:pointer;overflow:hidden;}
.ajax__calendar .ajax__calendar_quarter {border:1px solid #ffffff;}
.ajax__calendar .ajax__calendar_active .ajax__calendar_quarter {background-color:#edf9ff;border-color:#0066cc;color:#0066cc;}
.ajax__calendar .ajax__calendar_other .ajax__calendar_quarter {background-color:#ffffff;border-color:#ffffff;color:#646464;}
.ajax__calendar .ajax__calendar_hover .ajax__calendar_quarter {background-color:#edf9ff;border-color:#daf2fc;color:#0066cc;}
/*E3*/
&#91;/sourcecode&#93;

And that's it! Add a Calendar Extender to a page, set it's CalendarView to Quarter or QuarterYearDecade, set the Format to output a quarter (Format="Qq" will display Q1 or Q2 or Q3 or Q4.  Format="qq/yyyy" will display a quarter and year.

I added the following CalendarExtenders to the calendar samples page:

&#91;sourcecode language='html'&#93;

<b>Calendar with an associated button with CalendarView="Quarter":</b>
<asp:TextBox runat="server" ID="Date7" />
<asp:ImageButton runat="Server" ID="Image7" ImageUrl="~/images/Calendar_scheduleHS.png" AlternateText="Click to show calendar" />
<ajaxToolkit:CalendarExtender ID="CalendarExtender2" runat="server" TargetControlID="Date7" CalendarView="Quarter" Format="Qq"
    PopupButtonID="Image7" />
<div style="font-size: 90%">
    <em>(Click the image button to show the calendar; this calendar dismisses automatically
        when you choose a date)</em></div>
<b>Calendar with an associated button with CalendarView="QuarterYearDecade":</b>
<asp:TextBox runat="server" ID="Date8" />
<asp:ImageButton runat="Server" ID="Image8" ImageUrl="~/images/Calendar_scheduleHS.png" AlternateText="Click to show calendar" />
<ajaxToolkit:CalendarExtender ID="CalendarExtender3" runat="server" TargetControlID="Date8" CalendarView="QuarterYearDecade" Format="qq/yyyy"
    PopupButtonID="Image8" />
<div style="font-size: 90%">
    <em>(Click the image button to show the calendar; this calendar dismisses automatically
        when you choose a date)</em>;</div>

If you want to check whether your modifications are consistent with my enhancements, or if you went wrong somewhere, download the code from here:

http://cid-5e237543fffb2891.skydrive.live.com/self.aspx/Public/AjaxControlToolkit-DecadeQuarterInitialView.zip

2 Responses to How to patch the Ajax Control Toolkit Calendar Extender control to show a Quarter view

  1. […] How to patch the Ajax Control Toolkit Calendar Extender control to show a Quarter view […]

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: