UNDERSTANDING THE TEMPLATE SEARCH ORDER

Một phần của tài liệu Giáo trình lập trình ASP.NET Apress pro ASP NET MVC3 framework pre release (Trang 519 - 536)

Our Role.cshtml template works because the MVC framework looks for custom templates for a given C# type before it uses one of the built-in templates. In fact, there is a very specific sequence that the MVC framework follows to find a suitable template:

1.The template passed to the helper – for example, Html.EditorFor(m =>

m.SomeProperty, "MyTemplate") would lead to MyTemplate being used.

2. Any template that is specified by metadata attributes, such as UIHint.

3. The template associated with any data type specified by metadata, such as the DataType attribute.

4. Any template that corresponds to the.NET class name of the data type being processed.

5. If the data type being processed is a simple type, then the built-in String template is used.

6. Any template that corresponds to the base classes of the data type.

7. If the data type implements IEnumerable, then the built-in Collection template will be used.

8.If all else fails, the Object template will be used – subject to the rule that scaffolding is not recursive.

Some of these steps rely on the built-in templates, which are described in Table 4. At each stage in the template search process, the MVC framework looks for a template called

EditorTemplates/<name> or DisplayTemplates/<name>. For our Role template, we satisfied step 4 in the search process – we created a template called Role.cshtml and placed it in the

~/Views/Shared/EditorTemplates folder.

Custom templates are found using the same search pattern as regular views – which means that we can create a controller-specific custom template and place it in the

~/Views/<controller>/EditorTemplates folder to override the templates found in the

~/Views/Shared folder. We explain more about the way that views are located in Chapter 15.

Creating a Custom Display Template

The process for creating a custom display template is similar to that for creating a custom editor, except that we place the templates in the DisplayTemplates folder. Listing 17 shows a custom display template for the Role enumeration - we have created this file as

~/Views/Shared/DisplayTemplates/Role.cshtml.

Listing 17. A custom display template

@model Role

@foreach (Role value in Enum.GetValues(typeof(Role))) { if (value == Model) {

<b>@value</b>

} else { @value }

}

This template lists all of the values in the Role enumeration and emphasizes the one that corresponds to the model value in bold. The effect of rendering a display for the Person.Role property is shown in Figure 13.

Figure 13. Using a custom display template

Creating a Generic Template

We are not limited to creating type-specific templates – we can, for example, create a template that works for all enumerations, and then specify that this template be selected using the UIHint attribute. If you look at the template search sequence in the Understanding the Template Search Order side-bar, you will see that templates specified using the UIHint attribute take precedence over type-specific ones. Listing 18 shows a template called Enum.cshtml and placed in the

~/Views/Shared/EditorTemplates folder. This template is a more general treatment for C#

enumerations.

Listing 18. A general enumeration editor template

@model Enum

@Html.DropDownListFor(m => m, Enum.GetValues(Model.GetType()) .Cast<Enum>()

.Select(m => {

string enumVal = Enum.GetName(Model.GetType(), m);

return new SelectListItem() {

Selected = (Model.ToString() == enumVal), Text = enumVal,

Value = enumVal };

}))

The view model type for this template is Enum, which allows us to work with any enumeration. We could have created the template using static HTML and Razor tags again, but we wanted to show that you can build templates using other HTML helper methods – in this case we have used the strongly-typed DropDownListFor helper and used some LINQ magic to transform the enumeration values into SelectListItems. Listing 19 shows the UIHint template applied to the Person.Role property.

Listing 19. Using the UIHint attribute to specify a custom template public partial class Person {

public int PersonId { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public DateTime BirthDate { get; set; } public Address HomeAddress { get; set; } public bool IsApproved { get; set; } [UIHint("Enum")]

public Role Role { get; set; } }

This approach gives a more general solution, but it lacks the automatic elegance that comes with the type-specific templates. It can be more convenient to have one template that you can apply widely – as long as you remember to apply the attributes correctly.

Replacing the Built-In Templates

If we create a custom template that has the same name as one of the built-in templates, the MVC framework will use the custom version in preference to the built-in one. Listing 20 shows a replacement for the Boolean template, which is used to render bool and bool? values. Table 4 lists the names of the other built-in templates and what they do.

Listing 20. Replacing a built-in template

@model bool?

@if (ViewData.ModelMetadata.IsNullableValueType && Model == null) { @:True False <b>Not Set</b>

} else if (Model.Value) { @:<b>True</b> False Not Set } else {

@:True <b>False</b> Not Set }

We have taken a different approach to the built-in Boolean template – all values are treated as though they are nullable and we simply highlight the value that corresponds to the model object. There is no compelling reason to do this, other than to demonstrate how to override a built-in template. The template in the listing is a display template, so we placed it in the ~/Views/Shared/DisplayTemplates folder.

Important When creating a template for a type that has a nullable equivalent, it is sensible to set the view model type to the nullable type – for example, in Listing 20, we have specified that the model type is bool? rather than bool. The MVC framework expects templates to cope with null values and an exception will be thrown if your model type doesn’t support this. We can determine if the view model object is nullable by reading the ViewData.ModelMetadata.IsNullableValueType property, as shown in the listing.

When we render a display for a bool or bool? value, our custom template is used – an example of the output can be seen in Figure 14.

Figure 14. Using a custom template to override a built-in template

Tip The search sequence for custom alternatives to the built-in templates follows the standard template search pattern. We placed our view in the ~/Views/Shared/DisplayTemplates folder, which

means that the MVC framework will use this template in any situation where the Boolean template is required. We can narrow the focus of our template to a single controller by placing it in

~/Views/<controller>/DisplayTempates instead.

Using the ViewData.TemplateInfo Property

The MVC framework provides the ViewData.TemplateInfo property to make writing custom view templates easier. The property returns a TemplateInfo object – the most useful members of this class are described in Table 5.

Table 5. Useful members of the TemplateInfo class

Member Description

FormattedModelValue Returns a string representation of the current model, taking into account formatting metadata such as the DataType attribute. See below for details.

GetFullHtmlFieldId() Returns a string that can be used in an HTML id attribute.

GetFullHmlFieldName() Returns a string that can be used in an HTML name attribute.

HtmlFieldPrefix Returns the field prefix – see below for details.

Respecting Data Formatting

Perhaps the most useful of the TemplateInfo properties is FormattedModelValue, which allows us to respect formatting metadata without having to detect and process the attributes ourselves.

Listing 20 shows a custom template called DateTime.cshtml that generates an editor for DateTime objects.

Listing 20. Taking formatting metadata into account in a custom view template

@model DateTime

@Html.TextBox("", ViewData.TemplateInfo.FormattedModelValue)

This is a very simple template – we just call the Html.TextBox helper and pass in the value from the ViewData.TemplateInfo.FormattedModelValue property. If we apply the DataType attribute to the BirthDate property from our Person model class, like this:

...

[DataType(DataType.Date)]

public DateTime BirthDate { get; set; } ...

then the value returned from the FormattedModelValue property will be just the date component of the DateTime value. The output from the template is shown in Figure 15.

Figure 15. Using the FormattedModelValue property to take formatting metadata into account

Working with HTML Prefixes

When we render a hierarchy of views, the MVC framework keeps track of the names of the properties that we are rendering and provides is with a unique reference point through the HtmlFieldPrefix property of the TemplateInfo object. This is especially useful when we are processing nested objects, such as the HomeAddress property of our Person class, which is an Address object. If we render a template for a property like this:

@Html.EditorFor(m => m.HomeAddress.PostalCode)

then the HtmlFieldPrefix value that is passed to the template will be

HomeAddress.PostalCode. We can use this information to ensure that the HTML elements we generate can be uniquely identified – the usual way of doing this is through the id and name attributes. The value returned by the HtmlFieldPrefix property will often not be useable directly as an attribute value, so the TemplateInfo object contains the GetFullHtmlFieldId and

GetFullHtmlFieldName methods to convert the unique ID into something we can use. The value of the HTML prefix will be apparent when we look at model binding in Chapter 17.

Passing Additional Metadata to a Template

On occasion, we want to provide additional direction to our templates that we cannot express using the built-in attributes. We can do this using the AdditionalMetadata attribute, as shown in Listing 21.

Listing 21. Using the AdditionalMetadata attribute ...

[AdditionalMetadata("RenderList", "true")]

public bool IsApproved { get; set; } ...

We have applied the AdditionalMetadata attribute to the IsApproved property – this attribute takes key/value parameters for the information we want to pass along. In this example, we have defined a key RenderList that we will use to specify whether the editor for a bool property should be a dropdown list (RenderList is true) or a textbox (RenderList is false). We detect these values through the ViewData property in our template as the editor template Boolean.cshtml shows in Listing 22

Listing 22. Using AdditionalMetadata values

@model bool?

@{

bool renderList = true;

if (ViewData.ModelMetadata.AdditionalValues.ContainsKey("RenderList")) { renderList =

bool.Parse(ViewData.ModelMetadata.AdditionalValues["RenderList"].ToString());

} }

@if (renderList) {

SelectList list = ViewData.ModelMetadata.IsNullableValueType ? new SelectList(new [] {"True", "False", "Not Set"}, Model) : new SelectList(new [] {"True", "False"}, Model);

@Html.DropDownListFor(m => m, list)

} else {

@Html.TextBoxFor(m => m) }

We can access the key/value pairs from the attribute through the

ViewData.ModelMetadata.AdditionalValues collection, as shown in bold in the listing. It is important not to assume that the additional values you are checking for are present. This is especially true when replacing a built-in template – it can be hard to determine in advance which model objects your template will be used for.

Understanding the Metadata Provider System

The metadata examples we have shown you so far have relied on the

DataAnnotationsModelMetadataProvider class – this is the class that has been detecting and

processing the attributes we have been adding to our classes so that the templates and formatting options are taken care of.

Underlying the model metadata system is the ModelMetadata class, which contains a number of properties that specify how a model or property should be rendered. The

DataAnnotationsModelMetadata processes the attributes we have applied and sets value for the properties in a ModelMetadata object – this is then passed to the template system for processing.

Table 6 shows the most useful properties of the ModelMetadata class.

Table 6. Useful members of the ModelMetadata class

Member Description

DataTypeName Provides information about the meaning of the data item – this is set from the value of the DataType attribute.

DisplayFormatString Returns a composite formatting string, such as {0:2}. This value can be set directly using the DisplayFormat attribute (by providing a value for the DataFormatting property) or indirectly by other attributes – for example, the DataType.Currency value passed to the DataType attribute results in a formatting string that produces two decimal places and a currency symbol.

DisplayName Returns a human-readable name for the data item – this is set using the Display or DisplayName attributes.

EditFormatString The editor equivalent of DisplayFormatString.

HideSurroundingHtml Returns true if the HTML element should be hidden – this is set to be true by using the HiddenInput attribute with a DisplayValue value of false.

Model Returns the model object that is being processed

NullDisplayText Returns the string that should be displayed when the value is null.

ShowForDisplay Returns true if the items should be included in display (as opposed to editing) scaffolding. Set using the ScaffoldColumn attribute.

ShowForEdit Returns true if the items should be included in editing (as opposed to display) scaffolding. Set using the ScaffoldColumn attribute.

TemplateHint Returns the name of the template that should be used to render the item. Set using the UIHint attribute.

The DataAnnotationsModelMetadataProvider class sets the values in Table 6 based on the attributes we have applied – the name of this class comes from the fact that most (but not all) of these attributes are from the System.ComponentModel.DataAnnotations namespace.

Creating a Custom Model Metadata Provider

We can create a custom model metadata provider if the data annotations system doesn’t suit our needs. Providers must be derived from the abstract ModelMetadataProvider class, which is shown in Listing 23.

Listing 23. The ModelMetadataProvider class namespace System.Web.Mvc {

using System.Collections.Generic;

public abstract class ModelMetadataProvider {

public abstract IEnumerable<ModelMetadata> GetMetadataForProperties(

object container, Type containerType);

public abstract ModelMetadata GetMetadataForProperty(

Func<object> modelAccessor, Type containerType, string propertyName);

public abstract ModelMetadata GetMetadataForType(

Func<object> modelAccessor, Type modelType);

} }

If you are confident using .NET reflection, then We you can implement each of these methods to create a custom provider. A simpler approach is to derive a class from

AssociatedMetadataProvider, which takes care of the reflection for us and only requires us to implement a single method. Listing 24 shows a skeletal metadata provider created this way.

Listing 24. A skeletal custom model metadata provider

public class CustomModelMetadataProvider : AssociatedMetadataProvider{

protected override ModelMetadata CreateMetadata(

IEnumerable<Attribute> attributes, Type containerType,

Func<object> modelAccessor, Type modelType,

string propertyName) { //...implementation goes here...

}

}

This CreateMetadata method will be called by the MVC framework for each model object or property that is being rendered. The parameters are described in Table 7 – our descriptions are for when an individual property is being rendered as this is the most frequent use, but if we render a scaffolding template the method will also be called for the view model itself – in our examples, this would mean a call for a Person object and then for each of the properties defined by the Person class.

Table 7. The parameters for the CreateMetadata method

Parameter Description

attributes The set of attributes that are applied to the property containerType The type of the object that contains the current property modelAccessor A Func that returns the property value

modelType The type of the current property

propertyName The name of the current property

We are free to implement any metadata policy we choose – Listing 25 shows a simple approach that formats the name of certain properties.

Listing 25. A simple custom model metadata provider

public class CustomModelMetadataProvider : AssociatedMetadataProvider{

protected override ModelMetadata CreateMetadata(

IEnumerable<Attribute> attributes, Type containerType,

Func<object> modelAccessor, Type modelType,

string propertyName) {

ModelMetadata metadata = new ModelMetadata(this, containerType, modelAccessor, modelType, propertyName);

if (propertyName != null && propertyName.EndsWith("Name")) {

metadata.DisplayName = propertyName.Substring(0, propertyName.Length - 4);

}

return metadata;

} }

The first part of our implementation creates the ModelMetadata object that we must return from the CreateMetadata method – the constructor parameters for ModelMetadata line up nicely with the method parameters. Once we have created the result object, we can express our policy by setting the properties we described in Table 6. In our simple example, if the name of the property ends with Name, we remove the last four characters of the name – so that FirstName

becomes First, for example.

We register our provider in the Application_Start method of Global.asax, as shown in Listing 26.

Listing 26. Registering a custom model metadata provider protected void Application_Start() {

AreaRegistration.RegisterAllAreas();

ModelMetadataProviders.Current = new CustomModelMetadataProvider();

RegisterGlobalFilters(GlobalFilters.Filters);

RegisterRoutes(RouteTable.Routes);

}

We set the value of the static ModelMetadataProviders.Current property to an instance of our custom class. The MVC framework only supports one provider, so our metadata is the only metadata that will be used. The effect of our provider is shown in Figure 16, where we have used the Html.EditorForModel helper on a Person object.

Figure 16. The effect of a custom model metadata provider

Customizing the Data Annotations Model Metadata Provider

The problem with custom model metadata providers is that we lose the benefits of the data annotations metadata. If you look at Figure 16, you can see that the HTML we have generated has some the same problems that we started the chapter with – the PersonId property is visible and can be edited and the BirthDate is displayed with a time. It is not all bad – our custom templates are still used – for example, the editor for the Role property is a dropdown list.

If we want to implement a custom policy and we want to take advantage of the data annotations attributes, then we can derive our custom metadata provider from

DataAnnotationsModelMetadataProvider. This class is derived from

AssociatedMetadataProvider, so we only have to override the CreateMetadata method, as shown in Listing 27.

Listing 27. Deriving a custom metadata provider from AssociatedMetadataProvider public class CustomModelMetadataProvider : DataAnnotationsModelMetadataProvider { protected override ModelMetadata CreateMetadata(

IEnumerable<Attribute> attributes, Type containerType,

Func<object> modelAccessor, Type modelType,

string propertyName) {

ModelMetadata metadata = base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName);

if (propertyName != null && propertyName.EndsWith("Name")) {

metadata.DisplayName = propertyName.Substring(0, propertyName.Length - 4);

}

return metadata;

} }

