0

In an ASP.NET MVC 4 application I have a view model that contains a nullable TimeSpan property:

[DisplayName("My time")]
public TimeSpan? MyTime { get; set; }

It is bound to an input element in the view:

@Html.EditorFor(model => model.MyTime)

The input box gets rendered with the help of a custom editor template TimeSpan.cshtml:

@model Nullable<System.TimeSpan>

@Html.TextBox("", (Model.HasValue
    ? Model.Value.ToString(@"hh\:mm") : string.Empty),
    new { @class = "text-box single-line hasTimepicker" data_timepicker = true })

Now, if I enter the following two kinds of invalid times and submit the page I get the following different behaviour of the model binder:

  • If I enter a letter, say "a" into the input element the ModelError for this property when I drill into the ModelState.Values collection has the ErrorMessage property set to a message ("The value \"a\" for \"My time\" is invalid.") and the Exception property is null. The bound value of MyTime is null.

    This ErrorMessage is displayed in the validation summary of the page.

  • If I enter an invalid time, say "25:12", into the input element the ModelError for this property has the ErrorMessage property set to an empty string but the Exception property set to an exception of type InvalidOperationException with an inner exception of type OverflowException telling me that TimeSpan could not be analysed because one of its numeric components is out of valid range. The bound value of MyTime is null.

    Again, the ErrorMessage is displayed in the validation summary of the page. But because it is empty it's not very useful.

Ideally for the second case of invalid input I would prefer to have the same kind of error message like for the first case, for example "The value \"25:12\" for \"My time\" is invalid.".

How can I solve this problem?

Edit

A custom validation attribute apparently doesn't help here because it is not called for the invalid input in the examples above when already the model binder detects invalid values. I had tried that approach without success.

Community
  • 1
  • 1
Slauma
  • 175,098
  • 59
  • 401
  • 420

2 Answers2

2

The problem is that the error is happening at model binding and that's where you need to be catching and checking it.

I have a Timespan model binder and editor template for TimeSpan? which should do what you need that's up on Gist

Chao
  • 3,033
  • 3
  • 30
  • 36
  • The idea to create a model binder (which I wasn't very familiar with) and experimenting with your code pointed me into right direction although I did it differently in the end (see my own answer here). I learned something new. Thank you! – Slauma May 10 '13 at 18:06
1

@Chao's answer brought me on the right track to use a custom model binder.

Because I wanted to keep as much functionality of the default model binder (flexibility of entered formats, localization, etc.) as possible unchanged and only have a useful error message for the user in the case when he enters "25:12" or similar I've created the following binder that just detects if the default model binder has added an OverflowException (as an inner exception) to the model state and if yes I add an error message to the state:

public class TimeSpanModelBinder : DefaultModelBinder
{
    public override object BindModel(ControllerContext controllerContext,
        ModelBindingContext bindingContext)
    {
        object timeSpanValue = base.BindModel(controllerContext, bindingContext);

        var modelState = bindingContext.ModelState[bindingContext.ModelName];
        var hasOverflowException = modelState.Errors
            .Any(e => e.Exception != null &&
                e.Exception.InnerException is OverflowException);

        if (hasOverflowException)
        {
            var rawValues = modelState.Value.RawValue as string[];
            if (rawValues != null && rawValues.Length >= 1)
            {
                bindingContext.ModelState.AddModelError(
                    bindingContext.ModelName, string.Format(
                        "The value \"{0}\" for field \"{1}\" is invalid.",
                        rawValues[0],
                        bindingContext.ModelMetadata.GetDisplayName()));
            }
        }

        return timeSpanValue;
    }
}

Added in global.asax/Application_Start() to the ModelBinders collection:

ModelBinders.Binders.Add(typeof(TimeSpan), new TimeSpanModelBinder());
ModelBinders.Binders.Add(typeof(TimeSpan?), new TimeSpanModelBinder());
Slauma
  • 175,098
  • 59
  • 401
  • 420
  • Is there anyway to get this `ModelBinder` to bind with `AM/PM` format? It still complains when I submit a TimeSpan with format `11:45 PM` – Jack Apr 09 '18 at 23:04