2010-06-21 Marek Habersack <mhabersack@novell.com>
[mcs.git] / class / System.Web.Mvc / System.Web.Mvc / DefaultModelBinder.cs
blob21f52b2d1213bd63478cce3a11b4634b3ff3f423
1 /* ****************************************************************************
3 * Copyright (c) Microsoft Corporation. All rights reserved.
5 * This software is subject to the Microsoft Public License (Ms-PL).
6 * A copy of the license can be found in the license.htm file included
7 * in this distribution.
9 * You must not remove this notice, or any other, from this software.
11 * ***************************************************************************/
13 namespace System.Web.Mvc {
14 using System;
15 using System.Collections;
16 using System.Collections.Generic;
17 using System.ComponentModel;
18 using System.Diagnostics.CodeAnalysis;
19 using System.Globalization;
20 using System.Linq;
21 using System.Reflection;
22 using System.Web.Mvc.Resources;
24 public class DefaultModelBinder : IModelBinder {
26 private ModelBinderDictionary _binders;
27 private static string _resourceClassKey;
29 [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly",
30 Justification = "Property is settable so that the dictionary can be provided for unit testing purposes.")]
31 protected internal ModelBinderDictionary Binders {
32 get {
33 if (_binders == null) {
34 _binders = ModelBinders.Binders;
36 return _binders;
38 set {
39 _binders = value;
43 public static string ResourceClassKey {
44 get {
45 return _resourceClassKey ?? String.Empty;
47 set {
48 _resourceClassKey = value;
52 internal void BindComplexElementalModel(ControllerContext controllerContext, ModelBindingContext bindingContext, object model) {
53 // need to replace the property filter + model object and create an inner binding context
54 BindAttribute bindAttr = (BindAttribute)TypeDescriptor.GetAttributes(bindingContext.ModelType)[typeof(BindAttribute)];
55 Predicate<string> newPropertyFilter = (bindAttr != null)
56 ? propertyName => bindAttr.IsPropertyAllowed(propertyName) && bindingContext.PropertyFilter(propertyName)
57 : bindingContext.PropertyFilter;
59 ModelBindingContext newBindingContext = new ModelBindingContext() {
60 Model = model,
61 ModelName = bindingContext.ModelName,
62 ModelState = bindingContext.ModelState,
63 ModelType = bindingContext.ModelType,
64 PropertyFilter = newPropertyFilter,
65 ValueProvider = bindingContext.ValueProvider
68 // validation
69 if (OnModelUpdating(controllerContext, newBindingContext)) {
70 BindProperties(controllerContext, newBindingContext);
71 OnModelUpdated(controllerContext, newBindingContext);
75 internal object BindComplexModel(ControllerContext controllerContext, ModelBindingContext bindingContext) {
76 object model = bindingContext.Model;
77 Type modelType = bindingContext.ModelType;
79 // if we're being asked to create an array, create a list instead, then coerce to an array after the list is created
80 if (model == null && modelType.IsArray) {
81 Type elementType = modelType.GetElementType();
82 Type listType = typeof(List<>).MakeGenericType(elementType);
83 object collection = CreateModel(controllerContext, bindingContext, listType);
85 ModelBindingContext arrayBindingContext = new ModelBindingContext() {
86 Model = collection,
87 ModelName = bindingContext.ModelName,
88 ModelState = bindingContext.ModelState,
89 ModelType = listType,
90 PropertyFilter = bindingContext.PropertyFilter,
91 ValueProvider = bindingContext.ValueProvider
93 IList list = (IList)UpdateCollection(controllerContext, arrayBindingContext, elementType);
95 if (list == null) {
96 return null;
99 Array array = Array.CreateInstance(elementType, list.Count);
100 list.CopyTo(array, 0);
101 return array;
104 if (model == null) {
105 model = CreateModel(controllerContext,bindingContext,modelType);
108 // special-case IDictionary<,> and ICollection<>
109 Type dictionaryType = ExtractGenericInterface(modelType, typeof(IDictionary<,>));
110 if (dictionaryType != null) {
111 Type[] genericArguments = dictionaryType.GetGenericArguments();
112 Type keyType = genericArguments[0];
113 Type valueType = genericArguments[1];
115 ModelBindingContext dictionaryBindingContext = new ModelBindingContext() {
116 Model = model,
117 ModelName = bindingContext.ModelName,
118 ModelState = bindingContext.ModelState,
119 ModelType = modelType,
120 PropertyFilter = bindingContext.PropertyFilter,
121 ValueProvider = bindingContext.ValueProvider
123 object dictionary = UpdateDictionary(controllerContext, dictionaryBindingContext, keyType, valueType);
124 return dictionary;
127 Type enumerableType = ExtractGenericInterface(modelType, typeof(IEnumerable<>));
128 if (enumerableType != null) {
129 Type elementType = enumerableType.GetGenericArguments()[0];
131 Type collectionType = typeof(ICollection<>).MakeGenericType(elementType);
132 if (collectionType.IsInstanceOfType(model)) {
133 ModelBindingContext collectionBindingContext = new ModelBindingContext() {
134 Model = model,
135 ModelName = bindingContext.ModelName,
136 ModelState = bindingContext.ModelState,
137 ModelType = modelType,
138 PropertyFilter = bindingContext.PropertyFilter,
139 ValueProvider = bindingContext.ValueProvider
141 object collection = UpdateCollection(controllerContext, collectionBindingContext, elementType);
142 return collection;
146 // otherwise, just update the properties on the complex type
147 BindComplexElementalModel(controllerContext, bindingContext, model);
148 return model;
151 public virtual object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) {
152 if (bindingContext == null) {
153 throw new ArgumentNullException("bindingContext");
156 bool performedFallback = false;
158 if (!String.IsNullOrEmpty(bindingContext.ModelName) && !DictionaryHelpers.DoesAnyKeyHavePrefix(bindingContext.ValueProvider, bindingContext.ModelName)) {
159 // We couldn't find any entry that began with the prefix. If this is the top-level element, fall back
160 // to the empty prefix.
161 if (bindingContext.FallbackToEmptyPrefix) {
162 bindingContext = new ModelBindingContext() {
163 Model = bindingContext.Model,
164 ModelState = bindingContext.ModelState,
165 ModelType = bindingContext.ModelType,
166 PropertyFilter = bindingContext.PropertyFilter,
167 ValueProvider = bindingContext.ValueProvider
169 performedFallback = true;
171 else {
172 return null;
176 // Simple model = int, string, etc.; determined by calling TypeConverter.CanConvertFrom(typeof(string))
177 // or by seeing if a value in the request exactly matches the name of the model we're binding.
178 // Complex type = everything else.
179 if (!performedFallback) {
180 ValueProviderResult vpResult;
181 bindingContext.ValueProvider.TryGetValue(bindingContext.ModelName, out vpResult);
182 if (vpResult != null) {
183 return BindSimpleModel(controllerContext, bindingContext, vpResult);
186 if (TypeDescriptor.GetConverter(bindingContext.ModelType).CanConvertFrom(typeof(string))) {
187 return null;
190 return BindComplexModel(controllerContext, bindingContext);
193 private void BindProperties(ControllerContext controllerContext, ModelBindingContext bindingContext) {
194 PropertyDescriptorCollection properties = GetModelProperties(controllerContext, bindingContext);
195 foreach (PropertyDescriptor property in properties) {
196 BindProperty(controllerContext, bindingContext, property);
200 protected virtual void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor) {
201 // need to skip properties that aren't part of the request, else we might hit a StackOverflowException
202 string fullPropertyKey = CreateSubPropertyName(bindingContext.ModelName, propertyDescriptor.Name);
203 if (!DictionaryHelpers.DoesAnyKeyHavePrefix(bindingContext.ValueProvider, fullPropertyKey)) {
204 return;
207 // call into the property's model binder
208 IModelBinder propertyBinder = Binders.GetBinder(propertyDescriptor.PropertyType);
209 object originalPropertyValue = propertyDescriptor.GetValue(bindingContext.Model);
210 ModelBindingContext innerBindingContext = new ModelBindingContext() {
211 Model = originalPropertyValue,
212 ModelName = fullPropertyKey,
213 ModelState = bindingContext.ModelState,
214 ModelType = propertyDescriptor.PropertyType,
215 ValueProvider = bindingContext.ValueProvider
217 object newPropertyValue = propertyBinder.BindModel(controllerContext, innerBindingContext);
219 // validation
220 if (OnPropertyValidating(controllerContext, bindingContext, propertyDescriptor, newPropertyValue)) {
221 SetProperty(controllerContext, bindingContext, propertyDescriptor, newPropertyValue);
222 OnPropertyValidated(controllerContext, bindingContext, propertyDescriptor, newPropertyValue);
226 internal object BindSimpleModel(ControllerContext controllerContext, ModelBindingContext bindingContext, ValueProviderResult valueProviderResult) {
227 bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);
229 // if the value provider returns an instance of the requested data type, we can just short-circuit
230 // the evaluation and return that instance
231 if (bindingContext.ModelType.IsInstanceOfType(valueProviderResult.RawValue)) {
232 return valueProviderResult.RawValue;
235 // since a string is an IEnumerable<char>, we want it to skip the two checks immediately following
236 if (bindingContext.ModelType != typeof(string)) {
238 // conversion results in 3 cases, as below
239 if (bindingContext.ModelType.IsArray) {
240 // case 1: user asked for an array
241 // ValueProviderResult.ConvertTo() understands array types, so pass in the array type directly
242 object modelArray = ConvertProviderResult(bindingContext.ModelState, bindingContext.ModelName, valueProviderResult, bindingContext.ModelType);
243 return modelArray;
246 Type enumerableType = ExtractGenericInterface(bindingContext.ModelType, typeof(IEnumerable<>));
247 if (enumerableType != null) {
248 // case 2: user asked for a collection rather than an array
249 // need to call ConvertTo() on the array type, then copy the array to the collection
250 object modelCollection = CreateModel(controllerContext, bindingContext, bindingContext.ModelType);
251 Type elementType = enumerableType.GetGenericArguments()[0];
252 Type arrayType = elementType.MakeArrayType();
253 object modelArray = ConvertProviderResult(bindingContext.ModelState, bindingContext.ModelName, valueProviderResult, arrayType);
255 Type collectionType = typeof(ICollection<>).MakeGenericType(elementType);
256 if (collectionType.IsInstanceOfType(modelCollection)) {
257 CollectionHelpers.ReplaceCollection(elementType, modelCollection, modelArray);
259 return modelCollection;
263 // case 3: user asked for an individual element
264 object model = ConvertProviderResult(bindingContext.ModelState, bindingContext.ModelName, valueProviderResult, bindingContext.ModelType);
265 return model;
268 private static bool CanUpdateReadonlyTypedReference(Type type) {
269 // value types aren't strictly immutable, but because they have copy-by-value semantics
270 // we can't update a value type that is marked readonly
271 if (type.IsValueType) {
272 return false;
275 // arrays are mutable, but because we can't change their length we shouldn't try
276 // to update an array that is referenced readonly
277 if (type.IsArray) {
278 return false;
281 // special-case known common immutable types
282 if (type == typeof(string)) {
283 return false;
286 return true;
289 [SuppressMessage("Microsoft.Globalization", "CA1304:SpecifyCultureInfo", MessageId = "System.Web.Mvc.ValueProviderResult.ConvertTo(System.Type)",
290 Justification = "The target object should make the correct culture determination, not this method.")]
291 [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes",
292 Justification = "We're recording this exception so that we can act on it later.")]
293 private static object ConvertProviderResult(ModelStateDictionary modelState, string modelStateKey, ValueProviderResult valueProviderResult, Type destinationType) {
294 try {
295 object convertedValue = valueProviderResult.ConvertTo(destinationType);
296 return convertedValue;
298 catch (Exception ex) {
299 modelState.AddModelError(modelStateKey, ex);
300 return null;
304 protected virtual object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType) {
305 Type typeToCreate = modelType;
307 // we can understand some collection interfaces, e.g. IList<>, IDictionary<,>
308 if (modelType.IsGenericType) {
309 Type genericTypeDefinition = modelType.GetGenericTypeDefinition();
310 if (genericTypeDefinition == typeof(IDictionary<,>)) {
311 typeToCreate = typeof(Dictionary<,>).MakeGenericType(modelType.GetGenericArguments());
313 else if (genericTypeDefinition == typeof(IEnumerable<>) || genericTypeDefinition == typeof(ICollection<>) || genericTypeDefinition == typeof(IList<>)) {
314 typeToCreate = typeof(List<>).MakeGenericType(modelType.GetGenericArguments());
318 // fallback to the type's default constructor
319 return Activator.CreateInstance(typeToCreate);
322 protected static string CreateSubIndexName(string prefix, int index) {
323 return String.Format(CultureInfo.InvariantCulture, "{0}[{1}]", prefix, index);
326 protected static string CreateSubPropertyName(string prefix, string propertyName) {
327 return (!String.IsNullOrEmpty(prefix)) ? prefix + "." + propertyName : propertyName;
330 private static Type ExtractGenericInterface(Type queryType, Type interfaceType) {
331 Func<Type, bool> matchesInterface = t => t.IsGenericType && t.GetGenericTypeDefinition() == interfaceType;
332 return (matchesInterface(queryType)) ? queryType : queryType.GetInterfaces().FirstOrDefault(matchesInterface);
335 protected virtual PropertyDescriptorCollection GetModelProperties(ControllerContext controllerContext, ModelBindingContext bindingContext) {
336 PropertyDescriptorCollection allProperties = TypeDescriptor.GetProperties(bindingContext.ModelType);
337 Predicate<string> propertyFilter = bindingContext.PropertyFilter;
339 var filteredProperties = from PropertyDescriptor property in allProperties
340 where ShouldUpdateProperty(property, propertyFilter)
341 select property;
343 return new PropertyDescriptorCollection(filteredProperties.ToArray());
346 private static string GetValueRequiredResource(ControllerContext controllerContext) {
347 string resourceValue = null;
348 if (!String.IsNullOrEmpty(ResourceClassKey) && (controllerContext != null) && (controllerContext.HttpContext != null)) {
349 // If the user specified a ResourceClassKey try to load the resource they specified.
350 // If the class key is invalid, an exception will be thrown.
351 // If the class key is valid but the resource is not found, it returns null, in which
352 // case it will fall back to the MVC default error message.
353 resourceValue = controllerContext.HttpContext.GetGlobalResourceObject(ResourceClassKey, "PropertyValueRequired", CultureInfo.CurrentUICulture) as string;
355 return resourceValue ?? MvcResources.DefaultModelBinder_ValueRequired;
358 protected virtual void OnModelUpdated(ControllerContext controllerContext, ModelBindingContext bindingContext) {
359 IDataErrorInfo errorProvider = bindingContext.Model as IDataErrorInfo;
360 if (errorProvider != null) {
361 string errorText = errorProvider.Error;
362 if (!String.IsNullOrEmpty(errorText)) {
363 bindingContext.ModelState.AddModelError(bindingContext.ModelName, errorText);
368 protected virtual bool OnModelUpdating(ControllerContext controllerContext, ModelBindingContext bindingContext) {
369 // default implementation does nothing
371 return true;
374 protected virtual void OnPropertyValidated(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, object value) {
375 IDataErrorInfo errorProvider = bindingContext.Model as IDataErrorInfo;
376 if (errorProvider != null) {
377 string errorText = errorProvider[propertyDescriptor.Name];
378 if (!String.IsNullOrEmpty(errorText)) {
379 string modelStateKey = CreateSubPropertyName(bindingContext.ModelName, propertyDescriptor.Name);
380 bindingContext.ModelState.AddModelError(modelStateKey, errorText);
385 protected virtual bool OnPropertyValidating(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, object value) {
386 // default implementation just checks to make sure that required text entry fields aren't left blank
388 string modelStateKey = CreateSubPropertyName(bindingContext.ModelName, propertyDescriptor.Name);
389 return VerifyValueUsability(controllerContext, bindingContext.ModelState, modelStateKey, propertyDescriptor.PropertyType, value);
392 [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes",
393 Justification = "We're recording this exception so that we can act on it later.")]
394 protected virtual void SetProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, object value) {
395 if (propertyDescriptor.IsReadOnly) {
396 return;
399 try {
400 propertyDescriptor.SetValue(bindingContext.Model, value);
402 catch (Exception ex) {
403 string modelStateKey = CreateSubPropertyName(bindingContext.ModelName, propertyDescriptor.Name);
404 bindingContext.ModelState.AddModelError(modelStateKey, ex);
408 private static bool ShouldUpdateProperty(PropertyDescriptor property, Predicate<string> propertyFilter) {
409 if (property.IsReadOnly && !CanUpdateReadonlyTypedReference(property.PropertyType)) {
410 return false;
413 // if this property is rejected by the filter, move on
414 if (!propertyFilter(property.Name)) {
415 return false;
418 // otherwise, allow
419 return true;
422 internal object UpdateCollection(ControllerContext controllerContext, ModelBindingContext bindingContext, Type elementType) {
423 IModelBinder elementBinder = Binders.GetBinder(elementType);
425 // build up a list of items from the request
426 List<object> modelList = new List<object>();
427 for (int currentIndex = 0; ; currentIndex++) {
428 string subIndexKey = CreateSubIndexName(bindingContext.ModelName, currentIndex);
429 if (!DictionaryHelpers.DoesAnyKeyHavePrefix(bindingContext.ValueProvider, subIndexKey)) {
430 // we ran out of elements to pull
431 break;
434 ModelBindingContext innerContext = new ModelBindingContext() {
435 ModelName = subIndexKey,
436 ModelState = bindingContext.ModelState,
437 ModelType = elementType,
438 PropertyFilter = bindingContext.PropertyFilter,
439 ValueProvider = bindingContext.ValueProvider
441 object thisElement = elementBinder.BindModel(controllerContext, innerContext);
443 // we need to merge model errors up
444 VerifyValueUsability(controllerContext, bindingContext.ModelState, subIndexKey, elementType, thisElement);
445 modelList.Add(thisElement);
448 // if there weren't any elements at all in the request, just return
449 if (modelList.Count == 0) {
450 return null;
453 // replace the original collection
454 object collection = bindingContext.Model;
455 CollectionHelpers.ReplaceCollection(elementType, collection, modelList);
456 return collection;
459 internal object UpdateDictionary(ControllerContext controllerContext, ModelBindingContext bindingContext, Type keyType, Type valueType) {
460 IModelBinder keyBinder = Binders.GetBinder(keyType);
461 IModelBinder valueBinder = Binders.GetBinder(valueType);
463 // build up a list of items from the request
464 List<KeyValuePair<object, object>> modelList = new List<KeyValuePair<object, object>>();
465 for (int currentIndex = 0; ; currentIndex++) {
466 string subIndexKey = CreateSubIndexName(bindingContext.ModelName, currentIndex);
467 string keyFieldKey = CreateSubPropertyName(subIndexKey, "key");
468 string valueFieldKey = CreateSubPropertyName(subIndexKey, "value");
470 if (!(DictionaryHelpers.DoesAnyKeyHavePrefix(bindingContext.ValueProvider, keyFieldKey) && DictionaryHelpers.DoesAnyKeyHavePrefix(bindingContext.ValueProvider, valueFieldKey))) {
471 // we ran out of elements to pull
472 break;
475 // bind the key
476 ModelBindingContext keyBindingContext = new ModelBindingContext() {
477 ModelName = keyFieldKey,
478 ModelState = bindingContext.ModelState,
479 ModelType = keyType,
480 ValueProvider = bindingContext.ValueProvider
482 object thisKey = keyBinder.BindModel(controllerContext, keyBindingContext);
484 // we need to merge model errors up
485 VerifyValueUsability(controllerContext, bindingContext.ModelState, keyFieldKey, keyType, thisKey);
486 if (!keyType.IsInstanceOfType(thisKey)) {
487 // we can't add an invalid key, so just move on
488 continue;
491 // bind the value
492 ModelBindingContext valueBindingContext = new ModelBindingContext() {
493 ModelName = valueFieldKey,
494 ModelState = bindingContext.ModelState,
495 ModelType = valueType,
496 PropertyFilter = bindingContext.PropertyFilter,
497 ValueProvider = bindingContext.ValueProvider
499 object thisValue = valueBinder.BindModel(controllerContext, valueBindingContext);
501 // we need to merge model errors up
502 VerifyValueUsability(controllerContext, bindingContext.ModelState, valueFieldKey, valueType, thisValue);
503 KeyValuePair<object, object> kvp = new KeyValuePair<object, object>(thisKey, thisValue);
504 modelList.Add(kvp);
507 // if there weren't any elements at all in the request, just return
508 if (modelList.Count == 0) {
509 return null;
512 // replace the original collection
513 object dictionary = bindingContext.Model;
514 CollectionHelpers.ReplaceDictionary(keyType, valueType, dictionary, modelList);
515 return dictionary;
518 private static bool VerifyValueUsability(ControllerContext controllerContext, ModelStateDictionary modelState, string modelStateKey, Type elementType, object value) {
519 if (value == null && !TypeHelpers.TypeAllowsNullValue(elementType)) {
520 if (modelState.IsValidField(modelStateKey)) {
521 // a required entry field was left blank
522 string message = GetValueRequiredResource(controllerContext);
523 modelState.AddModelError(modelStateKey, message);
525 // we don't care about "you must enter a value" messages if there was an error
526 return false;
529 return true;
532 // This helper type is used because we're working with strongly-typed collections, but we don't know the Ts
533 // ahead of time. By using the generic methods below, we can consolidate the collection-specific code in a
534 // single helper type rather than having reflection-based calls spread throughout the DefaultModelBinder type.
535 // There is a single point of entry to each of the methods below, so they're fairly simple to maintain.
537 private static class CollectionHelpers {
539 private static readonly MethodInfo _replaceCollectionMethod = typeof(CollectionHelpers).GetMethod("ReplaceCollectionImpl", BindingFlags.Static | BindingFlags.NonPublic);
540 private static readonly MethodInfo _replaceDictionaryMethod = typeof(CollectionHelpers).GetMethod("ReplaceDictionaryImpl", BindingFlags.Static | BindingFlags.NonPublic);
542 public static void ReplaceCollection(Type collectionType, object collection, object newContents) {
543 MethodInfo targetMethod = _replaceCollectionMethod.MakeGenericMethod(collectionType);
544 targetMethod.Invoke(null, new object[] { collection, newContents });
547 private static void ReplaceCollectionImpl<T>(ICollection<T> collection, IEnumerable newContents) {
548 collection.Clear();
549 if (newContents != null) {
550 foreach (object item in newContents) {
551 // if the item was not a T, some conversion failed. the error message will be propagated,
552 // but in the meanwhile we need to make a placeholder element in the array.
553 T castItem = (item is T) ? (T)item : default(T);
554 collection.Add(castItem);
559 public static void ReplaceDictionary(Type keyType, Type valueType, object dictionary, object newContents) {
560 MethodInfo targetMethod = _replaceDictionaryMethod.MakeGenericMethod(keyType, valueType);
561 targetMethod.Invoke(null, new object[] { dictionary, newContents });
564 private static void ReplaceDictionaryImpl<TKey, TValue>(IDictionary<TKey, TValue> dictionary, IEnumerable<KeyValuePair<object, object>> newContents) {
565 dictionary.Clear();
566 foreach (var item in newContents) {
567 // if the item was not a T, some conversion failed. the error message will be propagated,
568 // but in the meanwhile we need to make a placeholder element in the dictionary.
569 TKey castKey = (TKey)item.Key; // this cast shouldn't fail
570 TValue castValue = (item.Value is TValue) ? (TValue)item.Value : default(TValue);
571 dictionary[castKey] = castValue;