How to patch the Ajax Control Toolkit Calendar Extender control to show a Day, Month, MonthYearDecade, Quarter, YearDecade and Decade view

April 23, 2009

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, first of 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/

Secondly, I enhanced the calendar extender control so that it showed a quarter, and quarter/year/decade view. That article is available here: https://tonesdotnetblog.wordpress.com/2009/04/17/how-to-patch-the-ajax-control-toolkit-calendar-extender-control-to-show-a-quarter-view/

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, initial view and quarter view source code can be found here: http://cid-5e237543fffb2891.skydrive.live.com/self.aspx/Public/AjaxControlToolkit-DecadeQuarterInitialView.zip This enhancement is an extension of that control, so you’ll need to download that code to start this exercise.

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

What we are trying to achieve:

As long as computers have been around, there have been scenarios where selecting a date, a month, a year or some other timeframe has been necessary.  Zhi-Qiang Ni, from Microsoft, wrote a modification to display a month view of the control, so that a month and year can be selected using the Ajax Control Toolkit. I have implemented Zhi-Qiang’s version, which may be found here: https://tonesdotnetblog.wordpress.com/2009/04/21/how-to-create-a-month-view-with-the-ajax-control-toolkit-calendar-extender-control/

In this situtation, I extend my existing enhancement to select a month, or a month and a year, or a year or even a decade. I have also added a few extra modes, where I hide the title and today bar under certain conditions, for example when you want the user to select a month only.

Selection of a Decade:

Decade View in the Ajax Control Toolkit Calendar Extender control

Decade View in the Ajax Control Toolkit Calendar Extender control

Selection of a Month:

Month Selection

Month Selection

Selection of a Month and Year:

Month Year Selection

Month Year Selection

The code:

Ok, the first thing to do is to modify the already existing CalendarView class, found in the Calendar folder in the Ajax Control Toolkit source code. I added this class in the previous article, which enhanced the Calendar Extender to show a quarter view. I will be using tags S4/E4 for this enhancement. Any other tags are from previous enhancements and should only be changed if it is required by S4.

Original code:


//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

Modified code:


//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.
//S4 - add in Month, MonthYearDecade, YearDecade and Decade views
//        QuarterYearDecade = 2   //shows Quarter Year Decade.
        QuarterYearDecade = 2,    //shows Quarter Year Decade.
        Month = 3,                //shows Month only.
        MonthYearDecade = 4,      //shows Month Year Decade.
        YearDecade = 5,           //shows Year Decade
        Decade = 6                //shows Decade only
//E4
    }
}
//E3

 Next, we need to make the changes to the enumeration within the javascript to support the new values. In the CalendarBehavior.js file, make the following changes to bring the two into line. The code is found at the bottom of the CalendarBehavior.js file.

Original code:

AjaxControlToolkit.CalendarView.prototype = {
    DayMonthYearDecade: 0,
    Quarter: 1,
    QuarterYearDecade: 2
}

Modified code:

AjaxControlToolkit.CalendarView.prototype = {
    DayMonthYearDecade: 0,
    Quarter: 1,
    //S4 add variables to be consistent with server side code
    //    QuarterYearDecade: 2
    QuarterYearDecade: 2,
    Month: 3,
    MonthYearDecade: 4,
    YearDecade: 5,
    Decade: 6
    //E4
}

Within the “show” function, we need to introduce new entry points for these views. Below the case statement within show for switching modes for QuarterYearDecade, add the following lines of code, which handle initial view for each of the types, and also switch to the correct modes based on the selected calendar view.

New code:

//S4 introduce new entry points                      
case AjaxControlToolkit.CalendarView.Month:
    this._switchMode("months", true);
    break;
case AjaxControlToolkit.CalendarView.MonthYearDecade:
    if (this._selectedDate == null) {
        switch (_initialView) {
            //case AjaxControlToolkit.InitialView.Quarter:     
            //    this._switchMode("quarters", true);     
            //    break;     
            case AjaxControlToolkit.InitialView.Year:
                this._switchMode("years", true);
                break;
            case AjaxControlToolkit.InitialView.Decade:
                this._switchMode("decades", true);
                break;
            default:
                this._switchMode("months", true);
                break;
        }
    }
    else {
        this._switchMode("months", true);
    }
    break;
case AjaxControlToolkit.CalendarView.YearDecade:
    if (this._selectedDate == null && _initialView == AjaxControlToolkit.InitialView.Decade) {
        this._switchMode("decades", true);
    }
    else {
        this._switchMode("years", true);
    }
    break;
case AjaxControlToolkit.CalendarView.Decade:
    this._switchMode("decades", true);
    break;
