applications this is an unlikely scenario. The ASP.NET MVC framework makes it very easy to
parameterize action methods from a variety of sources.
As mentioned in the previous section, the “Default” route exposes an “id” parameter, which
defaults to an empty string. To access the value of the “id” parameter from within the action
method you can just add it to the signature of the method itself as the following snippet shows:
c#
public ActionResult Details(int id)
{
using (var db = new ProductsDataContext())
{
var product = db.Products.SingleOrDefault(x = > x.ProductID == id);
if (product == null)
return View("NotFound");
return View(product);
}
}
Code snippet Controllers\ProductsController.cs
Vb
Public Function Details(ByVal id As Integer) As ActionResult
Using db As New ProductsDataContext
Dim product = db.Products.FirstOrDefault(Function(p As Product)
p.ProductID = id)
Return View(product)
End Using
End Function
Code snippet Controllers\ProductsController.vb
When the MVC framework executes the Details action method it will search through the parameters
that have been extracted from the URL by the matching route. These parameters are matched up
with the parameters on the action method by name and then passed in when the method is called.
As the details method shows, the framework is even able to convert the type of the parameter on the
fly. Action methods can also retrieve parameters from the query string portion of the URL and from
HTTP POST data using the same technique.
If the conversion cannot be made for any reason, an exception is thrown.
advanced MVC .
457
Additionally, an action method can accept a parameter of the FormValues type that will aggregate
all of the HTTP POST data into a single parameter. If the data in the FormValues collection
represents the properties of an object, you can simply add a parameter of that type and a new
instance will be created when the action method is called. The Create action, shown in the
following snippet, uses this to construct a new instance of the Product class and then save it:
c#
public ActionResult Create()
{
return View();
}
[HttpPost]
public ActionResult Create([Bind(Exclude="ProductId")]Product product)
{
if (!ModelState.IsValid)
return View();
using (var db = new ProductsDataContext())
{
db.Products.InsertOnSubmit(product);
db.SubmitChanges();
}
return RedirectToAction("List");
}
Code snippet Controllers\ProductsController.cs
Vb
< HttpPost() >
Function Create( < Bind(Exclude:="id") > ByVal product As Product)
If (Not ModelState.IsValid) Then
Return View()
End If
Using db As New ProductsDataContext
db.Products.InsertOnSubmit(product)
db.SubmitChanges()
End Using
Return RedirectToAction("List")
End Function
Code snippet Controllers\ProductsController.vb
There are two Create action methods here. The first one simply renders the “Create”
view. The second one is marked up with an HttpPostAttribute, which means that
it will only be selected if the HTTP request uses the POST verb. This is a common
practice in designing ASP.NET MVC web sites. In addition to HttpPostAttribute
there are also corresponding attributes for the GET, PUT, and DELETEverbs.
458 .
chaPter 21 ASp.neT mVc
Model Binders
The process of creating the new Product instance is the responsibility of a model binder. The model
binder matches properties in the HTTP POST data with properties on the type that it is attempting to
create. This works in this example because the template that was used to generate the “Create” view
renders the HTML INPUT fields with the correct name as this snippet of the rendered HTML shows:
htMl
<p>
<label for=”ProductID”>ProductID:</label>
<input id=”ProductID” name=”ProductID” type=”text” value=”” />
</p>
<p>
<label for=”Name”>Name:</label>
<input id=”Name” name=”Name” type=”text” value=”” />
</p>
A number of ways exist to control the behavior of a model binder including the BindAttribute,
which is used in the Create method shown previously. This attribute is used to include or exclude
certain properties and to specify a prefix for the HTTP POST values. This can be very useful if
multiple objects in the POST collection need to be bound.
Model binders can also be used from within the action method to update existing instances of
your model classes using the UpdateModel and TryUpdateModel methods. The chief difference is
that TryUpdateModel will return a Boolean value indicating whether or not it was able to build a
successful model and UpdateModel will just throw an exception if it can’t. The Edit action method
shows this technique:
c#
[HttpPost]
public ActionResult Edit(int id, FormCollection formValues)
{
using (var db = new ProductsDataContext())
{
var product = db.Products.SingleOrDefault(x => x.ProductID == id);
if (TryUpdateModel(product))
{
db.SubmitChanges();
return RedirectToAction("Index");
}
return View(product);
}
}
Code snippet Controllers\ProductsController.cs
Vb
<HttpPost()>
Function Edit(ByVal id As Integer, ByVal formValues As FormCollection)
Using db As New ProductsDataContext
advanced MVC .
459
Dim product = db.Products.FirstOrDefault(Function(p As Product)
p.ProductID = id)
If TryUpdateModel(product) Then
db.SubmitChanges()
Return RedirectToAction("Index")
End If
Return View(product)
End Using
End Function
Code snippet Controllers\ProductsController.vb
areas
An area is a self-contained part of an MVC
application that manages its own models,
controllers, and views. You can even define
routes specific to an area. To create a new area,
select Add . Area from the project context
menu in the Solution Explorer. The Add Area
dialog, shown in Figure 21-7,
prompts you to provide a name for your area.
After you click Add, many new files are added to your project to
support the area. Figure 21-8 shows a project with two areas added
to it named Shop and Blog, respectively.
In addition to having its own controllers and views, each area has
a class called AreaNameAreaRegistration that inherits from the
abstract base class AreaRegistration. This class contains an
abstract property for the name of your area and an abstract method
for integrating your area with the rest of the application. The
default implementation registers the standard routes.
c#
public class BlogAreaRegistration : AreaRegistration
{
public override string AreaName
{
get
{
return "Blog";
}
}
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"Blog_default",
"Blog/{controller}/{action}/{id}",
fiGure 21-7
fiGure 21-8
460 .
chaPter 21 ASp.neT mVc
new { action = "Index", id = "" }
);
}
}
Code snippet Areas\Blog\BlogAreaRegistration.cs
Vb
Public Class BlogAreaRegistration
Inherits AreaRegistration
Public Overrides ReadOnly Property AreaName() As String
Get
Return "Blog"
End Get
End Property
Public Overrides Sub RegisterArea(ByVal context As AreaRegistrationContext)
context.MapRoute( _
"Blog_default", _
"Blog/{controller}/{action}/{id}", _
New With {.action = "Index", .id = ""} _
)
End Sub
End Class
Code snippet Areas\Blog\BlogAreaRegistration.vb
The RegisterArea method of the BlogAreaRegistration class defines a route
in which every URL is prefixed with /Blog/ by convention. This can be useful
while debugging routes but is not necessary as long as area routes do not clash
with any other routes.
In order to link to a controller which is inside another area, you need to use an overload of
Html.ActionLink that accepts a routeValues parameter. The object you provide for this parameter
must include an area property set to the name of the area which contains the controller you are
linking to.
c#
< %= Html.ActionLink("Blog", "Index", new { area = "Blog" }) % >
Vb
< %= Html.ActionLink("Blog", "Index", New With {.area = "Blog"})%>
One issue that is frequently encountered when adding area support to a project is that the controller
factory becomes confused when multiple controllers have the same name. To avoid this issue you
can limit the namespaces that a route will use to search for a controller to satisfy any request. The
advanced MVC .
461
following code snippet limits the namespaces for the global routes to MvcApplication.Controllers,
which will not match any of the area controllers.
c#
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = "" },
null,
new[] { "MvcApplication.Controllers" }
);
Code snippet Global.asax.cs
Vb
routes.MapRoute( _
"Default", _
"{controller}/{action}/{id}", _
New With {.controller = "Home", .action = "Index", .id = ""}, _
Nothing, _
New String() {"MvcApplication.Controllers"} _
)
Code snippet Global.asax.vb
The AreaRegistrationContext automatically includes the area namespace
when you use it to specify routes so you should only need to supply namespaces
to the global routes.
Validation
In addition to just creating or updating it, a model
binder is able to decide whether the model instance
that it operating on is valid. The results of this
decision are found in the ModelState property.
Model binders can pick up some simple validation
errors by default, usually with regard to incorrect
types. Figure 21 - 9 shows the result of attempting to
save a Product when the form is empty. Most of
these validation errors are based on the fact that
these properties are non-nullable value types and
require a value.
The user interface for this error report is provided
by the Html.ValidationSummary call, which is
made on the view. This helper method examines the
ModelState and if it finds any errors it renders them
as a list along with a header message. fiGure 21-9
462 .
chaPter 21 ASp.neT mVc
You can add additional validation hints to the properties of the model class by marking them up with
using the attributes in the System.ComponentModel.DataAnnotations assembly. Because the Product
class is created by LINQ to SQL you should not update it directly. The LINQ to SQL generated classes
are defined as partial so you can extend them but there is no easy way to attach metadata to the
generated properties this way. Instead, you need to create a metadata proxy class with the properties
you want to mark up, provide them with the correct data annotation attributes, and then mark up the
partial class with a MetadataTypeAttribute identifying the proxy class. The following code snippet
shows this technique being used to provide some validation metadata to the Product class:
c#
[MetadataType(typeof(ProductValidationMetadata))]
public partial class Product
{
}
public class ProductValidationMetadata
{
[Required, StringLength(256)]
public string Name { get; set; }
[Range(0, 100)]
public int DaysToManufacture { get; set; }
}
Code snippet Models\Product.cs
Vb
Imports System.ComponentModel.DataAnnotations
<MetadataType(GetType(ProductMetaData))>
Partial Public Class Product
End Class
Public Class ProductMetaData
<Required(), StringLength(256)>
Property Name As String
<Range(0, 100)>
Property DaysToManufacture As Integer
End Class
Code snippet Models\Product.vb
Now, attempting to create a new Product with no name
and a negative “Days to Manufacture” produces the errors
shown in Figure 21-10.
fiGure 21-10
Partial Views
At times you have large areas of user interface markup that you would like to reuse. In the ASP.NET
MVC framework a re - usable section of view is called a partial view. Partial views act very similar to
views except that they have an .ascx extension and inherit from System.Web.Mvc.ViewUserControl .
To create a partial view, check the Create a Partial View checkbox on the same Add View dialog that
you use to create other views.
To render a partial view you can use the Html.RenderPartial method. The most common
overload of this method accepts a view name and a model object. Just as with a normal view, a
partial view can be either controller - specifi c or shared. Once the partial view has been rendered,
its HTML markup is inserted into the main view. This code snippet renders a “ Form ” partial for
the current model:
c#
< % Html.RenderPartial("Form", Model); % >
Vb
< % Html.RenderPartial("Form", Model) % >
You might notice that along with the error report at the top of the page, for each
fi eld which has a validation error, the textbox is colored red and has an asterisk
after it. The fi rst effect is caused by the Html.TextBox helper, which accepts the
value of the property that it is attached to. If it encounters an error in the model
state for its attached property, it adds an input - validation - error CSS class to
the rendered INPUT control. The default stylesheet defi nes the red background.
The second effect is caused by the Html.ValidationMessage helper. This helper
is also associated with a property and renders the contents of its second