MVC UpdateModel when the names don't match up
Asked Answered
M

2

3

Let's say that you have a Model that looks kind of like this:

public class MyClass {
    public string Name { get; set; }
    public DateTime MyDate { get; set; }
}

The default edit template that Visual Studio gives you is a plain textbox for the MyDate property. This is all fine and good, but let's say that you need to split that up into it's Month/Day/Year components, and your form looks like:

<label for="MyDate">Date:</label>
<%= Html.TextBox("MyDate-Month", Model.MyDate.Month) %>
<%= Html.TextBox("MyDate-Day", Model.MyDate.Day) %>
<%= Html.TextBox("MyDate-Year", Model.MyDate.Year) %>

When this is submitted, a call to UpdateModel won't work, since there isn't a definition for MyDate-Month. Is there a way to add a custom binder to the project to handle situations like this, or if the HTML inputs are named differently (for whatever reasons)?

One workaround I've found is to use JavaScript to inject a hidden input into the form before submission that concatenates the fields and is named properly, but that feels wrong.

Meade answered 23/7, 2009 at 18:36 Comment(0)
J
6

I would suggest you a custom model binder:

using System;
using System.Globalization;
using System.Web.Mvc;

public class MyClassBinder : DefaultModelBinder
{
    protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
    {
        var model = (MyClass)base.CreateModel(controllerContext, bindingContext, modelType);

        var day = bindingContext.ValueProvider["MyDate-Day"];
        var month = bindingContext.ValueProvider["MyDate-Month"];
        var year = bindingContext.ValueProvider["MyDate-Year"];

        var dateStr = string.Format("{0}/{1}/{2}", month.AttemptedValue, day.AttemptedValue, year.AttemptedValue);
        DateTime date;
        if (DateTime.TryParseExact(dateStr, "MM/dd/yyyy", null, DateTimeStyles.None, out date))
        {
            model.MyDate = date;
        }
        else
        {
            bindingContext.ModelState.AddModelError("MyDate", "MyDate has invalid format");
        }

        bindingContext.ModelState.SetModelValue("MyDate-Day", day);
        bindingContext.ModelState.SetModelValue("MyDate-Month", month);
        bindingContext.ModelState.SetModelValue("MyDate-Year", year);

        return model;
    }
}

This simplifies your controller action to:

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult MyAction(MyClass myClass)
{
    if (!ModelState.IsValid)
    {
        return View(myClass);
    }
    // Do something with myClass
    return RedirectToAction("success");
}

And register the binder in Global.asax:

protected void Application_Start()
{
    RegisterRoutes(RouteTable.Routes);
    ModelBinders.Binders.Add(typeof(MyClass), new MyClassBinder());
}
Johathan answered 23/7, 2009 at 19:1 Comment(3)
Interesting approach. I'm not familiar with Custom Model Binders, so how do I call this from the Controller? And do I need to add something separate for "normal" properties (like the Name one in my example) or will the default binder take care of that? Thanks.Meade
In my original post I suggested implementing IModelBinder directly which would mean that you need to handle all the properties manually and not only MyDate which could become tedious. I've modified my post so that MyClassBinder now derives from the DefaultModelBinder which means standard properties such as "Name" will be handled as default.Johathan
You don't need to call it from the controller. All you need is to register the binder in Application_Start and it will be invoked by the framework before the controller action just like the default model binder.Johathan
B
4

A simple way to handle this would be to get the values manually from the ValueProvider and construct the date server side, using UpdateModel with a white list that excludes these properties.

  int month = int.Parse( this.ValueProvider["MyDate-Month"].AttemptedValue );
  int day = ...
  int year = ...

  var model = db.Models.Where( m = > m.ID == id );
  var whitelist = new string[] { "Name", "Company", ... };

  UpdateModel( model, whitelist );

  model.MyDate = new DateTime( year, month, day );

Of course, you'd need to add validation/error handling manually as well.

Bolten answered 23/7, 2009 at 18:43 Comment(1)
That would work. Although I'd need to use ValueProvider[].AttemptedValue, RawValue returns an array of string :).Meade

© 2022 - 2024 — McMap. All rights reserved.