//E4                         

Now we want to hide the title display if the mode is Month. The title allows us to show the year and we don’t want that for month only view.

 Original code:

//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

 Modified code:

//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
//S4 hide for Month and Quarter only selection. Year allows clicking decade view, I haven't created
//a year only view and you'll need to switch between decades
//        if (this._calendarView == AjaxControlToolkit.CalendarView.Quarter)
//            this._header.style.display = 'none';
if ((this._calendarView == AjaxControlToolkit.CalendarView.Quarter)
    || (this._calendarView == AjaxControlToolkit.CalendarView.Month))
    this._header.style.display = 'none';
//E4
//E3

Finally, in the _cell_onclick, modify the code to return the date on cell selection. This needs to happen if the mode is “month” and the CalendarView is set to a month type, the mode is “year” and the CalendarView is a year type, or the mode is “decade” and the CalendarView is a decade type. I refactored this code in Zhi-Qiang’s example, and now I’ve had to refactor it again because of all the extra modes added.

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");

    //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

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");
    var calendarView = this.get_calendarView();
    //S4 introduce new modes selected. Returns when the mode and view matches
    if ((target.mode == "quarter" && (calendarView == AjaxControlToolkit.CalendarView.Quarter
                                   || calendarView == AjaxControlToolkit.CalendarView.QuarterYearDecade))
      || (target.mode == "month" && (calendarView == AjaxControlToolkit.CalendarView.Month
                                  || calendarView == AjaxControlToolkit.CalendarView.MonthYearDecade))
      || (target.mode == "year" && (calendarView == AjaxControlToolkit.CalendarView.YearDecade))
      || (target.mode == "decade" && (calendarView == AjaxControlToolkit.CalendarView.Decade))
      || (target.mode == "today")
      || (target.mode == "day")
       ) {
        this._closeCalendar(target);
        return;
    }
    switch (target.mode) {
        case "month":
            if (target.month != visibleDate.getMonth()) {
                this._visibleDate = target.date;
            }
            this._switchMode("days");
            break;
        case "year":
            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");
            }
            break;
        case "decade":
            if (target.date.getFullYear() != visibleDate.getFullYear()) {
                this._visibleDate = target.date;
            }
            this._switchMode("years");
            break;
        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;
                case "years": this._switchMode("decades"); break;
                case "quarters": this._switchMode("years"); break;
            }
            break;
    }
    //E4
},
//S4 - I introduced the _closeCalendar function to separate out the functionality for when the calendar closes.
//This was beneficial when determining how to refactor the code as I could then see what was common and what was not.
_closeCalendar: function(target) {
    this.set_selectedDate(target.date);
    this._blur.post(true);
    this.raiseDateSelectionChanged();
    return;
},
//E4

And that’s it! With only a handful of additional lines of code, there are now many more views that you can choose from. Note that I have also refactored the dispose code. There was a lot of repetition in the code, so I decided to make a method to do all the repeated work and call that to dispose of each of the views. That now looks like the following:

//E4 – refactor dispose code to remove repetition 
dispose: function() {
    ///

    /// Disposes this behavior’s resources
    ///

    if (this._popupBehavior) {
        this._popupBehavior.dispose();
        this._popupBehavior = null;
    }
    this._modes = null;
    this._modeOrder = null;
    if (this._modeChangeMoveTopOrLeftAnimation) {
        this._modeChangeMoveTopOrLeftAnimation.dispose();
        this._modeChangeMoveTopOrLeftAnimation = null;
    }
    if (this._modeChangeMoveBottomOrRightAnimation) {
        this._modeChangeMoveBottomOrRightAnimation.dispose();
        this._modeChangeMoveBottomOrRightAnimation = null;
    }
    if (this._modeChangeAnimation) {
        this._modeChangeAnimation.dispose();
        this._modeChangeAnimation = null;
    }
    if (this._container) {
        if (this._container.parentNode) { // added this check before calling removeChild WI: 8486
            this._container.parentNode.removeChild(this._container);
        }
        this._container = null;
    }
    if (this._popupDiv) {
        $common.removeHandlers(this._popupDiv, this._popup$delegates);
        this._popupDiv = null;
    }
    if (this._prevArrow) {
        $common.removeHandlers(this._prevArrow, this._cell$delegates);
        this._prevArrow = null;
    }
    if (this._nextArrow) {
        $common.removeHandlers(this._nextArrow, this._cell$delegates);
        this._nextArrow = null;
    }
    if (this._title) {
        $common.removeHandlers(this._title, this._cell$delegates);
        this._title = null;
    }
    if (this._today) {
        $common.removeHandlers(this._today, this._cell$delegates);
        this._today = null;
    }
    if (this._button) {
        $common.removeHandlers(this._button, this._button$delegates);
        this._button = null;
    }

   // disposeView is a new method I created to do all the work of cleaning up each view. Further refactoring of this method
// is possible.
    this.disposeView(this._daysBody);
    this.disposeView(this._monthsBody);
    this.disposeView(this._yearsBody);
    this.disposeView(this._decadesBody);
    this.disposeView(this._quartersBody);
    var elt = this.get_element();
    $common.removeHandlers(elt, this._element$delegates);
    AjaxControlToolkit.CalendarBehavior.callBaseMethod(this, “dispose”);
},
disposeView: function(viewBody) {
    if (viewBody) {
        for (var i = 0; i < viewBody.rows.length; i++) {             var row = viewBody.rows[i];             for (var j = 0; j < row.cells.length; j++) {                 $common.removeHandlers(row.cells[j].firstChild, this._cell$delegates);             }         }         viewBody = null;     } }, //E4 [/sourcecode] If you ran into trouble and need to have a look at the source code, it can be found here: AjaxControlToolkit-Framework3.5SP1withDecadeYearQuarterMonthDayandInitialView.zip

