Generic TimeSpan binding in Asp.NET MVC 2
I have an input form that is bound to a model. The model ha开发者_开发技巧s a TimeSpan property, but it only gets the value correctly if I enter the time as hh:mm or hh:mm:ss. What I want is for it to capture the value even if it's written as hhmm or hh.mm or hh.mm.ss or ... I want many different formats to be parsed correctly. Is this possible?
Thanks!
I added a few enhancements to Carles' code and wanted to share them here in case they're useful for others.
- Ensure that if no patterns successfully parse the time, then still call the base in order to show a validation error (otherwise the value is left as
TimeSpan.Zero
and no validation error raised.) - Use a loop rather than chained
if
s. - Support the use of
AM
andPM
suffices. - Ignore whitespace.
Here's the code:
public sealed class TimeSpanModelBinder : DefaultModelBinder
{
private const DateTimeStyles _dateTimeStyles = DateTimeStyles.AllowWhiteSpaces | DateTimeStyles.AssumeLocal | DateTimeStyles.NoCurrentDateDefault;
protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor)
{
var form = controllerContext.HttpContext.Request.Form;
if (propertyDescriptor.PropertyType.Equals(typeof(TimeSpan?)) || propertyDescriptor.PropertyType.Equals(typeof(TimeSpan)))
{
var text = form[propertyDescriptor.Name];
TimeSpan time;
if (text != null && TryParseTime(text, out time))
{
SetProperty(controllerContext, bindingContext, propertyDescriptor, time);
return;
}
}
// Either a different type, or we couldn't parse the string.
base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
}
public static bool TryParseTime(string text, out TimeSpan time)
{
if (text == null)
throw new ArgumentNullException("text");
var formats = new[] {
"HH:mm", "HH.mm", "HHmm", "HH,mm", "HH",
"H:mm", "H.mm", "H,mm",
"hh:mmtt", "hh.mmtt", "hhmmtt", "hh,mmtt", "hhtt",
"h:mmtt", "h.mmtt", "hmmtt", "h,mmtt", "htt"
};
text = Regex.Replace(text, "([^0-9]|^)([0-9])([0-9]{2})([^0-9]|$)", "$1$2:$3$4");
text = Regex.Replace(text, "^[0-9]$", "0$0");
foreach (var format in formats)
{
DateTime value;
if (DateTime.TryParseExact(text, format, CultureInfo.InvariantCulture, _dateTimeStyles, out value))
{
time = value.TimeOfDay;
return true;
}
}
time = TimeSpan.Zero;
return false;
}
}
This may seem a little over the top, but I want my users to be able to enter pretty much anything and have my app work it out.
It can be applied to all DateTime
instances via this code in Global.asax.cs
:
ModelBinders.Binders.Add(typeof(TimeSpan), new TimeSpanModelBinder());
Or just on a specific action method parameter:
public ActionResult Save([ModelBinder(typeof(TimeSpanModelBinder))] MyModel model)
{ ... }
And here's a simple unit test just to validate some potential inputs/outputs:
[TestMethod]
public void TimeSpanParsing()
{
var testData = new[] {
new { Text = "100", Time = new TimeSpan(1, 0, 0) },
new { Text = "10:00 PM", Time = new TimeSpan(22, 0, 0) },
new { Text = "2", Time = new TimeSpan(2, 0, 0) },
new { Text = "10", Time = new TimeSpan(10, 0, 0) },
new { Text = "100PM", Time = new TimeSpan(13, 0, 0) },
new { Text = "1000", Time = new TimeSpan(10, 0, 0) },
new { Text = "10:00", Time = new TimeSpan(10, 0, 0) },
new { Text = "10.00", Time = new TimeSpan(10, 0, 0) },
new { Text = "13:00", Time = new TimeSpan(13, 0, 0) },
new { Text = "13.00", Time = new TimeSpan(13, 0, 0) },
new { Text = "10 PM", Time = new TimeSpan(22, 0, 0) },
new { Text = " 10\t PM ", Time = new TimeSpan(22, 0, 0) },
new { Text = "10PM", Time = new TimeSpan(22, 0, 0) },
new { Text = "1PM", Time = new TimeSpan(13, 0, 0) },
new { Text = "1 am", Time = new TimeSpan(1, 0, 0) },
new { Text = "1 AM", Time = new TimeSpan(1, 0, 0) },
new { Text = "1 pm", Time = new TimeSpan(13, 0, 0) },
new { Text = "1 PM", Time = new TimeSpan(13, 0, 0) },
new { Text = "01 PM", Time = new TimeSpan(13, 0, 0) },
new { Text = "0100 PM", Time = new TimeSpan(13, 0, 0) },
new { Text = "01.00 PM", Time = new TimeSpan(13, 0, 0) },
new { Text = "01.00PM", Time = new TimeSpan(13, 0, 0) },
new { Text = "1:00PM", Time = new TimeSpan(13, 0, 0) },
new { Text = "1:00 PM", Time = new TimeSpan(13, 0, 0) },
new { Text = "12,34", Time = new TimeSpan(12, 34, 0) },
new { Text = "1012PM", Time = new TimeSpan(22, 12, 0) },
};
foreach (var test in testData)
{
try
{
TimeSpan time;
Assert.IsTrue(TimeSpanModelBinder.TryParseTime(test.Text, out time), "Should parse {0}", test.Text);
if (!Equals(time, test.Time))
Assert.Fail("Time parse failed. Expected {0} but got {1}", test.Time, time);
}
catch (FormatException)
{
Assert.Fail("Received format exception with text {0}", test.Text);
}
}
}
Hope that helps someone out.
Yes - write a custom model binder for your model object. There's an thread about just that subject here on SO: ASP.NET MVC2 - Custom Model Binder Examples
For the record, here's how I did it:
using System;
using System.Globalization;
using System.Web.Mvc;
namespace Utils.ModelBinders
{
public class CustomTimeSpanModelBinder : DefaultModelBinder
{
protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor)
{
var form = controllerContext.HttpContext.Request.Form;
if (propertyDescriptor.PropertyType.Equals(typeof(TimeSpan?)))
{
var text = form[propertyDescriptor.Name];
DateTime value;
if (DateTime.TryParseExact(text, "HH:mm", CultureInfo.InvariantCulture, DateTimeStyles.None, out value))
SetProperty(controllerContext,bindingContext,propertyDescriptor,value.TimeOfDay);
else if (DateTime.TryParseExact(text, "HH.mm", CultureInfo.InvariantCulture, DateTimeStyles.None, out value))
SetProperty(controllerContext, bindingContext, propertyDescriptor, value.TimeOfDay);
else if (DateTime.TryParseExact(text, "HHmm", CultureInfo.InvariantCulture, DateTimeStyles.None, out value))
SetProperty(controllerContext, bindingContext, propertyDescriptor, value.TimeOfDay);
else if (DateTime.TryParseExact(text, "HH,mm", CultureInfo.InvariantCulture, DateTimeStyles.None, out value))
SetProperty(controllerContext, bindingContext, propertyDescriptor, value.TimeOfDay);
else if (DateTime.TryParseExact(text, "HH", CultureInfo.InvariantCulture, DateTimeStyles.None, out value))
SetProperty(controllerContext, bindingContext, propertyDescriptor, value.TimeOfDay);
}
else
{
base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
}
}
}
}
精彩评论