Binding error in custom model minder removes the value the user inputted
Asked Answered
S

1

8

I'm using ASP.NET MVC 3 RTM, and I have a view model like this:

public class TaskModel
{
  // Lot's of normal properties like int, string, datetime etc.
  public TimeOfDay TimeOfDay { get; set; }
}

The TimeOfDay property is a custom struct I have, which is quite simple, so I'm not including it here. I've made a custom model binder to bind this struct. The model binder is pretty simple:

public class TimeOfDayModelBinder : IModelBinder
{
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var result = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
        try
        {
            // Let the TimeOfDay struct take care of the conversion from string.
            return new TimeOfDay(result.AttemptedValue, result.Culture);
        }
        catch (ArgumentException)
        {
            bindingContext.ModelState.AddModelError(bindingContext.ModelName, "Value is invalid. Examples of valid values: 6:30, 16:00");
            return bindingContext.Model; // Also tried: return null, return value.AttemptedValue
        }
    }
}

My custom model binder works fine, but the problem is when the user provided value could not be converted or parsed. When this happens (When the TimeOfDay constructor throws an ArgumentException), I add a model error which is correctly displayed in the view, but the value the user typed, which could not be converted, is lost. The textbox the user typed the value in, is just empty, and in the HTML source the value attribute is set to an empty string: "".

EDIT: I'm wondering if it might be my editor template which is doing something wrong, so I'm including it here:

@model Nullable<TimeOfDay>
@if (Model.HasValue)
{
    @Html.TextBox(string.Empty, Model.Value.ToString());
}
else
{
    @Html.TextBox(string.Empty);
}

How do I make sure the value is not lost when a binding error happens, so the user can correct the value?

Superposition answered 3/4, 2011 at 22:53 Comment(0)
S
17

Aha! I finally found the answer! This blog post gave the answer. What I was missing is to call ModelState.SetModelValue() in my model binder. So the code would be like this:

public class TimeOfDayModelBinder : IModelBinder
{
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var result = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
        try
        {
            // Let the TimeOfDay struct take care of the conversion from string.
            return new TimeOfDay(result.AttemptedValue, result.Culture);
        }
        catch (ArgumentException)
        {
            bindingContext.ModelState.AddModelError(bindingContext.ModelName, "Value is invalid. Examples of valid values: 6:30, 16:00");
            // This line is what makes the difference:
            bindingContext.ModelState.SetModelValue(bindingContext.ModelName, result);
            return bindingContext.Model;
        }
    }
}

I hope this saves someone else from the hours of frustration I've been through.

Superposition answered 4/4, 2011 at 9:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.