Refactoring MVP and custom javascript date controls

by MikeHogg22. September 2014 17:11

I was tasked to fix a custom UI date control in a legacy .Net web application. It was a requirement that each of the date components- day, month, year- had to be in separate dropdownlists. I recommended going with one of the standard date controls that exist in any of the .net or js libraries today (and have been extensively tested - hundred of hours in some cases) but there was a reason for going with a custom control. It was originally written years ago, and I ended up entirely rewriting it because of some design conflicts. 

This was a legacy maintenance project, where only emergency fixes are allowed, and no upgrades or refactoring time is available. Like most of these projects, I wanted to keep as close to the existing tools/libraries/patterns as I could, and write as little as possible custom code; I wanted the fewest changes.  So...

The site was using the old Model View Presenter Pattern (pre MVC), and there were runtime properties of the control, that were not available to the controller during design time.  My controller was going to need these properties, so I followed the existing pattern and set up an interface in the library and exposed my control properties to the controller through this library interface, so we could process these design time variables in our controller and wouldn’t get any surprise runtime exceptions.

The c# code was keeping track of each of the dropdownlists manually, so there were Day Month Year properties to select and clear, and the lists had to be populated depending on currently selected values, and selected values had to be selected or cleared at the right parts of the page lifecycle if we were posting back, and at other parts if we weren't, and gotten from Request values when they weren't available yet in the page lifecycle.  It was all very complicated and manual. I replaced all that with databinding for populating the lists, and just two properties- one SelectedDate property (get only) and one PreselectedDate property(set only) that were databound as well.

It was also using a lot of javascript, and a lot of it was the same as the c# code, populating the lists in the dropdowns again, but this time in the client, based on whether there were 30 or 31 or 29 days in that month and so on when the user changed them... The problem with this was the list counts of the control that was posted back would not match the list counts of the control in viewstate, and .net would barf on

“control tree not matching viewstate(Invalid Postback or callback argument. EventValidation is enabled using enableEventValidation="true") “

and you don't want to turn EventValidation off, or what's the whole point of using .net and viewstate?  Instead, what I do here, is add an AjaxPanel, or a Telerik ajax panel, since Telerik is what they were using in this case, and then all I had to do was call postback onchange of the dropdownlists, and use the same c# databinding code I already wrote.  Removed all the duplicate js code and no more EventValidation exceptions.

Above this, I was supposed to add validation.  (client side only – those were the requirements)  Which is the cool part that I'd like to share.  I could have done this a couple different ways but ended up writing my own short validation function in lieu of some other existing options that would have added much more (still custom) complexity.

Javascript dates are notoriously difficult to work with.  They have carryover parsing, and the zero index months.  Javascript can take a date(2011,12,1) and parse it to be 2012/1/1.  But if I just compare my js date fields after parsing, to my input fields, I could make sure the date is not only valid, but the expected date entered.

 

 

function validateDddp() {
            $('.txtDobValid:first').val('');
 
 var day = $('.dateList')[0].value;// using asp controls so Id can't be used, using class instead
 var month = $('.dateList')[1].selectedIndex -1;//js: zero based months
 var year = $('.dateList')[2].value;
 
 var adate = new Date(year, month, day);
 if (isValidDateDddp(adate) 
                && adate.getDate() == day 
                && adate.getMonth() == month
                && adate.getYear() == year - 1900) {
                    $('.txtDobValid:first').val('valid');
            }
 
            $('.txtDobValid:first').change();
        }
 
function isValidDateDddp(d) {
 if (Object.prototype.toString.call(d) !== "[object Date]")
 return false;
 return !isNaN(d.getTime());
        }
 
// for page loads where field is already populated 
        $(document).ready(function() {
if ($.grep($('.dateList'), function(e) { return e.selectedIndex != 0; }).length > 0) {
                validateDddp();
            }
        }
        );

 

For kicks here is the web View custom user control code-behind that handles all the binding/page load scenarios, and now also the ajax posting. Pretty basic.

