Skip to content

Instantly share code, notes, and snippets.

@chaoaretasty
Created January 24, 2012 18:07
Show Gist options
  • Save chaoaretasty/1671586 to your computer and use it in GitHub Desktop.
Save chaoaretasty/1671586 to your computer and use it in GitHub Desktop.
Timespan model binding for MVC3
=========================
Based on the Simple Time Picker Modelbinder by Sergi Papseit Valls
http://www.sharpedgesoftware.com/Blog/2011/06/08/simple-time-picker-model-binder-for-aspnet-mvc-3-and-how-to-save-it-to-sql-ce-4-using-ef-41
=========================
I tried the modelbinder above but found a few issues with it including not being able to deal with blank options or partially filled out TimeSpans. This is my current version of it (it is a bit messy I warn you).
This is designed to bind to nullable TimeSpans, I haven't tried it with non-nullable ones, but you can overcome this by adding a [Required] attribute. The features are:
1. Will allow showing an error in a single part of the TimeSpan but put the message for the main property. For example if hours were given but no minutes the drop down for the minutes property would be highlighted but you can get the error message via ValidationMessageFor(x=>x.MyTimeSpan) which should help it fit in better with your form design rather than checking each sub-property.
2. Will gracefully handle unexpected inputs (eg if you use textboxes rather than dropdowns or it the form data is altered to send something other than numbers in the right range).
3. Won't display a validation error if neither hours or minutes are selected. Will display an error message if the TimeSpan has the [Required] attribute
4. Can take a variable in the editor's ViewData to limit the number of options for the minutes
PS Remember to add the following line to your Application_Start() method:
ModelBinders.Binders.Add(typeof(TimeSpan?), new MyApp.ModelBinders.TimeBinder());
Update
======
Added ViewData properties to limit the hours range
public class TimeBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext,
ModelBindingContext bindingContext)
{
// Ensure there's incoming data
var keyH = bindingContext.ModelName + ".Value.Hours";
var keyM = bindingContext.ModelName + ".Value.Minutes";
var valueProviderResultH = bindingContext.ValueProvider
.GetValue(keyH);
var valueProviderResultM = bindingContext.ValueProvider
.GetValue(keyM);
if ((valueProviderResultH == null || valueProviderResultH.AttemptedValue == "") && (valueProviderResultM == null || valueProviderResultM.AttemptedValue == ""))
{
return null;
}
// Preserve it in case we need to redisplay the form
bindingContext.ModelState
.SetModelValue(keyH, valueProviderResultH);
bindingContext.ModelState
.SetModelValue(keyM, valueProviderResultM);
// Parse
var hours = ((string)valueProviderResultH.AttemptedValue);
var minutes = ((string)valueProviderResultM.AttemptedValue);
// A TimeSpan represents the time elapsed since midnight
int Hours = 0;
int Minutes = 0;
bool valid = true;
try
{
Hours = Convert.ToInt32(hours);
if (Hours < 0 || Hours > 23)
{
throw new ArgumentOutOfRangeException();
}
}
catch
{
bindingContext.ModelState.AddModelError(
bindingContext.ModelName+".Value.Hours","");
bindingContext.ModelState.AddModelError(
bindingContext.ModelName,
"Hours must be between 0 and 23");
valid = false;
}
try
{
Minutes = Convert.ToInt32(minutes);
if (Minutes < 0 || Minutes > 59)
{
throw new ArgumentOutOfRangeException();
}
}
catch
{
bindingContext.ModelState.AddModelError(
bindingContext.ModelName + ".Value.Minutes","");
bindingContext.ModelState.AddModelError(
bindingContext.ModelName,
"Minutes must be between 0 and 59");
valid = false;
}
var time = new TimeSpan(Convert.ToInt32(Hours),
Convert.ToInt32(Minutes), 0);
if (!valid)
return null;
return time;
}
}
@model TimeSpan?
@{
int firstHour = (int)(ViewData["firstHour"] ?? 0);
int lastHour = (int)(ViewData["lastHour"] ?? 23);
int minuteIntervals = (int)(ViewData["minuteIntervals"] ?? 1);
//If there's an error for the TimeSpan but not for its subproperties add an error for both subproperties, this will add the css class for both fields
string modelPrefix = ViewData.TemplateInfo.HtmlFieldPrefix;
string minPrefix = modelPrefix + ".Value.Minutes";
string hourPrefix = modelPrefix + ".Value.Hours";
var subProps = ViewData.ModelState.Where(x => x.Key == hourPrefix || x.Key== minPrefix);
bool modelError = ViewData.ModelState[modelPrefix] != null && ViewData.ModelState[modelPrefix].Errors.Any();
if(!subProps.Any(x => x.Value.Errors.Any()) && modelError)
{
ViewData.ModelState.AddModelError(minPrefix, "");
ViewData.ModelState.AddModelError(hourPrefix, "");
}
}
@Html.DropDownListFor(x=>x.Value.Hours, Enumerable.Range(firstHour, (lastHour-firstHour+1))
.Select(i => new SelectListItem
{
Value = i.ToString(),
Text = i.ToString()
}), "")&nbsp;:
@Html.DropDownListFor(x=>x.Value.Minutes, Enumerable.Range(0, (int)Math.Floor(60f/minuteIntervals)).Select(x => x * minuteIntervals)
.Select(i => new SelectListItem
{
Value = i.ToString(),
Text = i.ToString()
}), "")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment