ASP.NET MVC Preview 5 and Form Posting Scenarios
page 4 of 9
by Scott Guthrie
Average Rating: This article has not yet been rated.
Views (Total / Last 10 Days): 40278/ 84

Model Binders

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.

View Entire Article

User Comments

No comments posted yet.

Product Spotlight
Product Spotlight 

Community Advice: ASP | SQL | XML | Regular Expressions | Windows

©Copyright 1998-2024  |  Page Processed at 2024-02-27 10:59:44 AM  AspAlliance Recent Articles RSS Feed
About ASPAlliance | Newsgroups | Advertise | Authors | Email Lists | Feedback | Link To Us | Privacy | Search