Still to go, I believe I have figured out a way to add date restriction to the component. It might take me a few days to figure this out, but I should be able to do it.

Edit: I just put in a fix to the _isSelected function in the CalendarBehavior.js file. I had misread the original code which allowed the date to return a true in the wrong case. This caused the selected date square to potentially be around more than one date on the calendar view. The zip file above now has the corrected code.


How to add a Year View to the Ajax Control Toolkit Calendar Extender control

April 22, 2009

Further to Zhi-Qiang Ni’s article on showing a Month View in the Calendar Extender control, the next obvious step is to produce a Year View. I have separated out the javascript that was in the CreateDelegate function. I added that as a separate function, called _cell_onclick.

I added a separate calendar extender control to the page, so that the Year view can be demonstrated, and I changed the output date format so that it displays a full year.

I created a javascript method for each of the different start modes. By modifying the start mode of the calendar it displays the different calendar view. I do the same thing in the InitialView property I added in a previous article.

Because only one calendar shows at a time, I will leave the cal variable and create two new variables, monthCalendar and yearCalendar. I’ve also renamed the BehaviorID properties to monthCalendar and yearCalendar. The BehaviorID is what is used to find the calendar control from within client script. $find is then used to find the client side javascript Ajax control object.

I couldn’t help myself, so I’ve refactored the code in the _cell_onclick function.

The result is this:

Year View in the Calendar Extender control

Year View in the Calendar Extender control

<!--Page Language="C#" AutoEventWireup="true" CodeBehind="Sample5.aspx.cs" Inherits="CalendarSample.Sample5"-->

 

<script type="text/javascript"><!--
        var cal;
        var monthCalendar;
        var yearCalendar;

        function _cell_onclick(e) {
            /// <summary>
            /// Handles the click event of a cell
            /// </summary>
            ///
<span  name="e" type="Sys.UI.DomEvent" class="mceItemParam"></span>The arguments for the event</param>
            e.stopPropagation();
            e.preventDefault();
            if (!cal._enabled) return;
            var target = e.target;
            var visibleDate = cal._getEffectiveVisibleDate();
            Sys.UI.DomElement.removeCssClass(target.parentNode, "ajax__calendar_hover");
            //S1
            if ((target.mode == "year" && cal == yearCalendar)
              || (target.mode == "month" && cal == monthCalendar)
              || target.mode == "today")
                closeCalendar(cal, target.date);
            else if ((target.mode == "month" && target.month != visibleDate.getMonth())
                 || (target.mode == "year" && target.year != visibleDate.getYear())) {
                cal._visibleDate = target.date;
            }
            else if (target.mode == "prev" || target.mode == "next") {
                cal._switchMonth(target.date);
            }
            else if (target.mode == "title") {
                switch (cal._mode) {
                    case "days": cal._switchMode("months"); break;
                    case "months": cal._switchMode("years"); break;
                }
            }
            //            switch (target.mode) {
            //                case "prev":
            //                case "next":
            //                    cal._switchMonth(target.date);
            //                    break;
            //                case "title":
            //                    switch (cal._mode) {
            //                        case "days": cal._switchMode("months"); break;
            //                        case "months": cal._switchMode("years"); break;
            //                    }
            //                    break;
            //                case "month":
            //                    //if the mode is month, then stop switching to day mode.
            //                    if (target.month != visibleDate.getMonth()) {
            //                        cal._visibleDate = target.date;
            //                    }
            //                    //this._switchMode("days");
            //                    break;
            //                case "year":
            //                    if (target.date.getFullYear() != visibleDate.getFullYear()) {
            //                        cal._visibleDate = target.date;
            //                    }
            //                    break;
            //                //                case "day":
            //                //                    this.set_selectedDate(target.date);
            //                //                    this._switchMonth(target.date);
            //                //                    this._blur.post(true);
            //                //                    this.raiseDateSelectionChanged();
            //                //                    break;
            //                case "today":
            //                    closeCalendar(cal, target.date);
            //                    break;
            //            }
            //E1
        }

        function pageLoad() {
            monthCalendar = $find("monthCalendar");
            yearCalendar = $find("yearCalendar");
            //we need to modify the original delegate of the month cell.
            monthCalendar._cell$delegates = {
                mouseover: Function.createDelegate(monthCalendar, monthCalendar._cell_onmouseover),
                mouseout: Function.createDelegate(monthCalendar, monthCalendar._cell_onmouseout),
                click: Function.createDelegate(monthCalendar, _cell_onclick)
            }
            yearCalendar._cell$delegates = {
                mouseover: Function.createDelegate(yearCalendar, yearCalendar._cell_onmouseover),
                mouseout: Function.createDelegate(yearCalendar, yearCalendar._cell_onmouseout),
                click: Function.createDelegate(yearCalendar, _cell_onclick)
            }
        }

        function closeCalendar(cal, targetDate) {
            cal.set_selectedDate(targetDate);
            cal._switchMonth(targetDate);
            cal._blur.post(true);
            cal.raiseDateSelectionChanged();
        }

        function onCalendarMonth(sender, args) {
            //set the default mode to month
            cal = monthCalendar;
            sender._switchMode("months", true);
        }

        function onCalendarYear(sender, args) {
            cal = yearCalendar;
            sender._switchMode("years", true);
        }

