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 create a Month View with the Ajax Control Toolkit Calendar Extender Control

April 21, 2009

I read a forum entry from Microsoft’s own Zhi-Qiang Ni on how to make a Month view calender extender. I have implemented his version, which shows a From and To date version.

Here’s what Zhi-Qiang Ni produced:

Ajax Control Toolkit Month Calendar

Ajax Control Toolkit Month Calendar

You can see the original forum article here:

http://forums.asp.net/t/1349086.aspx 

Showing Month/Year view is a common requirement, and I intend to evolve this version into something more generic.  Note that this version does not require patching the Ajax Control Toolkit – it is a stand alone javascript version that redirects the delegates to call local javascript functions.

The benefit here is that when the Ajax Control Toolkit changes, you won’t need to make modifications to the Ajax Control Toolkit source code to continue with these changes.

Zhi-Qiang Ni produced another version, here:

http://forums.asp.net/t/1342938.aspx

Month View version 2

Month View version 2

The code he has put in the click delegate is taken straight out of the calendar extender CalendarBehavior.js file _cell_onclick method and modified.

In the file I have created a 3rd sample. In that one, I have taken the above example and removed the Day/Year part of it, so that it shows month only. I have also removed the adding and removing of delegates from the onCalendarShown1 javascript function as I don’t think it’s necessary – there are always 12 months, the delegates are common and always containing the same data.

In the final sample, I take the calendar image from the Calendar Extender samples site and add it to the end of the extender, making it a Calendar Extender popup image.

So now the example looks like this:

Month View from Sample 4

Month View from Sample 4

You can download Zhi-Qiang Ni’s version implemented here with some minor tweaking and extra sample pages here:

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

Thanks go to Microsoft’s Zhi-Qiang Ni for this version!


Patching the Ajax Control Toolkit Calendar Extender control to show a Month or Year

April 2, 2009

Edit: I have implemented Zhi-Qiang’s version of the Month calendar view and made some minor adjustments. You can find it here: https://tonesdotnetblog.wordpress.com/2009/04/21/how-to-create-a-month-view-with-the-ajax-control-toolkit-calendar-extender-control/

Edit2: I have just finished recreating the Ajax Control Toolkit Calendar Extender modifications from scratch, without looking at my previous code or article. That article may be found here: https://tonesdotnetblog.wordpress.com/2009/04/23/how-to-patch-the-ajax-control-toolkit-calendar-extender-control-to-show-a-month-monthyeardecade-yeardecade-and-decade-view/

I used to have code on my website that allowed you to modify the Ajax Control Toolkit to show a Month or a Year in the calendar extender control. A third party has now made claim to the code, so I have removed it as a precaution. If you have downloaded that modification, please destroy the code. Do not use that code and do not publish it elsewhere. 

This idea was first registered on CodePlex on February 15th 2007, well before the time I worked for the third party company and probably before they even knew about the Ajax Control Toolkit. If you want to provide a new patch for this on CodePlex, make sure you get a waiver from your company, otherwise things could become complicated when someone thinks they’ve got something unique, even though it’s obvious to the rest of us. But you must not use my code and you must not use my article.

And finally some links that provide a few hints to do with the Ajax Control Toolkit. They use standard internal methods inside the Ajax Control Toolkit Calendar Extender control to achieve this.

An alternative solution to selecting either Month or Year is found here and looks nothing like my code. It’s good to see Microsoft support coming up with these great solutions. All the calls are standard Ajax Control Toolkit Calendar control methods.

http://forums.asp.net/t/1349086.aspx

Adding a close button to the Calendar Extender control, and date restriction:

http://www.karpach.com/ajaxtoolkit-calendar-extender-tweaks.htm

So it’s good to see you all would have found a solution anyway if I hadn’t have posted my code – and there are a stack of other links, you just need to look for them!

If you just want to change the calendar mode displayed on selecting the calendar, but still select a day, this can be done by looking here:

http://scottrk.wordpress.com/2007/10/29/changing-the-aspnet-ajax-control-toolkit-calendar-display-mode/

And finally a jQuery calendar control on Rick Strahl’s blog. It’s just a normal calendar control, but the javascript is jQuery, which is cool. I hope the Ajax Control Toolkit inherits some of this.

http://www.west-wind.com/weblog/posts/167052.aspx