Thư viện tri thức trực tuyến
Kho tài liệu với 50,000+ tài liệu học thuật
© 2023 Siêu thị PDF - Kho tài liệu học thuật hàng đầu Việt Nam

Pro ASP.NET MVC Framework phần 8 docx
Nội dung xem thử
Mô tả chi tiết
This permits an elegant way of unit testing your model binding. Unit tests can run the
action method, supplying a FormCollection containing test data, with no need to supply a
mock or fake request context. It’s a pleasingly “functional” style of code, meaning that the
method acts only on its parameters and doesn’t touch external context objects.
Dealing with Model-Binding Errors
Sometimes users will supply values that can’t be assigned to the corresponding model properties, such as invalid dates, or text for int properties. To understand how the MVC Framework
deals with such errors, consider the following design goals:
• User-supplied data should never be discarded outright, even if it is invalid. The
attempted value should be retained so that it can reappear as part of a validation error.
• When there are multiple errors, the system should give feedback about as many errors
as it can. This means that model binding cannot bail out when it hits the first problem.
• Binding errors should not be ignored. The programmer should be guided to recognize
when they’ve happened and provide recovery code.
To comply with the first goal, the framework needs a temporary storage area for invalid
attempted values. Otherwise, since invalid dates can’t be assigned to a .NET DateTime property, invalid attempted values would be lost. This is why the framework has a temporary
storage area known as ModelState. ModelState also helps to comply with the second goal:
each time the model binder tries to apply a value to a property, it records the name of the
property, the incoming attempted value (always as a string), and any errors caused by the
assignment. Finally, to comply with the third goal, if ModelState has recorded any errors, then
UpdateModel() finishes by throwing an InvalidOperationException saying “The model of type
typename was not successfully updated.”
So, if binding errors are a possibility, you should catch and deal with the exception—for
example,
public ActionResult RegisterMember()
{
var person = new Person();
try
{
UpdateModel(person);
// ... now do something with person
}
catch (InvalidOperationException ex)
{
// Todo: Provide some UI feedback based on ModelState
}
}
This is a fairly sensible use of exceptions. In .NET, exceptions are the standard way to
signal the inability to complete an operation (and are not reserved for critical, infrequent, or
CHAPTER 11 ■ DATA ENTRY 375
10078ch11.qxd 3/26/09 12:13 PM Page 375
“exceptional” events, whatever that might mean).2 However, if you prefer not to deal with an
exception, you can use TryUpdateModel() instead. It doesn’t throw an exception, but returns a
bool status code—for example,
public ActionResult RegisterMember()
{
var person = new Person();
if(TryUpdateModel(person))
{
// ... now do something with person
}
else
{
// Todo: Provide some UI feedback based on ModelState
}
}
You’ll learn how to provide suitable UI feedback in the “Validation” section later in this
chapter.
■Note When a certain model property can’t be bound because the incoming data is invalid, that doesn’t
stop DefaultModelBinder from trying to bind the other properties. It will still try to bind the rest, which
means that you’ll get back a partially updated model object.
When you use model binding implicitly—i.e., receiving model objects as method parameters rather than using UpdateModel() or TryUpdateModel()—then it will go through the same
process but it won’t signal problems by throwing an InvalidOperationException. You can
check ModelState.IsValid to determine whether there were any binding problems, as I’ll
explain in more detail shortly.
Model-Binding to Arrays, Collections, and Dictionaries
One of the best things about model binding is how elegantly it lets you receive multiple data items
at once. For example, consider a view that renders multiple text box helpers with the same name:
Enter three of your favorite movies: <br />
<%= Html.TextBox("movies") %> <br />
<%= Html.TextBox("movies") %> <br />
<%= Html.TextBox("movies") %>
Now, if this markup is in a form that posts to the following action method:
public ActionResult DoSomething(List<string> movies)
{
// ...
}
376 CHAPTER 11 ■ DATA ENTRY
2. When you run in Release mode and don’t have a debugger attached, .NET exceptions rarely cause any
measurable performance degradation, unless you throw tens of thousands of exceptions per second.
10078ch11.qxd 3/26/09 12:13 PM Page 376
then the movies parameter will contain one entry for each corresponding form field.
Instead of List<string>, you can also choose to receive the data as a string[] or even an
IList<string>—the model binder is smart enough to work it out. If all of the text boxes were
called myperson.Movies, then the data would automatically be used to populate a Movies collection property on an action method parameter called myperson.
Model-Binding Collections of Custom Types
So far, so good. But what about when you want to bind an array or collection of some custom
type that has multiple properties? For this, you’ll need some way of putting clusters of related
input controls into groups—one group for each collection entry. DefaultModelBinder expects
you to follow a certain naming convention, which is best understood through an example.
Consider the following view template:
<% using(Html.BeginForm("RegisterPersons", "Home")) { %>
<h2>First person</h2>
<div>Name: <%= Html.TextBox("people[0].Name") %></div>
<div>Email address: <%= Html.TextBox("people[0].Email")%></div>
<div>Date of birth: <%= Html.TextBox("people[0].DateOfBirth")%></div>
<h2>Second person</h2>
<div>Name: <%= Html.TextBox("people[1].Name")%></div>
<div>Email address: <%= Html.TextBox("people[1].Email")%></div>
<div>Date of birth: <%= Html.TextBox("people[1].DateOfBirth")%></div>
...
<input type="submit" />
<% } %>
Check out the input control names. The first group of input controls all have a [0] index
in their name; the second all have [1]. To receive this data, simply bind to a collection or array
of Person objects, using the parameter name people—for example,
public ActionResult RegisterPersons(IList<Person> people)
{
// ...
}
Because you’re binding to a collection type, DefaultModelBinder will go looking for groups
of incoming values prefixed by people[0], people[1], people[2], and so on, stopping when it
reaches some index that doesn’t correspond to any incoming value. In this example, people
will be populated with two Person instances bound to the incoming data.
It works just as easily with explicit model binding. You just need to specify the binding
prefix people, as shown in the following code:
public ActionResult RegisterPersons()
{
var mypeople = new List<Person>();
UpdateModel(mypeople, "people");
// ...
}
CHAPTER 11 ■ DATA ENTRY 377
10078ch11.qxd 3/26/09 12:13 PM Page 377
■Note In the preceding view template example, I wrote out both groups of input controls by hand for clarity.
In a real application, it’s more likely that you’ll generate a series of input control groups using a <% for(...)
{ %> loop. You could encapsulate each group into a partial view, and then call Html.RenderPartial() on
each iteration of your loop.
Model-Binding to a Dictionary
If for some reason you’d like your action method to receive a dictionary rather than an array or
a list, then you have to follow a modified naming convention that’s more explicit about keys
and values—for example,
<% using(Html.BeginForm("RegisterPersons", "Home")) { %>
<h2>First person</h2>
<input type="hidden" name="people[0].key" value="firstKey" />
<div>Name: <%= Html.TextBox("people[0].value.Name")%></div>
<div>Email address: <%= Html.TextBox("people[0].value.Email")%></div>
<div>Date of birth: <%= Html.TextBox("people[0].value.DateOfBirth")%></div>
<h2>Second person</h2>
<input type="hidden" name="people[1].key" value="secondKey" />
<div>Name: <%= Html.TextBox("people[1].value.Name")%></div>
<div>Email address: <%= Html.TextBox("people[1].value.Email")%></div>
<div>Date of birth: <%= Html.TextBox("people[1].value.DateOfBirth")%></div>
...
<input type="submit" />
<% } %>
When bound to a Dictionary<string, Person> or IDictionary<string, Person>, this
form data will yield two entries, under the keys firstKey and secondKey, respectively. You
could receive the data as follows:
public ActionResult RegisterPersons(IDictionary<string, Person> people)
{
// ...
}
Creating a Custom Model Binder
You’ve learned about the rules and conventions that DefaultModelBinder uses to populate
arbitrary .NET types according to incoming data. Sometimes, though, you might want to
bypass all that and set up a totally different way of using incoming data to populate a particular object type. To do this, implement the IModelBinder interface.
For example, if you want to receive an XDocument object populated using XML data from
a hidden form field, you need a very different binding strategy. It wouldn’t make sense to let
378 CHAPTER 11 ■ DATA ENTRY
10078ch11.qxd 3/26/09 12:13 PM Page 378
DefaultModelBinder create a blank XDocument, and then try to bind each of its properties, such
as FirstNode, LastNode, Parent, and so on. Instead, you’d want to call XDocument’s Parse()
method to interpret an incoming XML string. You could implement that behavior using the
following class, which can be put anywhere in your ASP.NET MVC project.
public class XDocumentBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext,
ModelBindingContext bindingContext)
{
// Get the raw attempted value from the value provider
string key = bindingContext.ModelName;
ValueProviderResult val = bindingContext.ValueProvider[key];
if ((val != null) && !string.IsNullOrEmpty(val.AttemptedValue)) {
// Follow convention by stashing attempted value in ModelState
bindingContext.ModelState.SetModelValue(key, val);
// Try to parse incoming data
string incomingString = ((string[])val.RawValue)[0];
XDocument parsedXml;
try {
parsedXml = XDocument.Parse(incomingString);
}
catch (XmlException) {
bindingContext.ModelState.AddModelError(key, "Not valid XML");
return null;
}
// Update any existing model, or just return the parsed XML
var existingModel = (XDocument)bindingContext.Model;
if (existingModel != null) {
if (existingModel.Root != null)
existingModel.Root.ReplaceWith(parsedXml.Root);
else
existingModel.Add(parsedXml.Root);
return existingModel;
}
else
return parsedXml;
}
// No value was found in the request
return null;
}
}
CHAPTER 11 ■ DATA ENTRY 379
10078ch11.qxd 3/26/09 12:13 PM Page 379