// --></script>

 

<form id="form1" enctype="application/x-www-form-urlencoded">
<div>The Month:</div>
<div>The Year:</div>
</form>

Now I will move this code back into my already enhanced Ajax Control Toolkit calendar control. The code will take me about 20 minutes to do, but the time to write the article takes a lot longer (seems to be a ratio of about 1 to 4 at the moment). I’ll do that next week.

Code for this is at http://cid-5e237543fffb2891.skydrive.live.com/self.aspx/Public/CalendarSampleWithYearView.zip


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

April 17, 2009

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


How to patch the Ajax Control Toolkit CalendarExtender to add Decade support and InitialView – Part 4

April 15, 2009

Missed the rest of the article? Click here for Part 1

Ok, so now we have a decade view. If you keep clicking on the title of the calendar, you’ll eventually get to decade view. Hopefully it works as good for you as it does for me.

Next step is to create an InitialView so that you can set the calendar to show the Decade when you first click the calendar image.

To add any class that you want to have access on both the server and the client, you need to add a class file to the Calendar folder. I simply right-clicked on the CalendarPosition.cs file, then clicked Copy. Then I Pasted the file into the Calendar folder, and renamed it “InitialView”.

Initial View cs file

Initial View cs file

Now we need an enumeration so that we can set the view.  Change the code in the file so that the class name is InitialView, and it contains Day, Month, Year and Decade, as follows:

//S2 - Add Initial View so that you can choose a starting view for your date selection. You might want to do this
//so that selection of the date is more efficient.</pre>
namespace AjaxControlToolkit
{
    /// <summary>
    /// The initial view of the calendar
    /// </summary>
    public enum InitialView
    {
        Day = 0,
        Month = 1,
        Year = 2,
        Decade = 3
    }
}
//E2

Again, we are really just copying the CalendarPosition enumeration. To get this to be recognised in javascript, copy the three code blocks that make up the client side javascript block for CalendarPosition, then modify the code to be consistent with the InitialView that we are trying to achieve.

Original code:

AjaxControlToolkit.CalendarPosition = function() {
    /// <summary>
    /// Position of the popup relative to the target control
    /// </summary>
    /// <field name="BottomLeft" type="Number" integer="true" />
    /// <field name="BottomRight" type="Number" integer="true" />
    /// <field name="TopLeft" type="Number" integer="true" />
    /// <field name="TopRight" type="Number" integer="true" />
    /// <field name="Right" type="Number" integer="true" />
    /// <field name="Left" type="Number" integer="true" />
    throw Error.invalidOperation();
}
AjaxControlToolkit.CalendarPosition.prototype = {
    BottomLeft: 0,
    BottomRight: 1,
    TopLeft: 2,
    TopRight: 3,
    Right: 4,
    Left: 5
}
AjaxControlToolkit.CalendarPosition.registerEnum('AjaxControlToolkit.CalendarPosition');

 Additional code, add at the bottom of the CalendarBehavior.js file just below the code for registering the CalendarPosition enum:

//S2 Initial View. This is the code that synchronises the server side enum with the client side enum
AjaxControlToolkit.InitialView = function() {
    /// <summary>
    /// View of the calendar opened when the user selects the calendar
    /// </summary>
    /// <field name="Day" type="Number" integer="true" />
    /// <field name="Month" type="Number" integer="true" />
    /// <field name="Year" type="Number" integer="true" />
    /// <field name="Decade" type="Number" integer="true" />
    throw Error.invalidOperation();
}
AjaxControlToolkit.InitialView.prototype = {
    Day: 0,
    Month: 1,
    Year: 2,
    Decade: 3
}
AjaxControlToolkit.InitialView.registerEnum('AjaxControlToolkit.InitialView');
//E2

Still in the CalendarBehavior.js file, scroll to the top. We need to declare the _initialView variable and set it’s initial default value. Find the code that initialises the popup position. This is how the default Calendar Position is set. It is also the variable that we are copying.

Original code:

this._popupPosition = AjaxControlToolkit.CalendarPosition.BottomLeft;

Modified code:

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

To be consistent with the other client script variables, we create getters and setters. Simply copy the ones for popupPosition:

Original code:

 get_popupPosition: function() {
   /// <value type="AjaxControlToolkit.CalendarPosition">
   /// Where the popup should be positioned relative to the target control.
   /// Can be BottomLeft (Default), BottomRight, TopLeft, TopRight.
   /// </value>
   return this._popupPosition;
},
set_popupPosition: function(value) {
  if (this._popupPosition != value) {
    this._popupPosition = value;
    this.raisePropertyChanged('popupPosition');
  }
},

 

Modified code:

 get_popupPosition: function() {
   /// <value type="AjaxControlToolkit.CalendarPosition">
   /// Where the popup should be positioned relative to the target control.
   /// Can be BottomLeft (Default), BottomRight, TopLeft, TopRight.
   /// </value>
   return this._popupPosition;
},
set_popupPosition: function(value) {
  if (this._popupPosition != value) {
    this._popupPosition = value;
    this.raisePropertyChanged('popupPosition');
  }
},
//S2 - set and get variables for javascript initialView, copied off first day of week
get_initialView: function() {
  /// <value type="AjaxControlToolkit.InitialView">
  /// The initial view of the calendar, day, month, year or decade
  /// </value>
  return this._initialView;
},
set_initialView: function(value) {
  if (this._initialView != value) {
     this._initialView = value;
     this.invalidate();
     this.raisePropertyChanged("initialView");
  }
},
//E2

The final bit of javascript code is where we switch between modes of the calendar. This occurs in the show function. Basically, when the calendar needs to show, it calls the method this._switchMonth(null, true); so that the correct month is selected in the calendar. I think we should leave this to do it’s work. Instead, I will use the internal function _switchMode to switch between the different calendar views.

Original code:

show: function() {
  /// <summary>
  /// Shows the calendar
  /// </summary>
 

  this._ensureCalendar();

  if (!this._isOpen) {

     var eventArgs = new Sys.CancelEventArgs();
     this.raiseShowing(eventArgs);
     if (eventArgs.get_cancel()) {
        return;
     }

     this._isOpen = true;
     this._switchMonth(null, true);
     this._popupBehavior.show();
     this.raiseShown();
   }
},

Modified code:

show: function() {
  /// <summary>
  /// Shows the calendar
  /// </summary>
 

  this._ensureCalendar();

  if (!this._isOpen) {

     var eventArgs = new Sys.CancelEventArgs();
     this.raiseShowing(eventArgs);
     if (eventArgs.get_cancel()) {
        return;
     }

     this._isOpen = true;
     this._switchMonth(null, true);
     //S2 - We've let the code create the calendar, now switch the calendar to the initial view that we want.
     //Note that we only want this to happen if a date has not already been selected, as they may want to
     //change only some of the date
     if (this._selectedDate == null) {
        var _initialView = this.get_initialView();
        switch (_initialView) {
           case AjaxControlToolkit.InitialView.Day:
              this._switchMode("days");
              break;
           case AjaxControlToolkit.InitialView.Month:
              this._switchMode("months");
              break;
           case AjaxControlToolkit.InitialView.Year:
              this._switchMode("years");
              break;
           case AjaxControlToolkit.InitialView.Decade:
              this._switchMode("decades");
              break;
         }
     }
     //E2           
     this._popupBehavior.show();
     this.raiseShown();
   }
},