[System.Web.UI.ValidationProperty("SelectedDate")]
public partial class DropDownDatePicker : Microsoft.Practices.CompositeWeb.Web.UI.UserControl, IDropDownDatePickerWebPartView
{
private DropDownDatePickerWebPartPresenter _presenter;
private DateTime dateOfBirth;
public DateTime? SelectedDate
    {
        get
        {
            DateTime result;
 if (DateTime.TryParse(String.Format("{0}/{1}/{2}", this.ddlYear.SelectedValue, this.ddlMonth.SelectedIndex, this.ddlDay.SelectedValue), out result)) return result;
 else return null;
        }
    }
public DateTime DateOfBirth { set { dateOfBirth = value; } } 
public DateTime MinDate { get; set; }
public DateTime MaxDate { get; set; }
public bool ValidationEnabled { get; set; }
 
 
    [Dependency]
public DropDownDatePickerWebPartPresenter Presenter
    {
        get
        {
 return this._presenter;
        }
        set
        {
 if (value == null)
 throw new ArgumentNullException("value");
 
 this._presenter = value;
 this._presenter.View = this;
        }
    }
 
 
protected void Page_Load(object sender, EventArgs e)
    {
if (MaxDate == DateTime.MinValue)
        {
            MinDate = DateTime.Now.AddYears(-100);
            MaxDate = DateTime.Now;
        }
 
if (!this.IsPostBack)
        {
 this._presenter.OnViewInitialized();
 
            rfvDateOfBirth.Enabled = ValidationEnabled;
 
            BindLists();
 
            BindSelections();
        }
 
this._presenter.OnViewLoaded();
 
    }
 
 
#region Binds
private void BindLists()
    {
        ddlDay.DataSource = GetDays(dateOfBirth.Year, dateOfBirth.Month);
        ddlDay.DataBind();
 
        ddlMonth.DataSource = GetMonths();
        ddlMonth.DataBind();
 
        ddlYear.DataSource = GetYears();
        ddlYear.DataBind();
    }
 
private void RebindDays()
    {
int year;
        var month = ddlMonth.SelectedIndex;
        var currentday = ddlDay.SelectedValue;
 
if (ddlYear.SelectedIndex == 0) year = DateTime.Now.Year;
else int.TryParse(ddlYear.SelectedValue, out year);
 
if (year <= MaxDate.Year
            && year >= MinDate.Year
            && month > 0
            && month <= 12)
        {
            ddlDay.DataSource = GetDays(year, month);
            ddlDay.DataBind();
 if (ddlDay.Items.FindByValue(currentday) != null) ddlDay.SelectedValue = currentday;
        }
    }
private void BindSelections()
    {
if (dateOfBirth != DateTime.MinValue)
        {
 this.ddlYear.SelectedValue = dateOfBirth.Year.ToString();
 this.ddlMonth.SelectedIndex = dateOfBirth.Month;
 this.ddlDay.SelectedValue = dateOfBirth.Day.ToString();
        }
    }
 
#endregion
 
 
#region Get Lists
private List<string> GetDays(int year, int month)
    {
        var days = new List<string>();
        days.Add("-Day-"); 
while (days.Count <= DateTime.DaysInMonth(year, month)) days.Add(days.Count.ToString());
return days;
    }
 
/// <summary>
/// We'll can still use Months as List<string> if we use Index for the Int when creating DateTime objects
/// </summary>
/// <returns></returns>
private List<string> GetMonths()
    {
        var months = new List<string>();
        months.Add("-Month-");
while (months.Count <= 12) months.Add(new DateTime(2001, months.Count, 1).ToString("MMMM"));
return months;
    }
 
private List<string> GetYears()
    {
        var years = new List<string>();
        years.Add("-Year-");
int thisyear = MaxDate.Year;
int floor = MinDate.Year;
while (thisyear > floor) years.Add(thisyear--.ToString());
return years;
    }
 
#endregion
 
 
#region Events
protected void ddl_SelectedIndexChanged(object sender, EventArgs e)
    {
        RebindDays();
        Validate();
 
    }
 
#endregion
 
#region Validation
 
private void Validate()
    {
if (ValidationEnabled)
        {
 try
            {
 int year = Convert.ToInt16(this.ddlYear.SelectedValue);
 int month = Convert.ToInt16(this.ddlMonth.SelectedIndex);
 int day = Convert.ToInt16(this.ddlDay.SelectedValue);
 
                DateTime d = new DateTime(year, month, day);
 if (d > MaxDate || d < MinDate)
                {
 throw new Exception(String.Format("Server Side DropDownDatePicker.Validate: Date {0} not in valid range {1} - {2}",
                        d.ToShortDateString(), MinDate.ToShortDateString(), MaxDate.ToShortDateString())); 
                }
            }
 catch (Exception e)
            {
 //log... 
                txtDobValid.Text = String.Empty;
                rfvDateOfBirth.IsValid = false;
 // leave parent to validate page
            }
        }
    }
 
#endregion
 
 
}
}

About Mike Hogg

Mike Hogg is a c# developer in Brooklyn.

More Here

Favorite Books

This book had the most influence on my coding style. It drastically changed the way I write code and turned me on to test driven development even if I don't always use it. It made me write clearer, functional-style code using more principles such as DRY, encapsulation, single responsibility, and more.amazon.com

This book opened my eyes to a methodical and systematic approach to upgrading legacy codebases step by step. Incrementally transforming code blocks into testable code before making improvements. amazon.com

More Here