The model system in MVC 4 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.
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:
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:
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 ASP.NET MVC Futures. They include:
Microsoft has open sourced all of MVC (including MVC Futures) at http://aspnetwebstack.codeplex.com/, which should provide a good reference to help you get started building your own value providers and factories.
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 support 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); for generalized model binders, you will want to consult the built-in validation system to find the user-supplied validators and ensure that the 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 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:
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 decide to have a property of type Point inside another class (like a view model).
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).
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).
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 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 should turn on client-side validation inside the view so that you can verify that the client-side validation rules remain in place and functional.
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:
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:
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.
One distinct advantage of the sample fluent metadata provider over the built-in metadata attributes is that you can use it to describe and decorate types whose definitions you don't control. With a traditional attribute approach, the attributes must be applied to the type at the time that the type is written; with an approach like the fluent metadata provider, describing the types is done separately from the definition of the type itself, allowing you to apply rules to types you didn't write (for example, types built into the .NET framework itself).
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.
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]. In addition, if your MVC 4 project targets .NET 4.5, there are several new attributes that MVC supports in Data Annotations that match with the rules available with jQuery Validate, including [CreditCard], [EmailAddress], [FileExtensions], [MaxLength], [MinLength], [Phone], 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, three validator providers are installed by default:
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) ); }
Given that the MVC framework supports multiple validator providers, 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.