Extending Models

The model system in MVC has several extensible pieces, including the ability to describe models with metadata, to validate models, and to influence how models are constructed from the request data. We have a sample for each of these extensibility points within the system.

Turning Request Data into Models

The process of turning request data (such as form data, query string data, or even routing information) into models is called model binding. Model binding really happens in two phases:

  • Understanding where data comes from (through the use of value providers)
  • Creating/updating model objects with those values (through the use of model binders).

Exposing Request Data with Value Providers

When your MVC application participates in model binding, the values that are used for the actual model binding process come from value providers. The purpose of a value provider is simply to provide access to information that is eligible to be used in model binding. The MVC framework ships with several value providers which can provide data from the following sources:

  • Explicit values for child actions (RenderAction)
  • Form values
  • JSON data from XMLHttpRequest
  • Route values
  • Query string values
  • Uploaded files

Value providers come from value provider factories, and the system searches for data from those value providers in their registered order (the preceding list is the order that is used by default, top first to bottom last). Developers can write their own value provider factories and value providers, and insert them into the factory list contained inside ValueProviderFactories.Factories. Developers choose to implement a value provider factory and value provider when they need to provide an additional source of data to be used during model binding.

In addition to the value provider factories included in MVC itself, the team also included several provider factories and value providers in the ASP.NET MVC 3 Futures package, available for download from http://aspnet.codeplex.com/releases/view/58781 or by installing the NuGet package Mvc3Futures. They include:

  • Cookie value provider
  • Server variable value provider
  • Session value provider
  • TempData value provider

The source code for all of MVC (including MVC Futures) is available at that same CodePlex link, and includes the value provider factories and value providers that should help you get started building your own.

Creating Models with Model Binders

The other part of extending models is model binders. They take values from the value provider system and either create new models with the data or fill in existing models with the data. The default model binder in MVC (named DefaultModelBinder, conveniently) is an extremely powerful piece of code. It's capable of performing model binding against traditional classes, collection classes, lists, arrays, and even dictionaries.

One thing the default model binder can't do well is supporting immutable objects: that is, objects whose initial values must be set via a constructor and cannot be changed later. Our example model binder code in ∼/Areas/ModelBinder includes the source code for a model binder for the Point object from the CLR. Because the Point class is immutable, you must construct a new instance using its values:

public class PointModelBinder : IModelBinder {
    public object BindModel(ControllerContext controllerContext,
                            ModelBindingContext bindingContext) {
        var valueProvider = bindingContext.ValueProvider;
        int x = (int)valueProvider.GetValue("X").ConvertTo(typeof(int));
        int y = (int)valueProvider.GetValue("Y").ConvertTo(typeof(int));
        return new Point(x, y);
    }
}

When you create a new model binder, you need to tell the MVC framework that there exists a new model binder and when to use it. You can either decorate the bound class with the [ModelBinder] attribute, or you can register the new model binder in the global list at ModelBinders.Binders.

An often overlooked responsibility of model binders is validating the values that they're binding. The preceding example code is quite simple because it does not include any of the validation logic. The full sample does include support for validation, but it makes the example a bit more detailed. In some instances, you know the types you're model binding against, so supporting generic validation might not be necessary (because you could hard-code the validation logic directly into the model binder); in other cases, you want to consult the built-in validation system to ensure that your models are correct.

In the extended sample (which matches the code in the NuGet package), let's see what a more complete version of the model binder looks like, line by line. The new implementation of BindModel still looks relatively straightforward, because we've moved all the retrieval, conversion, and validation logic into a helper method:

public object BindModel(ControllerContext controllerContext,
                        ModelBindingContext bindingContext) {

    if (!String.IsNullOrEmpty(bindingContext.ModelName) &&
        !bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName)) {

        if (!bindingContext.FallbackToEmptyPrefix)
            return null;

        bindingContext = new ModelBindingContext {
            ModelMetadata = bindingContext.ModelMetadata,
            ModelState = bindingContext.ModelState,
            PropertyFilter = bindingContext.PropertyFilter,
            ValueProvider = bindingContext.ValueProvider
        };
    }

    bindingContext.ModelMetadata.Model = new Point();

    return new Point(
        Get<int>(controllerContext, bindingContext, "X"),
        Get<int>(controllerContext, bindingContext, "Y")
    );
}

