4

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;
    }
}
nhahtdh
  • 55,989
  • 15
  • 126
  • 162
Mindaugas
  • 163
  • 1
  • 7
  • 16
  • From your question it's not clear what values are submitted and how does your view look like. It is also not clear why do you need a custom model binder for this model and what exactly will be its purpose. Also it is not clear if you are directly implementing `IModelBinder` or if you subclassed the `DefaultModelBinder`. And another thing that is not clear is in which method of this model binder are you attempting to call `bindingContext.ValueProvider.GetValue(bindingContext.ModelName);` (obviously if you are directly implementing IModelBinder there would be a single method: BindModel). – Darin Dimitrov Jan 24 '12 at 20:36
  • view have two fields: text field with time and two radio buttons with PM/AM selection. I don't know what needs to be implemented or IModelBinder directly or subclasses DefaultModelBinder. Would be really nice to get advice on that. Yes i call it in BindModel i added my binder to my question. – Mindaugas Jan 25 '12 at 07:09
  • any chance to see your code? Also you didn't answer my question why do you need a custom model binder. What exactly are you trying to achieve? – Darin Dimitrov Jan 25 '12 at 07:12
  • because its system wide component for time picking. and i want to autobind it with model. Because in one end i get a string like that: 01:23 PM etc. and i need it to convert it to normal time. Same when i get time from database and i need it to show in the component. home that made things a bit more clear. – Mindaugas Jan 25 '12 at 07:15

1 Answers1

11

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());
Darin Dimitrov
  • 1,023,142
  • 271
  • 3,287
  • 2,928