Search code examples
asp.net-mvc-3asp.net-mvc-2custom-model-bindercustom-binding

Custom model binder for inner model


I got a model like this:

public class MainModel
{
   public string Id {get;set;}
   public string Title {get;set;}
   public TimePicker TimePickerField {get;set;}
}

TimePicker is an inner model which looks like this:

public class TimePicker 
{
   public TimeSpan {get;set;}
   public AmPmEnum AmPm {get;set;}
}

I'm trying to create a custom model binding for inner model: TimePicker

The question is: How do I get values in custom model binder which was submitted in form into TimePicker model fields?

If I try to get it like this:

var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);

I just get null in value.

I'm not sure how to implement the model binder correctly.

public class TimePickerModelBinder : DefaultModelBinder
{
    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        if (bindingContext == null)
        {
            throw new ArgumentNullException("bindingContext");
        }
        var result = new TimePicker();

        var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
        if (value != null)
        {
            bindingContext.ModelState.SetModelValue(bindingContext.ModelName, value);
            try
            {
                //result = Duration.Parse(value.AttemptedValue);
            }
            catch (Exception ex)
            {
               bindingContext.ModelState.AddModelError(bindingContext.ModelName, ex.Message);
            }
        }    

        return result;
    }
}

Solution

  • The following works for me.

    Model:

    public enum AmPmEnum
    {
        Am, 
        Pm
    }
    
    public class TimePicker 
    {
        public TimeSpan Time { get; set; }
        public AmPmEnum AmPm { get; set; }
    }
    
    public class MainModel
    {
        public TimePicker TimePickerField { get; set; }
    }
    

    Controller:

    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            var model = new MainModel
            {
                TimePickerField = new TimePicker
                {
                    Time = TimeSpan.FromHours(1),
                    AmPm = AmPmEnum.Pm
                }
            };
            return View(model);
        }
    
        [HttpPost]
        public ActionResult Index(MainModel model)
        {
            return View(model);
        }
    }
    

    View (~/Views/Home/Index.cshtml):

    @model MainModel
    @using (Html.BeginForm())
    {
        @Html.EditorFor(x => x.TimePickerField)
        <button type="submit">OK</button>
    }
    

    Custom editor template (~/Views/Shared/EditorTemplates/TimePicker.cshtml) which merges the Time and AmPm properties into a single input field and which will require a custom model binder later in order to split them when the form is submitted:

    @model TimePicker
    @Html.TextBox("_picker_", string.Format("{0} {1}", Model.Time, Model.AmPm))
    

    and the model binder:

    public class TimePickerModelBinder : IModelBinder
    {
        public object BindModel(
            ControllerContext controllerContext, 
            ModelBindingContext bindingContext
        )
        {
            var key = bindingContext.ModelName + "._picker_";
            var value = bindingContext.ValueProvider.GetValue(key);
            if (value == null)
            {
                return null;
            }
    
            var result = new TimePicker();
    
            try
            {
                // TODO: instead of hardcoding do your parsing
                // from value.AttemptedValue which will contain the string
                // that was entered by the user
                return new TimePicker
                {
                    Time = TimeSpan.FromHours(2),
                    AmPm = AmPmEnum.Pm
                };
            }
            catch (Exception ex)
            {
                bindingContext.ModelState.AddModelError(
                    bindingContext.ModelName, 
                    ex.Message
                );
                // This is important in order to preserve the original user
                // input in case of error when redisplaying the view
                bindingContext.ModelState.SetModelValue(key, value);
            }
            return result;
        }
    }
    

    and finally register your model binder in Application_Start:

    ModelBinders.Binders.Add(typeof(TimePicker), new TimePickerModelBinder());