We're doing two new things in this version of BindModel that you didn't see in the original.

  • The block of code with the first if block, which is trying to find values with the name prefix before falling back to an empty prefix. When the system starts model binding, the value in bindingContext.ModelName is set to the name of the model parameter (in our sample controller, that's pt). We look inside the value providers and ask if they have any sub-values that start with pt, because if they do, those are the values we want to use. With a parameter named pt, we would prefer to use values whose names were pt.X and pt.Y instead of just X and Y. However, if we don't find any values that start with pt, we need to be able to fall back to using just X and Y for the names.
  • The second thing that's new here is that we put an empty instance of the Point object into the ModelMetadata. The reason we need to do this is that most validation systems, including DataAnnotations, expect to see an instance of the container object even if it doesn't necessarily have the actual values in it yet. Our call to the Get method invokes validation, so we need to give the validation system a container object of some sort, even though we know it's not the final container.

The Get method has several pieces to it. Here's the whole function, and then you'll examine the code a few lines at a time:

private TModel Get<TModel>(ControllerContext controllerContext,
                            ModelBindingContext bindingContext,
                            string name) {

    string fullName = name;
    if (!String.IsNullOrWhiteSpace(bindingContext.ModelName))
        fullName = bindingContext.ModelName + "." + name;

    ValueProviderResult valueProviderResult =
        bindingContext.ValueProvider.GetValue(fullName);

    ModelState modelState = new ModelState { Value = valueProviderResult };
    bindingContext.ModelState.Add(fullName, modelState);

    ModelMetadata metadata = bindingContext.PropertyMetadata[name];

    string attemptedValue = valueProviderResult.AttemptedValue;
    if (metadata.ConvertEmptyStringToNull
            && String.IsNullOrWhiteSpace(attemptedValue))
        attemptedValue = null;

    TModel model;
    bool invalidValue = false;

    try
    {
        model = (TModel)valueProviderResult.ConvertTo(typeof(TModel));
        metadata.Model = model;
    }
    catch (Exception)
    {
        model = default(TModel);
        metadata.Model = attemptedValue;
        invalidValue = true;
    }

    IEnumerable<ModelValidator> validators =
        ModelValidatorProviders.Providers.GetValidators(
            metadata,
            controllerContext
        );

    foreach (var validator in validators)
        foreach (var validatorResult in validator.Validate(bindingContext.Model))
            modelState.Errors.Add(validatorResult.Message);

    if (invalidValue && modelState.Errors.Count == 0)
        modelState.Errors.Add(
            String.Format(
                "The value ‘{0}’ is not a valid value for {1}.",
                attemptedValue,
                metadata.GetDisplayName()
            )
        );

    return model;
}

The line by line analysis is as follows:

1. The first thing you need to do is retrieve the attempted value from the value provider, and then record the value in the model state so that the user can always see the exact value they typed, even if the value ended up being something the model cannot directly contain (for example, if the user types abc into a field that allows only integers):

 string fullName = name;
 if (!String.IsNullOrWhiteSpace(bindingContext.ModelName))
     fullName = bindingContext.ModelName + "." + name;

ValueProviderResult valueProviderResult = bindingContext.ValueProvider.GetValue(fullName);
ModelState modelState = new ModelState { Value = valueProviderResult }; bindingContext.ModelState.Add(fullName, modelState);

The fully qualified name prepends the model name, in the event that you're doing deep model binding. This might happen if you decided to have a property of type Point inside another class (like a view model).

2. Once you have the result from the value provider, you must get a copy of the model metadata that describes this property, and then determine what the attempted value was that the user entered:

 ModelMetadata metadata = bindingContext.PropertyMetadata[name];

string attemptedValue = valueProviderResult.AttemptedValue; if (metadata.ConvertEmptyStringToNull && String.IsNullOrWhiteSpace(attemptedValue)) attemptedValue = null;

You use the model metadata to determine whether you should convert empty strings into nulls. This behavior is generally on by default because HTML forms always post empty strings rather than nulls when the user hasn't entered any value. The validators which check for required values are generally written such that nulls fail a required check but empty strings succeed, so the developer can set a flag in the metadata to allow empty strings to be placed into the field rather than being converted to null (and thereby failing any required validation checks).

3. The next section of code attempts to convert the value into the destination type, and records if there was some kind of conversion error. Either way, you need to have a value placed into the metadata so that validation has a value to run against. If you can successfully convert the value, then you can use that; otherwise, you use the attempted value, even though you know it's not the right type.

 TModel model;
 bool invalidValue = false;

try { model = (TModel)valueProviderResult.ConvertTo(typeof(TModel)); metadata.Model = model; } catch (Exception) { model = default(TModel); metadata.Model = attemptedValue; invalidValue = true; }

You record whether there was a conversion failure for later, because you want to add conversion failure error messages only if no other validation failed (for example, you generally expect both required and data conversion failures for values that are required, but the required validator message is more correct, so you want to make sure it has higher priority).

4. Run all the validators and record each validation failure in the errors collection of the model state:

 IEnumerable<ModelValidator> validators =
     ModelValidatorProviders.Providers.GetValidators(
         metadata,
         controllerContext
     );

foreach (var validator in validators) foreach (var validatorResult in validator.Validate(bindingContext.Model)) modelState.Errors.Add(validatorResult.Message);

5. Record the data type conversion error, if one occurred and no other validation rules failed, and then return the value back so that it can be used for the rest of the model binding process:

 if (invalidValue && modelState.Errors.Count == 0)
     modelState.Errors.Add(
         String.Format(
             "The value ‘{0}’ is not a valid value for {1}.",
             attemptedValue,
             metadata.GetDisplayName()
         )
     );

return model;

The sample includes a simple controller and view that demonstrate the use of the model binder (which is registered in the area registration file). For this sample, the client-side validation is disabled so that you can easily see the server-side logic being run and debug into it. You can and should turn on client-side validation inside the view so that you can see the client-side validation rules remain in place and functional.

Describing Models with Metadata

The model metadata system was introduced in ASP.NET MVC 2. It helps describe meta-information about a model that is used to assist in the HTML generation and validation of models. The kinds of information exposed by the model metadata system include (but are not limited to) answers to the following questions:

  • What is the type of the model?
  • What is the type of the containing model, if any?
  • What is the name of the property this value came from?
  • Is it a simple type or a complex one?
  • What is the display name?
  • How do you format the value for display? For editing?
  • Is the value required?
  • Is the value read-only?
  • What template should I use to display this?

Out of the box, MVC supports model metadata that's expressed through attributes applied to classes and properties. These attributes are found primarily in the System.ComponentModel and System.ComponentModel.DataAnnotations namespaces.

The ComponentModel namespace has been around since .NET 1.0 and was originally designed for use in Visual Studio designers such as Web Forms and Windows Forms. The DataAnnotations classes were introduced in .NET 3.5 SP1 (along with ASP.NET Dynamic Data) and were designed primarily for use with model metadata. In .NET 4, the DataAnnotations classes were significantly enhanced, and started being used by the WCF RIA Services team as well as being ported to Silverlight 4. Despite getting their start on the ASP.NET team, they have been designed from the beginning to be agnostic of the UI presentation layer, which is why they live under System.ComponentModel rather than under System.Web.

ASP.NET MVC offers a pluggable model metadata provider system so that you can provide your own metadata source, if you'd prefer not to use DataAnnotations attributes. Implementing a metadata provider means deriving a class from ModelMetadataProvider and implementing the three abstract methods:

  • GetMetadataForType returns the metadata about a whole class
  • GetMetadataForProperty returns the metadata for a single property on a class
  • GetMetadataForProperties returns the metadata for all the properties on a class

There is a derived type, AssociatedMetadataProvider, that can be used by metadata providers that intend to provide metadata via attributes. It consolidates the three method calls down into a single one named CreateMetadata, and passes along the list of attributes that were attached to the model and/or model properties. If you're writing a metadata provider that is decorating your models with attributes, it's often a good idea to use AssociatedMetadataProvider as the base class for your provider class, because of the simplified API (and the automatic support for metadata “buddy classes”).

The sample code includes a fluent metadata provider example under ∼/Areas/FluentMetadata. The implementation is extensive, given how many different pieces of metadata are available to the end user, but the code is fairly simple and straightforward. Because MVC can use only a single metadata provider, the example derives from the built-in metadata provider so that the user can mix traditional metadata attributes and dynamic code-based metadata.

In our example, the metadata registration is performed inside of the area registration function:

ModelMetadataProviders.Current =
    new FluentMetadataProvider()
        .ForModel<Contact>()
            .ForProperty(m => m.FirstName)
                .DisplayName("First Name")
                .DataTypeName("string")
            .ForProperty(m => m.LastName)
                .DisplayName("Last Name")
                .DataTypeName("string")
            .ForProperty(m => m.EmailAddress)
                .DisplayName("E-mail address")
                .DataTypeName("email");

The implementation of CreateMetadata starts by getting the metadata that is derived from the annotation attributes, and then modifying those values through modifiers that are registered by the developer. The modifier methods (like the calls to DisplayName) simply record future modifications that are performed against the ModelMetadata object after it's been requested. The modifications are stored away in a dictionary inside of the fluent provider so that you can run them later in CreateMetadata, which is shown here:

protected override ModelMetadata CreateMetadata(
        IEnumerable<Attribute> attributes,
        Type containerType,
        Func<object> modelAccessor,
        Type modelType,
        string propertyName) {

    // Start with the metadata from the annotation attributes
    ModelMetadata metadata =
        base.CreateMetadata(
            attributes,
            containerType,
            modelAccessor,
            modelType,
            propertyName
        );

    // Look inside our modifier dictionary for registrations
    Tuple<Type, string> key =
        propertyName == null
            ? new Tuple<Type, string>(modelType, null)
            : new Tuple<Type, string>(containerType, propertyName);

    // Apply the modifiers to the metadata, if we found any
    List<Action<ModelMetadata>> modifierList;
    if (modifiers.TryGetValue(key, out modifierList))
        foreach (Action<ModelMetadata> modifier in modifierList)
            modifier(metadata);

    return metadata;
}

The implementation of this metadata provider is effectively just a mapping of either types to modifiers (for modifying the metadata of a class) or mappings of types + property names to modifiers (for modifying the metadata of a property). Although there are several of these modifier functions, they all follow the same basic pattern, which is to register the modification function in the dictionary of the provider so that it can be run later. Here is the implementation of DisplayName:

public MetadataRegistrar<TModel> DisplayName(string displayName)
{
    provider.Add(
        typeof(TModel),
        propertyName,
        metadata => metadata.DisplayName = displayName
    );

    return this;
}

The third parameter to the Add call is the anonymous function that acts as the modifier: given an instance of a metadata object, it sets the DisplayName property to the display name that the developer provided. Consult the full sample for the complete code, including controller and view, which shows everything working together.

Validating Models

Model validation has been supported since ASP.NET MVC 1.0, but it wasn't until MVC 2 that the team introduced pluggable validation providers. MVC 1.0 validation was based on the IDataErrorInfo interface (though this is still functional, developers should consider it to be deprecated). Instead, developers using MVC 2 or later can use the DataAnnotations validation attributes on their model properties. In the box in .NET 3.5 SP1 are four validation attributes: [Required], [Range], [StringLength], and [RegularExpression]. A base class, ValidationAttribute, is provided for developers to write their own custom validation logic.

The CLR team added a few enhancements to the validation system in .NET 4, including the new IValidatableObject interface. ASP.NET MVC 3 added two new validators: [Compare] and [Remote]. The team also shipped several validators in MVC 3 Futures, to match with the new set of validation rules available with jQuery Validate, including [CreditCard], [Email], [FileExtension], and [Url].

Chapter 6 covers writing custom validators in depth, so I won't rehash that material. Instead, the example focuses on the more advanced topic of writing validator providers. Validator providers allow the developer to introduce new sources of validation. In the box in MVC 3, three validator providers are installed by default:

  • DataAnnotationsModelValidatorProvider provides support for validators derived from ValidationAttribute and models that implement IValidatableObject
  • DataErrorInfoModelValidatorProvider provides support for classes that implement the IDataErrorInfo interface used by MVC 1.0's validation layer
  • ClientDataTypeModelValidatorProvider provides client validation support for the built-in numeric data types (integers, decimals, and floating-point numbers)

Implementing a validator provider means deriving from the ModelValidatorProvider base class, and implementing the single method that returns validators for a given model (represented by an instance of ModelMetadata and the ControllerContext). You register your custom model validator provider by using ModelValidatorProviders.Providers.

There is an example of a fluent model validation system present in the sample code under ∼/Areas/FluentValidation. Much like the fluent model metadata example, this is fairly extensive because it needs to provide several validation functions, but most of the code for implementing the validator provider itself is relatively straightforward and self-explanatory.

The sample includes fluent validation registration inside the area registration function:

ModelValidatorProviders.Providers.Add(
    new FluentValidationProvider()
        .ForModel<Contact>()
            .ForProperty(c => c.FirstName)
                .Required()
                .StringLength(maxLength: 15)
            .ForProperty(c => c.LastName)
                .Required(errorMessage: "You must provide the last name!")
                .StringLength(minLength: 3, maxLength: 20)
            .ForProperty(c => c.EmailAddress)
                .Required()
                .StringLength(minLength: 10)
                .EmailAddress()
);

We have implemented three different validators for this example, including both server-side and client-side validation support. The registration API looks nearly identical to the model metadata fluent API example examined previously. Our implementation of GetValidators is based on a dictionary that maps requested types and optional property names to validator factories:

public override IEnumerable<ModelValidator> GetValidators(
        ModelMetadata metadata,
        ControllerContext context) {

    IEnumerable<ModelValidator> results = Enumerable.Empty<ModelValidator>();

    if (metadata.PropertyName != null)
        results = GetValidators(metadata,
                                context,
                                metadata.ContainerType,
                                metadata.PropertyName);

    return results.Concat(
        GetValidators(metadata,
                      context,
                      metadata.ModelType)
    );
}

Unlike model metadata, the MVC framework supports multiple validator providers, so there is no need for you to derive from the existing validator provider or delegate to it. You just add your own unique validation rules as appropriate. The validators that apply to a particular property are those that are applied to the property itself as well as those that are applied to the property's type; so for example, if you have this model:

public class Contact
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string EmailAddress { get; set; }
}

