ASP.NET MVC 5 Model Binding

时间:2022-11-30 17:52:07

Default Model Binder

The Order in Which the DefaultModelBinder Class Looks for Parameter Data:

  1. Request.Form - Values provided by the user in HTML form elements
  2. RouteData.Values - The values obtained using the application routes
  3. Request.QueryString - Data included in the query string portion of the request URL
  4. Request.Files - Files that have been uploaded as part of the request

Bind to simple type:

The DefaultModelBinder tries to convert the string value, which has been obtained from the request data into the parameter type using the System.ComponentModel.TypeDescriptor class. If the value cannot be converted, then the DefaultModelBinder won’t be able to bind to the model.

Bind to complex type:

Then the DefaultModelBinder class uses reflection to obtain the set of public properties and then binds to each of them in turn. If a property requires another complex type, then the process is repeated for the new type. The set of public properties are obtained and the binder tries to find values for all of them. The difference is that the property names are nested.

Specifying Custom Prefixes:

There are occasions when the HTML you generate relates to one type of object, but you want to bind it to another. In this case, you can use Bind attribute to the action method parameter, which tells the binder which prefix to look for. Below example binds HomeAddress to AddressSummary.

public ActionResult DisplaySummary([Bind(Prefix="HomeAddress")]AddressSummary summary) {
  return View(summary);
}

Selectively Binding Properties:

The Exclude property of the Bind attribute allows you to exclude properties from the model binding process. Below example won't bind Country property.

public ActionResult DisplaySummary([Bind(Prefix="HomeAddress", Exclude="Country")]AddressSummary summary) {
    return View(summary);
}

As an alternative, you can use the Include property to specify only those properties that should be bound in the model; all other properties will be ignored.

When the Bind attribute is applied to an action method parameter, it only affects instances of that class that are bound for that action method; all other action methods will continue to try and bind all the properties defined by the parameter type. If you want to create a more widespread effect, then you can apply the Bind attribute to the model class itself.

namespace MvcModels.Models {
  [Bind(Include="City")]
  public class AddressSummary {
    public string City { get; set; }
    public string Country { get; set; }
  }
}

Binding to Arrays:

The default model binder sees that the action method requires an array and looks for data items that have the same name as the parameter.

Action Method:

public ActionResult Names(string[] names) {
  names = names ?? new string[0];
  return View(names);
}

Generated Html Code:

<form action="/Home/Names" method="post">
  <div>
    <label>1:</label>
    <input id="names" name="names" type="text" value="" />
  </div>
  <div>
    <label>2:</label>
    <input id="names" name="names" type="text" value="" />
  </div>
  <div>
    <label>3:</label>
    <input id="names" name="names" type="text" value="" />
  </div>
  <button type="submit">Submit</button>
</form>

Binding to Collections:

The default model binder sees that the action method requires a collection and looks for data items that have the same name as the parameter.

Action Method:

public ActionResult Names(IList<string> names) {
  names = names ?? new List<string>();
  return View(names);
}

Generated Html Code:

<form action="/Home/Names" method="post">
  <div>
    <label>1:</label>
    <input id="names" name="names" type="text" value="" />
  </div>
  <div>
    <label>2:</label>
    <input id="names" name="names" type="text" value="" />
  </div>
  <div>
    <label>3:</label>
    <input id="names" name="names" type="text" value="" />
  </div>
  <button type="submit">Submit</button>
</form>

Binding to Collections of Custom Model Types:

Action Method:

public ActionResult Address(IList<AddressSummary> addresses) {
  addresses = addresses ?? new List<AddressSummary>();
  return View(addresses);
}

Generated Html Code:

<fieldset>
  <legend>Address 1</legend>
  <div>
    <label>City:</label>
    <input class="text-box single-line" name="[0].City" type="text" value="" />
  </div>
  <div>
    <label>Country:</label>
    <input class="text-box single-line" name="[0].Country" type="text" value="" />
  </div>
</fieldset>
<fieldset>
  <legend>Address 2</legend>
  <div>
    <label>City:</label>
    <input class="text-box single-line" name="[1].City" type="text" value="" />
  </div>
  <div>
    <label>Country:</label>
    <input class="text-box single-line" name="[1].Country" type="text" value="" />
  </div>
</fieldset>

When the Html form is submitted, the default model binder realizes that it needs to create a collection of AddressSummary objects and uses the array index prefixes in the name attributes to obtain values for the object properties. The properties prefixed with [0] are used for the first AddressSummary object, those prefixed with [1] are used for the second object, and so on.

Manually Invoking Model Binding

public ActionResult Address() {
    IList<AddressSummary> addresses = new List<AddressSummary>();
    UpdateModel(addresses);
    return View(addresses);
}

The UpdateModel method takes a model object that I was previously defining as a parameter and tries to obtain values for its public properties using the standard binding process.

public ActionResult Address() {
    IList<AddressSummary> addresses = new List<AddressSummary>();
    UpdateModel(addresses, new FormValueProvider(ControllerContext));
    return View(addresses);
}

When I manually invoke the binding process, I am able to restrict the binding process to a single source of data. By default, the binder looks in four places: form data, route data, the query string, and any uploaded files.

The Built-in IValueProvider Implementations:

  1. Request.Form - FormValueProvider
  2. RouteData.Values - RouteDataValueProvider
  3. Request.QueryString - QueryStringValueProvider
  4. Request.Files - HttpFileCollectionValueProvider
public ActionResult Address(FormCollection formData) {
  IList<AddressSummary> addresses = new List<AddressSummary>();
  UpdateModel(addresses, formData);
  return View(addresses);
}

The FormCollection class implements the IValueProvider interface, and if I define the action method to take a parameter of this type, the model binder will provide me with an object that I can pass directly to the UpdateModel method.

Dealing with Binding Errors

public ActionResult Address(FormCollection formData) {
    IList<AddressSummary> addresses = new List<AddressSummary>();
    if (TryUpdateModel(addresses, formData)) {
    // proceed as normal
    } else {
    // provide feedback to user
    }
    return View(addresses);
}

Customizing the Model Binding System

Creating a Custom Value Provider

Creating a Custom Model Binder

Registering the Custom Model Binder

Registering a Model Binder with an Attribute