Long Cloud Technologies
"... A Yankee in the Land of the Long White Cloud, Aotearoa ..."

Updating Scott Hanselman’s DateTime Custom Model Binder (DateAndTimeModelBinder)

So new client asks me to help with some of the New ASP.NET MVC 2 development of a new site.  Very cool stuff and having a good time (pioneering spirit, arrows in the back and all).  Anyway it comes up that the model will have some DateTime fields, where we need to actually record time.  Date is easy, we go with the Telerik Asp.net MVC date picker (open-source free for non-commercial usage), but time…

Not to worry cause I remembered that Scott Hanselman had done a Custom Model Binder: DateAndTimeModelBinder for asp.net mvc here.  So I go and download it.  First up, things have changed a little since he first wrote it, for example TryGetValue

bindingContext.ValueProvider.TryGetValue

Is no longer valid on the ValueProvider.  Scott used it for checking if the prefix for the model was included in the name, instead we now have a new method to use for the case of checking for prefix:

bindingContext.ValueProvider.ContainsPrefix(

So with a few minor changes / edits I get it updated to work with the new MVC 2.0 get it into the project when my client says

Hey, end users can’t handle a 24 hour clock, I want AM PM selection, and some of the date fields will be nullable so we need to handle that as well.

DOH! Scott’s work doesn’t handle that.  Damn, that means I have to really understand how the ModelBinder works because I need to modify it.  Of course some of my best code is stolen from inspired by code samples I find on the web, but it does make it easy to “use” code without fully understanding how something works.  When you want to modify it, not so much!  The new documentation for the MVC 2.0 on msdn in general, and around ModelBinders and all the associated classes / methods is not exactly feature rich. Looking up “IModelBinder.BindModel” gets you “Binds the model to a value by using the specified controller context and binding context.” with no examples or very handy links.  Of course that means it’s up to me to add some Community Content (can’t complain if I’m not willing to “light a candle to push back the darkness") about what I have discovered, maybe some example code. However, trying to understand this complicated process with minimal input really makes one feel the arrows in the back.

Anyways, enough whining, in the end I figured it out, and I won’t give you all the hairy details but here in all its Glory(?) is my update to Scott Hanselman’s original DateAndTimeModelBinder with support for a 12 hour clock, am/pm selection and nullable values.

New Model Binders

ModelBinders.Binders[typeof( DateTime )] =
new DateAndTimeModelBinder( ) { Date = "datevalue" , Hour = "hour" , Minute = "minute" , AmPm = "ampm" };

ModelBinders.Binders[typeof( DateTime? )] =
    new DateAndTimeModelBinder( ) { Date = "datevalue" , Hour = "hour" , Minute = "minute" , AmPm = "ampm" , IsNull = "isnull" };

Example Editor Template

(using Telerik Date Control for display)

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<DateTime?>" %>

<%
	var pm = false;
	var hour = DateTime.MinValue.Hour + 1;
	var min = DateTime.MinValue.Minute;
	
	if ( Model.HasValue )
	{
		hour = Model.Value.Hour;
		min = Model.Value.Minute;
	
		if ( hour > 11 )
		{
			pm = true;
			if ( hour > 12 )
			{
				hour -= 12;
			}
		}
		else if ( hour == 0 )
		{
			hour = 12;
		}

	}
	
	var hourSelectList = new List<SelectListItem>( );

	for ( int i = 1 ; i < 13 ; i++ )
	{
		hourSelectList.Add( new SelectListItem( ) { Text = i.ToString( ) , Value = i.ToString( ) , Selected = ( i == hour ) } );
	}

	var minuteSelectList = new List<SelectListItem>( );
	
	for ( int i = 0 ; i < 60 ; i++ )
	{
		minuteSelectList.Add( new SelectListItem( ) { Text = i.ToString( ) , Value = i.ToString( ) , Selected = ( i == min ) } );
	}

	var ampmSelectList = new List<SelectListItem>( );
	ampmSelectList.Add( new SelectListItem( ) { Text = "AM" , Value = "false" , Selected = !pm } );
	ampmSelectList.Add( new SelectListItem( ) { Text = "PM" , Value = "true" , Selected = pm } );
%>

	<div class="editor-label">
		<%: Html.LabelFor(model => model) %>
	</div>
	<div class="editor-field">
		<%: Html.CheckBox("isnull", !Model.HasValue) %>
		<%: Html.Telerik().DatePicker()
					.Name( ViewData.ModelMetadata.ContainerType.Name + "." + ViewData.ModelMetadata.PropertyName + "." + "datevalue" )
									.HtmlAttributes( new { id = ViewData.ModelMetadata.PropertyName } )
			.Value( Model > DateTime.MinValue ? Model : DateTime.Today )
		%>

		<%: Html.DropDownList( "hour" , hourSelectList )%>
		<%: Html.DropDownList("minute", minuteSelectList) %>
		<%: Html.DropDownList( "ampm" , ampmSelectList )%>
		<%: Html.ValidationMessageFor(model => model) %>
	</div>

DateAndTimeModelBinder

public class DateAndTimeModelBinder : IModelBinder
{
    public DateAndTimeModelBinder ( ) { }

    public object BindModel ( ControllerContext controllerContext , ModelBindingContext bindingContext )
    {
        if ( bindingContext == null )
        {
            throw new ArgumentNullException( "bindingContext" );
        }

        //Maybe we're lucky and they just want a DateTime the regular way.
        DateTime? dateTimeAttempt = GetA<DateTime>( bindingContext , "DateTime" );
        if ( dateTimeAttempt != null )
        {
            return dateTimeAttempt.Value;
        }

        //If they haven't set Month,Day,Year OR Date, set "date" and get ready for an attempt
        if ( this.MonthDayYearSet == false && this.DateSet == false )
        {
            this.Date = "Date";
        }

        //If they haven't set Hour, Minute, Second OR Time, set "time" and get ready for an attempt
        if ( this.HourMinuteSecondSet == false && this.TimeSet == false )
        {
            this.Time = "Time";
        }

        //If this datetime can be set to Nullable, lets see if they set it to null
        if ( this.IsNullSet )
        {
            var isNull = GetA<bool>( bindingContext , this.IsNull );
            if ( isNull.HasValue && isNull.Value )
            {
                return null;
            }
        }

        //Did they want the Date *and* Time?
        DateTime? dateAttempt = GetA<DateTime>( bindingContext , this.Date );
        DateTime? timeAttempt = GetA<DateTime>( bindingContext , this.Time );

        //Maybe they wanted the Time via parts
        if ( this.HourMinuteSecondSet )
        {
            var hour = GetA<int>( bindingContext , this.Hour ).GetValueOrDefault();
            var min =  GetA<int>( bindingContext , this.Minute ).GetValueOrDefault( );
            var sec = GetA<int>( bindingContext , this.Second ).GetValueOrDefault( );

            //and if they are doing the am/pm thing we need to adjust the hour
            if ( AmPmSet )
            {
                var pm = GetA<bool>( bindingContext , this.AmPm ).GetValueOrDefault(false);

                // if we have set for pm
                if ( pm )
                {
                    //we need to convert the hour to the 24 hour clock for times greater then
                    //12 noon (pm)
                    if ( hour < 12 )
                    {
                        hour += 12;
                    }
                }
                //if we are in am
                else
                {
                    // just one case to handle here when they put in 12 midnight (am)
                    if ( hour = 12 )
                    {
                        hour = 0;
                    }
                }

            }

            timeAttempt = new DateTime(
                 DateTime.MinValue.Year , DateTime.MinValue.Month , DateTime.MinValue.Day ,
                 hour ,
                 min ,
                 sec );
        }

        //Maybe they wanted the Date via parts
        if ( this.MonthDayYearSet )
        {
            dateAttempt = new DateTime(
                 GetA<int>( bindingContext , this.Year ).GetValueOrDefault( ) ,
                 GetA<int>( bindingContext , this.Month ).GetValueOrDefault( ) ,
                 GetA<int>( bindingContext , this.Day ).GetValueOrDefault( ) ,
                 DateTime.MinValue.Hour , DateTime.MinValue.Minute , DateTime.MinValue.Second );
        }

        //If we got both parts, assemble them!
        if ( dateAttempt != null && timeAttempt != null )
        {
            return new DateTime( dateAttempt.Value.Year ,
                            dateAttempt.Value.Month ,
                            dateAttempt.Value.Day ,
                            timeAttempt.Value.Hour ,
                            timeAttempt.Value.Minute ,
                            timeAttempt.Value.Second );
        }
        //Only got one half? Return as much as we have!
        return dateAttempt ?? timeAttempt;
    }

    private Nullable<T> GetA<T> ( ModelBindingContext bindingContext , string key ) where T : struct
    {
        if ( String.IsNullOrEmpty( key ) )
            return null;

        ValueProviderResult valueResult;

        //Try it with the prefix...
        if ( bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName) )
        {
            valueResult = bindingContext.ValueProvider.GetValue( bindingContext.ModelName + "." + key );
        }
        //Didn't work? Try without the prefix if needed...
        else
        {
            valueResult = bindingContext.ValueProvider.GetValue( key );
        }

        if ( valueResult == null )
        {
            return null;
        }

        return ( Nullable<T> )valueResult.ConvertTo( typeof( T ) );
    }

    public string Date { get; set; }
    public string Time { get; set; }

    public string Month { get; set; }
    public string Day { get; set; }
    public string Year { get; set; }

    public string Hour { get; set; }
    public string Minute { get; set; }
    public string Second { get; set; }
    public string AmPm { get; set; }

    public string IsNull { get; set; }

    public bool IsNullSet { get { return !String.IsNullOrWhiteSpace( IsNull ); } }

    public bool DateSet { get { return !String.IsNullOrWhiteSpace( Date ); } }
    public bool MonthDayYearSet { get { return !( String.IsNullOrWhiteSpace( Month ) && String.IsNullOrWhiteSpace( Day ) && String.IsNullOrWhiteSpace( Year ) ); } }

    public bool TimeSet { get { return !String.IsNullOrWhiteSpace( Time ); } }
    public bool HourMinuteSecondSet { get { return !( String.IsNullOrWhiteSpace( Hour ) && String.IsNullOrWhiteSpace( Minute ) && String.IsNullOrWhiteSpace( Second ) ); } }
    public bool AmPmSet { get { return !string.IsNullOrWhiteSpace( AmPm ); } }

}

public class DateAndTimeAttribute : CustomModelBinderAttribute
{
    private IModelBinder _binder;

    // The user cares about a full date structure and full
    // time structure, or one or the other.
    public DateAndTimeAttribute ( string date , string time )
    {
        _binder = new DateAndTimeModelBinder
        {
            Date = date ,
            Time = time
        };
    }

    // The user wants to capture the date and time (or only one)
    // as individual portions.
    public DateAndTimeAttribute ( string year , string month , string day ,
         string hour , string minute , string second )
    {
        _binder = new DateAndTimeModelBinder
        {
            Day = day ,
            Month = month ,
            Year = year ,
            Hour = hour ,
            Minute = minute ,
            Second = second
        };
    }

    // The user wants to capture the date and time (or only one)
    // as individual portions.
    public DateAndTimeAttribute ( string date , string time ,
         string year , string month , string day ,
         string hour , string minute , string second )
    {
        _binder = new DateAndTimeModelBinder
        {
            Day = day ,
            Month = month ,
            Year = year ,
            Hour = hour ,
            Minute = minute ,
            Second = second ,
            Date = date ,
            Time = time
        };
    }

    public override IModelBinder GetBinder ( ) { return _binder; }
}

Closing Note:

Due to loosing the fight against protecting my blog comments from the bloody spammers.  I’ve given up and just shut them off.  If you would like to comment you can reach me on twitter at @matthewhintzen.

Posted on 21 Apr 10 23:10 by matthew.hintzen |

Bookmark this post with:

E-mail | Comments(0) | Comment RSS