Now the last thing we need to do is to ensure that there is a method of setting the InitialView on the calendar extender. This is done in the CalendarExtender.cs file.

Scroll down to the code that handles to PopupPosition property. Copy and Paste that, and rename CalendarPosition to InitialView, popupPosition to initialView (case sensitive) and set the default value to InitialView.Day. It should now look like the following:

 //S2 - This is how to get a variable from the server to the client.
[DefaultValue(InitialView.Day)]
[ExtenderControlProperty]
[ClientPropertyName("initialView")]
public virtual InitialView InitialView
{
  get { return GetPropertyValue("InitialView", InitialView.Day); }
  set { SetPropertyValue("InitialView", value); }
}
//E2

Add a text box to a page, attach a calendar extender, then add the attribute InitialView=”Decade” and when you click on the calendar, it will open in decade view. But it will only do this if there isn’t a date already selected. If there is a date already in the text box, then the calendar will open in Day view. This is by design.

Remember to update the assembly version. This is a custom variation to the AjaxControlToolkit, so we don’t want it pretending it’s the original. Under the properties folder in the AjaxControlToolkit project, double-click on the AssemblyInfo.cs file. Change the AssemblyVersion and AssemblyFileVersion attributes. I changed mine as follows:

[assembly: AssemblyVersion("3.0.20822.*")]
[assembly: AssemblyFileVersion("3.0.20822.0")]

And that’s pretty much it!

Got messed up somewhere along the line? Here’s the source code complete with patch so you can see where you went wrong. http://cid-5e237543fffb2891.skydrive.live.com/self.aspx/Public/DecadeView.zip


How to patch the Ajax Control Toolkit CalendarExtender to add Decade support and InitialView – Part 3

April 15, 2009

Now for the code. The vast majority of the code for the Calendar Extender control resides in the CalendarBehavior.js file. This is found in the AjaxControlToolkit source code under the Calendar folder.

To add support for decade, we need to copy the year code and modify it to look like a decade view. So the first thing to do is to find the variables that support year view and copy them.

Original code:

this._years = null;
this._yearsTable = null;
this._yearsBody = null;

Modified code:

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

Note that I have tagged my code. If I modify code, I start the tag with an S. Where I finish the modification, I finish the tag with an E. In this case, it’s the first modification, so I have tagged it with S1 and E1.

Next, we need to add the modes to the calendar extender control. Go to the lines of code that show the modes.

Original code:

this._modes = {"days" : null, "months" : null, "years" : null};
this._modeOrder = {"days" : 0, "months" : 1, "years" : 2 };

Modified 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

Note that I have commented out the original code, and added the new code. The new lines show “decades” and put the decades in correct mode order. Not too sure here, but I believe this is needed for animations. Note that commenting out the old code helps me in case I make a mistake and need to go back.  They can always be cleaned up later!

When the calendar is created, it creates the entire content of the calendar in a method called _buildBody. We need to add a method to the calendar extender to support the building of the Decade view. And we do this just below the line that builds the year view.

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();
},

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       
},

Next we need to add in the function that actually builds the decades. To do this,  I have copied the method _buildYear and renamed it “_buildDecades”, then I have renamed all the references from “year” to “decade”. The display of a year is basically a table of cells. So I reuse this mechanism to build a table of decade cells. But decades are not the same as years – they take up more space than years. 2000-2009 is not the same as 2000. So after a fair bit of mucking around, I settled on a 3 by 3 grid of decades. Here’s the code:

New code :

_buildDecades: function() {
   /// <summary>
   /// Builds a "decades" view for the calendar
   /// </summary>

   var id = this.get_id();

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

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

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

   // this is the bit of code that determines the rows (i) and cells (j) that the decades go into.
   // so there are 9 cells.
   for (var i = 0; i < 3; i++) {
      var decadesRow = $common.createElementFromTemplate({ nodeName: "tr" }, this._decadesBody);
      for (var j = 0; j < 3; j++) {
         var decadeCell = $common.createElementFromTemplate({ nodeName: "td" }, decadesRow);
         var decadeDiv = $common.createElementFromTemplate({
            nodeName: "div",
            properties: {
               id: id + "_decade_" + i + "_" + j,
               mode: "decade",
               decade: ((i * 3) + j)
            },
            events: this._cell$delegates,
               cssClasses: &#91;"ajax__calendar_decade"&#93;
         }, decadeCell);
      }
   }
},
&#91;/sourcecode&#93;