The significant change is that we call the base implementation of the CreateMetadata method to get a ModelMetadata object that takes into account the data annotations attributes.

We can then apply our custom policy to override the values defined by the

DataAnnotationsModelMetadataProvider class – we get the benefits of the attribute system and the flexibility of a custom policy. The effect on our Person editor is shown in Figure 17.

Figure 17. Combining the data annotations metadata with a custom policy

Summary

In this chapter we have shown you the system of model templates that are accessible through the templated view helper methods. It can take a little while to set up the templates, metadata data and providers you need, but creating views is a simpler and more convenient process once these are in place.

n n n nn n

Model Binding

Model binding is the process of creating .NET objects using the data sent to useuseby the browser in an HTTP request. We have been relying on the model binding process each time we have defined an action method that takes a parameter – the parameter objects are created by model binding. In this chapter, we’ll show you how the model binding system works and demonstrate the techniques required to customize it for advanced use.

Understanding Model Binding

Imagine that we have defined an action method in a controller as shown in Listing 1.

Listing 1. A simple action method using System;

using System.Web.Mvc;

using MvcApp.Models;

namespace MvcApp.Controllers {

public class HomeController : Controller {

public ViewResult Person(int id) { // get a person record from the repository

Person myPerson = null; //...retrieval logic goes here...

return View(myPerson);

} } }

Our action method is defined in the HomeController class, which means that the default route that Visual Studio creates for us will let us query invoke our action method. As a

reminder, here is the default route:

Một phần của tài liệu Giáo trình lập trình ASP.NET Apress pro ASP NET MVC3 framework pre release (Trang 519 - 536)

Tải bản đầy đủ (PDF)

(603 trang)