In our sample above the signature of the Controller action
method that handles the form-post takes a String and a Decimal as method
arguments. The action method then creates a new Product object, assigns
these input values to it, and then attempts to insert it into the database:
One of the new capabilities in "Preview
5" that can make this scenario cleaner is its "Model Binder" support. Model Binders provide a way for complex types to be de-serialized from the
incoming HTTP input, and passed to a Controller action method as
arguments. They also provide support for handling input exceptions, and
make it easier to redisplay forms when errors occur (without requiring the
end-user to have to re-enter all their data again - more on this later in this
blog post).
For example, using the model binder support we could re-factor the above action method to instead take a Product object as an
argument like so:
This makes the code a little more terse and
clean. It also allows us to avoid having repetitive form-parsing code
scattered across multiple controllers/actions (allowing us to maintain the DRY
principle: "don't repeat yourself").
Registering Model Binders
Model Binders in ASP.NET MVC are classes that
implement the IModelBinder interface, and can be used to help manage the
binding of types to input parameters. A model binder can be written to
work against a specific object type, or can alternatively be used to handle a
broad range of types. The IModelBinder interface allows you to unit test
binders independent of the web-server or any specific controller
implementation.
Model Binders can be registered at 4 different
levels within an ASP.NET MVC application, which enables a great deal of
flexibility in how you use them:
1) ASP.NET MVC first looks for the presence of
a model binder declared as a parameter attribute on an action method. For
example, we could indicate that we wanted to use a hypothetical
"Bind" binder by annotating our product parameter using an attribute
like below (note how we are indicating that only two properties should be bound
using a parameter on the attribute):
Note: "Preview 5" doesn't have a
built-in [Bind] attribute like above just yet (although we are considering
adding it as a built-in feature of ASP.NET MVC in the future). However
all of the framework infrastructure necessary to implement a [Bind] attribute
like above is now implemented in preview 5. The open source MVCContrib project
also has a DataBind attribute like above that you can use today.
2) If no binder attribute is present on the action
parameter, ASP.NET MVC then looks for the presence of a binder registered as an
attribute on the type of the parameter being passed to the action method.
For example, we could register an explicit "ProductBinder" binder for
our LINQ to SQL "Product" object by adding code like below to our
Product partial class:
3) ASP.NET MVC also supports the ability to
register binders at application startup using the ModelBinders.Binders
collection. This is useful when you want to use a type written by a third
party (that you can't annotate) or if you don't want to add a binder attribute
annotation on your model object directly. The below code demonstrates how
to register two type-specific binders at application startup in your
global.asax:
4) In addition to registering type-specific
global binders, you can use the ModelBinders.DefaultBinder property to register
a default binder that will be used when a type-specific binder isn't
found. Included in the MVCFutures assembly (which is currently referenced
by default with the mvc preview builds) is a ComplexModelBinder implementation
that uses reflection to set properties based on incoming form post
names/values. You could register it to be used as the fallback for all
complex types passed as Controller action arguments using the code below:
Note: the MVC team plans to tweak the IModelBinder interface
further for the next drop (they recently discovered a few scenarios that
necessitate a few changes). So if you build a custom model binder with
preview 5 expect to have to make a few tweaks when the next drop comes out
(probably nothing too major - but just a heads up that we know a few arguments
will change on its methods).
UpdateModel and TryUpdateModel Methods
The ModelBinder support above is great for scenarios where
you want to instantiate new objects and pass them in as arguments to a
controller action method. There are also scenarios, though, when you want
to be able to bind input values to existing object instances that you own
retrieving/creating yourself within the action method. For example, when
enabling an edit scenario for an existing product in the database, you might
want to use an ORM to retrieve an existing product instance from the database
first within your action method, then bind the new input values to the
retrieved product instance, and then save the changes back to the database.
"Preview 5" adds two new methods on the Controller
base class to help enable this - UpdateModel() and TryUpdateModel(). Both
allow you to pass in an existing object instance as the first argument, and
then as a second argument you pass in a security white-list of properties you
want to update on them using the form post values. For example, below I'm
retrieving a Product object using LINQ to SQL, and then using the UpdateModel
method to update the product's name and price properties with form data.
The UpdateModel methods will attempt to update all of the
properties you list (even if there is an error on an early one in the
list). If it encounters an error for a property (for example: you entered
bogus string data for a UnitPrice property which is of type Decimal), it will
store the exception object raised as well the original form posted value in a
new "ModelState" collection added with "Preview 5".
We'll cover this new ModelState collection in a little bit - but in a nutshell
it provides an easy way for us to redisplay forms with the user-entered values
automatically populated for them to fix when there is an error.
After attempting to update all of the indicated properties,
the UpdateModel method will raise an exception if any of them failed. The
TryUpdateModel method works the same way - except that instead of raising an
exception it will return a boolean true/false value which indicates whether
there were any errors. You can choose whichever method works best with
your error handling preferences.
Product Edit Example
To see an example of using the UpdateModel method in use,
let's implement a simple product editing form. We'll use a URL format of
/Products/Edit/{ProductId} to indicate which product we want to edit. For
example, below the URL is /Products/Edit/4 - which means we are going to edit
the product whose ProductId is 4:
Users can change the product name or unit
price, and then click the Save button. When they do our post action
method will update the database and then show the user a "Product
Updated!" message if it was successful:
We can implement the above functionality using
the two Controller actions methods below. Notice how we are using the
[AcceptVerbs] attribute to differentiate the Edit action that displays the
initial form, and the one that handles the form post submission:
Our POST action method above uses LINQ to SQL
to retrieve an instance of the product object we are editing from the database,
then uses UpdateModel to attempt to update the product's ProductName and
UnitPrice values using the form post values. It then calls
SubmitChanges() on the LINQ to SQL datacontext to save the updates back to the
database. If that was successful, we then store a success message string
in the TempData collection and redirect the user back to the GET action method
using a client-side redirect (which will cause the newly saved product to be
redisplayed - along with our TempData message string indicating it was
updated). If there is an error either with the form posted values, or
with updating the database, an exception will be raised and caught in our catch
block - and we will redisplay the form view again to the user for them to fix.
You might wonder - what is up with this
redirect when we are successful? Why not just redisplay the form again
and show the success message? The reason for the client-redirect is to
ensure that if the user hits the refresh button after successfully pressing the
save button, they don't resubmit the form again and get hit with a browser
prompt like this:
Doing the redirect back to the GET version of the action
method ensures that a user hitting refresh will simply reload the page again
and not post back. This approach is called the "Post/Redirect/Get"
(aka PRG) pattern. Tim Barcz has a nice article here that talks about this more with ASP.NET MVC.
The above two controller action methods are all we need to
implement in order to handle editing and updating a Product object. Below
is the "Edit" view to go with the above Controller:
Useful Tip: In the past once you started added parameters to
URLs (for example: /Products/Edit/4) you had to write code in your view to
update the form's action attribute to include the parameters in the post
URL. "Preview 5" includes a Html.Form() helper method that can
make this easier. Html.Form() has many overloaded versions that allow you
to specify a variety of parameter options. A new overloaded Html.Form()
method that takes with no parameters has been added that will now output the
same URL as the current request.
For example, if the incoming URL to the Controller that
rendered the above view was "/Products/Edit/5", calling Html.Form()
like above would automatically output <form
action="/Products/Edit/5" method="post"> as the markup
output. If the incoming URL to the Controller that rendered the above
view was "/Products/Edit/55", calling Html.Form() like above would
automatically output <form action="/Products/Edit/55"
method="post"> as the markup output. This provides a nifty
way to avoid having to write any custom code yourself to construct the URL or
indicate parameters.