when the system requests validation rules for FirstName, the system provides rules that have been applied to the FirstName property itself, as well as any rules that have been applied to System.String (because that's the type FirstName is).

The implementation of the private GetValidators method used in the previous example then becomes:

private IEnumerable<ModelValidator> GetValidators(
        ModelMetadata metadata,
        ControllerContext context,
        Type type,
        string propertyName = null)
{
    var key = new Tuple<Type, string>(type, propertyName);
    List<ValidatorFactory> factories;
    if (validators.TryGetValue(key, out factories))
        foreach (var factory in factories)
            yield return factory(metadata, context);
}

This code looks up all the validator factories that have been registered with the provider. The functions you saw in registration like Required and StringLength are how those validator factories get registered. All those functions tend to follow the same pattern:

public ValidatorRegistrar<TModel> Required(
        string errorMessage = "{0} is required")
{
    provider.Add(
        typeof(TModel),
        propertyName,
        (metadata, context) =>
            new RequiredValidator(metadata, context, errorMessage)
    );

    return this;
}

The third parameter in the call to provider.Add is the anonymous function that acts as the validator factory. Given an input of the model metadata and the controller context, it returns an instance of a class that derives from ModelValidator.

The ModelValidator base class is the class that MVC understands and consumes for the purposes of validation. You saw the implicit use of the ModelValidator class in the previous model binder example, because the model binder is ultimately responsible for running validation while it's creating and binding the objects. Our implementation of the RequiredValidator that we're using has two core responsibilities: perform the server-side validation, and return metadata about the client-side validation. Our implementation looks like this:

private class RequiredValidator : ModelValidator {
    private string errorMessage;

    public RequiredValidator(ModelMetadata metadata,
                             ControllerContext context,
                             string errorMessage) : base(metadata, context) {
        this.errorMessage = errorMessage;
    }

    private string ErrorMessage {
        get {
            return String.Format(errorMessage, Metadata.GetDisplayName());
        }
    }

    public IEnumerable<ModelClientValidationRule> GetClientValidationRules() {
        yield return new ModelClientValidationRequiredRule(ErrorMessage);
    }

    public IEnumerable<ModelValidationResult> Validate(object container) {
        if (Metadata.Model == null)
            yield return new ModelValidationResult { Message = ErrorMessage };
    }
}

The full example includes implementation of three validation rules (Required, StringLength, and EmailAddress), including a model, controller, and view, which shows it all working together. Client-side validation has been turned off by default so that you can verify and debug into the server-side validation. You can remove the single line of code from the view to re-enable client-side validation and see how it works.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.119.19.174