Friday, May 19, 2017
Model binding in ASP.NET MVC
Model binding is a mechanism ASP.NET MVC uses to create parameter objects defined in controller action methods. The parameters can be of any type, from simple to complex ones. It simplifies working with data sent by the browser because data is automatically assigned to the specified model. Without this mechanism, developers would need to manually assign values to model properties, which would result in writing almost the same code in every action method.
How default model binding works?
Let’s say we are working on an application for managing employees of a company. In our app, we will have an EmployeeController
class with the following action method:
[HttpGet]
public ActionResult Edit(int id)
{
//code to fetch employee
...
return View(employee);
}
Since the default route configuration is used, when you navigate to /Employee/Edit/5
, the default action invoker will use the routing information to discover that the Edit
action method of the EmployeeController
should handle the request. However, it can not invoke the Edit
method without a valid value for the id
parameter. The ControllerActionInvoker
then looks for the responsible model binder for the int
type and, if you did not provide your own, the DefaultModelBinder
is selected.
By default, the DefaultModelBinder
searches for the values in four locations, in the following order:
Source | Description |
---|---|
Request.Form | Values submitted in a form |
RouteData.Values | Route values, e.g. id in /Employee/Edit/{id} |
Request.QueryString | Data extracted from the query string portion of the URL |
Request.Files | Uploaded files |
The search stops as soon as the value is found. In this case, the binder will first look in the Form
collection. Since there was no form submitted and thus no value named id
in that collection, it will look in the RouteData
dictionary. The binder finds what it was looking for and does not go on to search in QueryString
and Files
collections. The ControllerActionInvoker
now has all that’s necessary to invoke the method.
Customizing model binding
Sometimes, it may be convenient to use a custom model binder. We can do this by providing our own implementation of the IModelBinder
interface. An alternative is to make your custom model binder inherit from DefaultModelBinder
class and override the BindModel
method. In my example, I will use the first way and implement IModelBinder
directly.
Let’s say we want to create a form for adding new employees. We define a controller action method:
[HttpPost]
public ActionResult Create(EmployeeViewModel employee)
{
......
return RedirectToAction("Index");
}
The form will look like this:
@using(Html.BeginForm("Create", "Employee", FormMethod.Post))
{
<p>
@Html.Label("FirstName")
@Html.TextBox("FirstName")
</p>
<p>
@Html.Label("MiddleName")
@Html.TextBox("MiddleName")
</p>
<p>
@Html.Label("LastName")
@Html.TextBox("LastName")
</p>
<p>
@Html.LabelFor(p => p.Address)
@Html.TextBoxFor(p => p.Address)
</p>
<p>
@Html.LabelFor(p => p.Department)
@Html.TextBoxFor(p => p.Department)
</p>
<p>
@Html.LabelFor(p => p.JoinDate)
@Html.TextBoxFor(p => p.JoinDate, new { type = "date" })
</p>
<input type="submit" value="Save" />
}
For the purpose of this example, we want to have a form with separate input fields for First, Middle and Last Name, but for some reason, the EmployeeViewModel
has only FullName property, and we can not change it:
public class EmployeeViewModel
{
public int Id { get; set; }
public string FullName { get; set; }
public DateTime JoinDate { get; set; }
public string Address { get; set; }
public string Department { get; set; }
}
We could deal with this easily within the Create
action method, but it would be nicer to have that part of the code abstracted away. So we will create a new class named EmployeeModelBinder
and have it implement IModelBinder
:
public class EmployeeModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
HttpRequestBase request = controllerContext.HttpContext.Request;
List<string> nameValues = new List<string>();
int id = Convert.ToInt32(request.Form.Get("Id"));
nameValues.Add(request.Form.Get("FirstName"));
nameValues.Add(request.Form.Get("MiddleName"));
nameValues.Add(request.Form.Get("LastName"));
string address = request.Form.Get("Address");
string department = request.Form.Get("Department");
DateTime joinDate = Convert.ToDateTime(request.Form.Get("JoinDate"));
return new EmployeeViewModel()
{
Id = id,
FullName = string.Join(" ", nameValues),
Address = address,
Department = department,
JoinDate = joinDate
};
}
}
The custom model binder must be registered. If we want to do it globally, it can be done in Global.asax.cs
within Application_Start()
method:
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
ModelBinders.Binders.Add(typeof(EmployeeViewModel), new EmployeeModelBinder());
}
If we don’t want to register the binder globally, we can attach a ModelBinder
attribute to the action method parameter:
[HttpPost]
public ActionResult Create([ModelBinder(typeof(EmployeeModelBinder))] EmployeeViewModel employee)
{
...
return RedirectToAction("Index");
}
Now, when we submit our form with separate name values, the custom model binder will make sure they are all concatenated and assigned to the FullName
property of the EmployeeViewModel
.