Lesser known fact: you can actually add custom attributes to just about any tag. In this case, the tag inside the cell (td) is a div. Using Ajax methods, I simply add an id, mode and decade property. The decade property is used to determine the decade number (0 to 9) that will be used to calculate the decade to be displayed. The current decades to show is actually generated when you click on the Year Title bar. That bit of code is actually required in the _performLayout function. Before we move on, note that I have renamed the css classes to support decades. I will show you the css class mods later. For now, we'll move onto the _performLayout method.

Internally, the calendar extender calls the different views "modes". In the _performLayout function, it selects a mode based on the current visible date. Inside this function, there is a case statement. It is used to render the calendar modes.

Original (pseudo) 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>

//code to initialise the layout and get the current view

switch (this._mode) {
   case "days":
   //code to generate day view
   break;
   case "months":
   //code to generate month view

   break;
   case "years":
   //code to generate year view
   break;
}

//code to set the today bar

}

You’ll need to add a case statement for handling the decade view, as follows:

 Modified code:

 
//S1 – This code generates the decade values for the currently visible decade                         
   case “decades”:
   // the following line is where it rounds down to the start of the current decade
   var minDecade = (Math.floor(visibleDate.getFullYear() / 10 ) * 10 );
   for (var i = 0; i < this._decadesBody.rows.length; i++) {       var row = this._decadesBody.rows[i];       for (var j = 0; j < row.cells.length; j++) {          var cell = row.cells[j].firstChild;   // the following line calculates the decade for the 8 decades prior to the current decade (9 total)          cell.date = new Date(minDecade + ((cell.decade - 8 ) * 10 ), 0, 1, this._hourOffsetForDst);          if (cell.firstChild) {             cell.removeChild(cell.lastChild);          } else {             cell.appendChild(document.createElement("br"));          }          //the following line generates the decade text to be displayed in each cell.          var decadeString = (minDecade + ((cell.decade - 8 ) * 10 )).toString() + "-" + (minDecade + ((cell.decade - 8 ) * 10 ) + 9 ).toString();          cell.appendChild(document.createTextNode(decadeString));          $common.removeCssClasses(cell.parentNode, ["ajax__calendar_other", "ajax__calendar_active"]);          Sys.UI.DomElement.addCssClass(cell.parentNode, this._getCssClass(cell.date, 'y'));       }    }    if (this._title.firstChild) {       this._title.removeChild(this._title.firstChild);    }    //the following sets the text for the title bar. Note that we are at the top level, so clicking on this bar does nothing    this._title.appendChild(document.createTextNode(( minDecade - 80 ).toString() + "-" + ( minDecade + 9 ).toString()));    this._title.date = visibleDate;    //I have enabled the previous and next arrows. They skip 90 years each way - the start of the next batch of decades    this._prevArrow.date = new Date(minDecade - 90, 0, 1, this._hourOffsetForDst);    this._nextArrow.date = new Date(minDecade + 90, 0, 1, this._hourOffsetForDst);    break; //E1 [/sourcecode] Note comments inline. The next bit of code occurs when you click on a cell. When clicking on the cell, it changes the mode to a different mode (if it's year or month) or if it's the day view, it sets the date, closes the calendar and  inserts the formatted date into the text box. Go to the _cell_onclick function, find the following code: [sourcecode language='javascript']  case "title":   switch (this._mode) {      case "days": this._switchMode("months"); break;      case "months": this._switchMode("years"); break;   }   break; [/sourcecode]  We add in the new mode: Modified code: [sourcecode language='javascript']  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; [/sourcecode] All internal methods, of course, nothing special here. Now, in the same function, _cell_onclick, find the case "year" code block. I copied this block and made modifications to support decade. Original code: [sourcecode language='javascript']  case "year":   if (target.date.getFullYear() != visibleDate.getFullYear()) {      this._visibleDate = target.date;   }   this._switchMode("months");   break; [/sourcecode] Modified code: [sourcecode language='javascript']  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                        [/sourcecode] Note how similar the year switching code is to the decade switching code . Final code changes are to clean up the generated decade table, cells and divs. Go to the "dispose" function and copy the code that cleans up the year code. Rename all references from "year" to "decade". Original code: [sourcecode language='javascript']  if (this._yearsBody) {   for (var i = 0; i < this._yearsBody.rows.length; i++) {      var row = this._yearsBody.rows[i];      for (var j = 0; j < row.cells.length; j++) {         $common.removeHandlers(row.cells[j].firstChild, this._cell$delegates);      }   }   this._yearsBody = null; } [/sourcecode] Modified code: [sourcecode language='javascript']  if (this._yearsBody) {   for (var i = 0; i < this._yearsBody.rows.length; i++) {      var row = this._yearsBody.rows[i];      for (var j = 0; j < row.cells.length; j++) {         $common.removeHandlers(row.cells[j].firstChild, this._cell$delegates);      }   }   this._yearsBody = null; } //S1 - Clean up decade variables          if (this._decadesBody) {   for (var i = 0; i < this._decadesBody.rows.length; i++) {     var row = this._decadesBody.rows[i];     for (var j = 0; j < row.cells.length; j++) {       $common.removeHandlers(row.cells[j].firstChild, this._cell$delegates);     }   }   this._decadesBody = null; } //E1        [/sourcecode] Note how similar the year dispose code is to the decade dispose code. Finally, you need to add css styles to the calendar to support decades. This is found in the Calendar.css file. After a little bit of tinkering with the styles using IE Dev Toolbar, I settled on the following additional styles, which you can simply add to the bottom of the file. [sourcecode language='css']  /*S1*/ .ajax__calendar_decade {height:44px;width:55px;text-align:center;cursor:pointer;overflow:hidden;} .ajax__calendar .ajax__calendar_decade {border:1px solid #ffffff;} .ajax__calendar .ajax__calendar_active .ajax__calendar_decade {background-color:#edf9ff;border-color:#0066cc;color:#0066cc;} .ajax__calendar .ajax__calendar_other .ajax__calendar_decade {background-color:#ffffff;border-color:#ffffff;color:#646464;} .ajax__calendar .ajax__calendar_hover .ajax__calendar_decade {background-color:#edf9ff;border-color:#daf2fc;color:#0066cc;} /*E1*/ [/sourcecode] And that's it, you now have a decade view! Click for Part 4 >>


How to patch the Ajax Control Toolkit CalendarExtender to add Decade support and InitialView – Part 2

April 15, 2009

In my previous article, I explain a problem with the current Ajax Control Toolkit Calendar Extender control when selecting birthdates – it simply takes too many clicks of the mouse. In this article, I explain my solution to this problem.

Firstly, I introduce the decade view. This allows the user to select from one of 9 decades displayed, as follows:

Calendar Extender Decade View

Calendar Extender Decade View

To get here, you simply click on the Title bar in the Year view:

Year View title click

Year View title click

Ok, so that allows you to remove 3 clicks, but it adds 1 click. But what if you were able to open the calendar in decade view?

Then you would simply select the decade,

Calendar Extender Decade View

Calendar Extender Decade View

then the Year,

Calendar Extender 70s View

Calendar Extender 70s View

then the Month

Calendar Extender 1970 Month View

Calendar Extender 1970 Month View

and finally the Day.

 

Calendar Extender May 1970 Day View

Calendar Extender May 1970 Day View

Bingo, 4 clicks!

Click for Part 3 >>


How to patch the Ajax Control Toolkit CalendarExtender to add Decade support and InitialView – Part 1

April 15, 2009

This is an educational article intended to show how to enhance the publicly available Ajax Control Toolkit Calendar Extender control.  All code here is added to a fresh download of the Ajax Control Toolkit. All code here is provided under the Microsoft Public Licence.

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

The licence is available here: http://ajaxcontroltoolkit.codeplex.com/license

In this case, I am intending to improve the usability of birthdate selection.

Currently, when the user wants to enter a birthdate into the text box attached to the Calendar Extender control, they click in the text box, or click on the calendar image next to the text box. The calendar extender control then pops up a Day view, as follows:

Calendar Extender Day View

Calendar Extender Day View

But we want a birthdate, so you don’t want to select a day here as it will potentially be from the wrong month or year, so what you do to select a different month is to click on the Title bar. That’s the bit that shows “April, 2009”. The calender extender control then pops up the Month view, as follows:

Calendar Extender Month View

Calendar Extender Month View

Next, you want the year, so you click on the Title bar again. That now shows “2009”. The calendar extender control then pops up the Year view, as follows:

Calendar Extender Year View

Calendar Extender Year View

Ok, so say we want 26 May 1970. We now need to choose a different decade. But clicking on the title doesn’t allow you to select a new decade. Instead, you need to use the arrows to go forward or backwards between decades. So you would click the left arrow in the top left corner 3 times to get to the decade that you want, the 1970s, as follows:

Calendar Extender 1970-1979 year view

Calendar Extender 1970-1979 year view

Then you need to click on the year you want, 1970. On selection, it switches back to the month view. You click the month that you want, May, then it switches to the Day view. You can then select the day that you want, 26. On selecting this, the calendar closes, and the text box is populated with 26/05/1970 (or whatever your date format is for the selected region and setting in the calendar extender). That’s a lot of work to select a birthdate.

That’s a total of 9 mouse clicks. Using this innovation, I have managed to get this down to 4 mouse clicks.

Click here for Part 2 >>