1. Trang chủ
  2. » Công Nghệ Thông Tin

Pro ASP.NET MVC Framework phần 8 docx

56 1,4K 0

Đang tải... (xem toàn văn)

Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống

THÔNG TIN TÀI LIỆU

Thông tin cơ bản

Định dạng
Số trang 56
Dung lượng 16,26 MB

Nội dung

This permits an elegant way of unit testing your model binding. Unit tests can run the a ction method, supplying a F ormCollection c ontaining 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 proper- ties, 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 prop- erty, 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 ex ceptions . I n .NET , ex ceptions ar e 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 parame- ters 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) { // } CHAPTER 11 ■ DATA ENTRY376 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 col- lection 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 r eaches some index that doesn ’ t corr espond 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 pr efix people, as sho wn in the follo wing 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 differ ent way of using incoming data to populate a particu- lar 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 for m field, y ou need a very different binding strategy. It wouldn’t make sense to let CHAPTER 11 ■ DATA ENTRY378 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. p ublic 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 This isn’t as complex as it initially appears. All that a custom binder needs to do is accept a M odelBindingContext , which provides both the M odelName ( the name of the parameter or pre- fix being bound) and a ValueProvider from which you can receive incoming data. The binder should ask the value provider for the raw incoming data, and can then attempt to parse the data. If the binding context provides an existing model object, then you should update that instance; otherwise, return a new instance. Configuring Which Model Binders Are Used The MVC Framework won’t use your new custom model binder unless you tell it to do so. If you own the source code to XDocument, you could associate your binder with the XDocument type by applying an attribute as follows: [ModelBinder(typeof(XDocumentBinder))] public class XDocument { // } This attribute tells the MVC Framework that whenever it needs to bind an XDocument, it should use your custom binder class, XDocumentBinder. However, you probably don’t own the source code to XDocument, so you need to use one of the following two alternative configura- tion mechanisms instead. The first option is to register your binder with ModelBinders.Binders. You only need to do this once, during application initialization. For example, in Global.asax.cs, add the following: protected void Application_Start() { RegisterRoutes(RouteTable.Routes); ModelBinders.Binders.Add(typeof(XDocument), new XDocumentBinder()); } The second option is to specify which model binder to use on a case-by-case basis. When binding action method parameters, you can use [ModelBinder], as follows: public ActionResult MyAction([ModelBinder(typeof(XDocumentBinder))] XDocument xml) { // } U nfor tunately , if y ou ’ re invoking model binding explicitly, it’s somewhat messier to spec- ify a particular model binder, because for some reason UpdateModel() has no overload to let y ou do so. Here’s a utility method that you might want to add to your controller: private void UpdateModelWithCustomBinder(object model, string prefix, IModelBinder binder, string include, string exclude) { var modelType = model.GetType(); var bindAttribute = new BindAttribute { Include = include, Exclude = exclude }; var bindingContext = new ModelBindingContext { Model = model, CHAPTER 11 ■ DATA ENTRY380 10078ch11.qxd 3/26/09 12:13 PM Page 380 ModelType = modelType, ModelName = prefix, ModelState = ModelState, ValueProvider = ValueProvider, PropertyFilter = (propName => bindAttribute.IsPropertyAllowed(propName)) }; binder.BindModel(ControllerContext, bindingContext); if (!ModelState.IsValid) throw new InvalidOperationException("Error binding " + modelType.FullName); } With this, you can now easily invoke your custom binder, as follows: public ActionResult MyAction() { var doc = new XDocument(); UpdateModelWithCustomBinder(doc, "xml", new XDocumentBinder(), null, null); // } So, there are several ways of nominating a model binder. How does the framework resolve conflicting settings? It selects model binders according to the following priority order: 1. The binder explicitly specified for this binding occasion (e.g., if you’re using a [ModelBinder] attribute on an action method parameter). 2. The binder registered in ModelBinders.Binders for the target type. 3. The binder assigned using a [ModelBinder] attribute on the target type itself. 4. The default model binder. Usually, this is DefaultModelBinder, but you can change that by assigning an IModelBinder instance to ModelBinders.Binders.DefaultBinder. Configure this during application initialization—for example, in Global.asax.cs’s Application_Start() method. ■Tip Specifying a model binder on a case-by-case basis (i.e., option 1) makes most sense when you’re more concerned about the incoming da ta format than about what .NET type it needs to map to. For example, you might sometimes receive data in JSON format, in which case it makes sense to create a JSON binder tha t can construct .NET objects of arbitrary type. You wouldn’t register that binder globally for any particular model type, but would just nominate it for certain binding occasions. Using M odel Binding to Receiv e File Uploads Remember that in S portsStore, in Chapter 5, we used a custom model binder to supply Cart instances to certain action methods? The action methods didn’t need to know or care where the Cart instances came from—they just appeared as method parameters. CHAPTER 11 ■ DATA ENTRY 381 10078ch11.qxd 3/26/09 12:13 PM Page 381 ASP.NET MVC takes a similar approach to let your action methods receive uploaded files. A ll you have to do is accept a method parameter of type H ttpPostedFileBase , and ASP.NET MVC will populate it (where possible) with data corresponding to an uploaded file. ■Note Behind the scenes, this is implemented as a custom model binder called HttpPostedFileBaseModelBinder. The framework registers this by default in ModelBinders.Binders. For example, to let the user upload a file, add to one of your views a <form> like this: <form action="<%= Url.Action("UploadPhoto") %>" method="post" enctype="multipart/form-data"> Upload a photo: <input type="file" name="photo" /> <input type="submit" /> </form> You can then retrieve and work with the uploaded file in the action method: public ActionResult UploadPhoto(HttpPostedFileBase photo) { // Save the file to disk on the server string filename = // pick a filename photo.SaveAs(filename); // or work with the data directly byte[] uploadedBytes = new byte[photo.ContentLength]; photo.InputStream.Read(uploadedBytes, 0, photo.ContentLength); // now do something with uploadedBytes } ■Note The previous example showed a <form> tag with an attribute you may find unfamiliar: enctype="multipart/form-data". This is necessar y for a successful upload! Unless the form has this attribute, the browser won’t actually upload the file—it will just send the name of the file instead, and the Request.Files collection will be empty. (This is how browsers work; ASP.NET MVC can’t do anything about it.) Similarly, the form must be submitted as a POST request (i.e. method="post"); otherwise, it will contain no files. In this example, I chose to render the <form> ta g by writing it out as literal HTML. Alternatively, you can genera te a <form> ta g with an enctype a ttribute by using Html.BeginForm(), but only by using the four - parameter overload tha t takes a parameter called htmlAttributes. P ersonally , I think literal HTML is more readable than sending so many parameters to Html.BeginForm(). CHAPTER 11 ■ DATA ENTRY382 10078ch11.qxd 3/26/09 12:13 PM Page 382 Validation W hat is validation? For many developers, it’s a mechanism to ensure that incoming data con- forms to certain patterns. (e.g., that an e-mail address is of the form x@y.z, or that customer names are less than a certain length). But what about saying that usernames must be unique, o r that appointments can’t be booked on national holidays—are those validation rules, or are they business rules? There’s a fuzzy boundary between validation rules and business rules, if in fact there is any boundary at all. In MVC architecture, the responsibility for maintaining and enforcing all of these rules lies in your model layer. After all, they are rules about what you deem permissible in your business domain (even if it’s just your definition of a suitably complex password). The ability to define all kinds of business rules in one place, detached from any particular UI technology, is a key benefit of MVC design. It leads to simpler and more robust applications, as compared to spreading and duplicating your rules across all your different UI screens. This is an example of the don’t repeat yourself principle. ASP.NET MVC doesn’t have any opinion about how you should implement your domain model. That’s because a plain .NET class library project combined with all the technologies in the .NET ecosystem (such as your choice of database access technology) gives you a huge range of options. So, it would be wildly inappropriate for ASP.NET MVC to interfere with your model layer by forcing you to use some specific validation rules engine. Thankfully, it doesn’t: you can implement your rules however you like. Plain C# code works well! What the MVC Framework is concerned with, however, is helping you to present a UI and interact with users over HTTP. To help you tie your business rules into the overall request pro- cessing pipeline, there’s a convention regarding how you should tell ASP.NET MVC about errors you’ve detected, so that view templates can display them to the user. Over the next few pages, you’ll see how this convention works though simple examples of enforcing validation rules directly within controller code. Later, you’ll see how to move valida- tion rules into your application’s model layer, consolidating them with arbitrarily complex business rules and database constraints—eliminating code repetition while still fitting into ASP.NET MVC’s convention for reporting errors. ■Note In previous chapters, you saw a way of implementing validation using an interface called IDataErrorInfo. That’s just one special case within ASP.NET MVC’s error reporting convention, so we’ll ignore it for now, explore the underlying mechanism, and then come back to IDataErrorInfo later. Registering Errors in ModelState As you learned earlier in this chapter, the MVC Framework’s model binding system uses ModelState as a tempor ar y stor age area. ModelState stor es both incoming attempted v alues and details of any binding errors. You can also manually register errors in ModelState, which is CHAPTER 11 ■ DATA ENTRY 383 10078ch11.qxd 3/26/09 12:13 PM Page 383 how to communicate error information to views, and is also how input controls can recover t heir previous state after a validation or model binding failure. Here’s an example. You’re creating a controller called BookingController, which lets users book appointments. Appointments are modeled as follows: public class Appointment { public string ClientName { get; set; } public DateTime AppointmentDate { get; set; } } To place a booking, users first visit BookingController’s MakeBooking action: public class BookingController : Controller { [AcceptVerbs(HttpVerbs.Get)] public ActionResult MakeBooking() { return View(); } } This action does nothing more than render its default view, MakeBooking.aspx, which includes the following form: <h1>Book an appointment</h1> <% using(Html.BeginForm()) { %> <p> Your name: <%= Html.TextBox("appt.ClientName") %> </p> <p> Appointment date: <%=Html.TextBox("appt.AppointmentDate", DateTime.Now.ToShortDateString()) %> </p> <p> <%= Html.CheckBox("acceptsTerms") %> <label for="acceptsTerms">I accept the Terms of Booking</label> </p> <input type="submit" value="Place booking" /> <% } %> Notice that the text boxes have names corresponding to properties on Appointment. That will help with model binding in a moment. Altogether, the MakeBooking() method renders the scr een sho wn in F igure 11-1. CHAPTER 11 ■ DATA ENTRY384 10078ch11.qxd 3/26/09 12:13 PM Page 384 [...]... nonnullable property In this case, it’s because DateTime is a value type and can’t hold null Fortunately, users will rarely see such messages if you prepopulate the field with a default value and provide a date picker control Users are even less likely to see the built-in messages if you also use client-side validation, as discussed shortly 387 10078ch11.qxd 388 3/26/09 12:13 PM Page 388 CHAPTER 11... applied a value to a property on a custom model object Checks whether the model object implements IDataErrorInfo If so, queries its this[propertyName] indexed property to find any property-level error message and registers any nonempty value as an error in ModelState 389 10078ch11.qxd 390 3/26/09 12:13 PM Page 390 CHAPTER 11 I DATA ENTRY Also, if there are any parsing exceptions or property setter exceptions... "." + key, value); } } I If you’re keeping RuleException in your domain model project and don’t want to have a reference Tip from that project to System.Web .Mvc. dll, then you won’t be able to reference the ModelStateDictionary type directly from RuleException Instead, consider implementing CopyToModelState() in your MVC project as an extension method on RuleException If you don’t want to hard-code... in ModelState OnPropertyValidating Runs each time DefaultModelBinder is about to apply a value to a property on a custom model object Returns a bool value to specify whether the value should be applied If the property type doesn’t allow null values and the incoming value is null, registers an error in ModelState and blocks the value by returning false Otherwise, just returns true OnPropertyValidated... any awards for elegance or clarity I’ll soon describe a tidier way of doing this, but for now I’m just trying to demonstrate the most basic way of registering validation errors 385 10078ch11.qxd 386 3/26/09 12:13 PM Page 386 CHAPTER 11 I DATA ENTRY I Note I’ve included DateTime in this example so that you can see that it’s a tricky character to deal with It’s a value type, so the model binder will... doesn’t provide any means of reporting multiple errors relating to a single property, or multiple errors relating to the whole model object, other than concatenating all the messages into a single string DefaultModelBinder only attempts to apply a value to a property when some matching key/value pair is included in the request It could be possible for someone to bypass validation on a particular property... to update the values of all properties on a custom model object Returns a bool value to specify whether binding should be allowed to proceed Does nothing—just returns true OnModelUpdated Runs after DefaultModelBinder has tried to update the values of all properties on a custom model object Checks whether the model object implements IDataErrorInfo If so, queries its Error property to find any object-level... solution from being used repeatedly (this is known as a replay attack) 407 10078ch11.qxd 4 08 3/26/09 12:13 PM Page 4 08 CHAPTER 11 I DATA ENTRY Creating an HTML Helper Method Let’s start by creating an HTML helper method that will display a CAPTCHA test Create a new static class called CaptchaHelper anywhere in your web application project (e.g., in a folder called /Helpers), and add the following code As... priority: 1 Previously attempted value recorded in ModelState["name"].Value.AttemptedValue 2 Explicitly provided value (e.g., ) 3 ViewData, by calling ViewData.Eval("name") (so ViewData["name"] takes precedence over ViewData.Model.name) 10078ch11.qxd 3/26/09 12:13 PM Page 389 CHAPTER 11 I DATA ENTRY Since model binders record all attempted values in ModelState, regardless... helper simply produces a bulleted list of errors recorded in ModelState If you submit a blank form, you’ll now get the output shown in Figure 11-2 This screenshot uses CSS styles to highlight error messages and the input controls to which they correspond—you’ll learn how to do that in a moment Figure 11-2 Validation messages rendered by Html.ValidationSummary 10078ch11.qxd 3/26/09 12:13 PM Page 387 CHAPTER . 11 ■ DATA ENTRY 380 10078ch11.qxd 3/26/09 12:13 PM Page 380 ModelType = modelType, ModelName = prefix, ModelState = ModelState, ValueProvider = ValueProvider, PropertyFilter = (propName =>. just appeared as method parameters. CHAPTER 11 ■ DATA ENTRY 381 10078ch11.qxd 3/26/09 12:13 PM Page 381 ASP. NET MVC takes a similar approach to let your action methods receive uploaded files. A ll. principle. ASP. NET MVC doesn’t have any opinion about how you should implement your domain model. That’s because a plain .NET class library project combined with all the technologies in the .NET ecosystem

Ngày đăng: 06/08/2014, 08:22

TỪ KHÓA LIÊN QUAN