1 /* ****************************************************************************
\r
3 * Copyright (c) Microsoft Corporation. All rights reserved.
\r
5 * This software is subject to the Microsoft Public License (Ms-PL).
\r
6 * A copy of the license can be found in the license.htm file included
\r
7 * in this distribution.
\r
9 * You must not remove this notice, or any other, from this software.
\r
11 * ***************************************************************************/
\r
13 namespace System.Web.Mvc {
\r
15 using System.Collections;
\r
16 using System.Collections.Generic;
\r
17 using System.ComponentModel;
\r
18 using System.Diagnostics.CodeAnalysis;
\r
19 using System.Globalization;
\r
21 using System.Reflection;
\r
22 using System.Runtime.CompilerServices;
\r
23 using System.Web.Mvc.Resources;
\r
25 public class DefaultModelBinder : IModelBinder {
\r
27 private ModelBinderDictionary _binders;
\r
28 private static string _resourceClassKey;
\r
30 [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly",
\r
31 Justification = "Property is settable so that the dictionary can be provided for unit testing purposes.")]
\r
32 protected internal ModelBinderDictionary Binders {
\r
34 if (_binders == null) {
\r
35 _binders = ModelBinders.Binders;
\r
44 public static string ResourceClassKey {
\r
46 return _resourceClassKey ?? String.Empty;
\r
49 _resourceClassKey = value;
\r
53 private static void AddValueRequiredMessageToModelState(ControllerContext controllerContext, ModelStateDictionary modelState, string modelStateKey, Type elementType, object value) {
\r
54 if (value == null && !TypeHelpers.TypeAllowsNullValue(elementType) && modelState.IsValidField(modelStateKey)) {
\r
55 modelState.AddModelError(modelStateKey, GetValueRequiredResource(controllerContext));
\r
59 internal void BindComplexElementalModel(ControllerContext controllerContext, ModelBindingContext bindingContext, object model) {
\r
60 // need to replace the property filter + model object and create an inner binding context
\r
61 ModelBindingContext newBindingContext = CreateComplexElementalModelBindingContext(controllerContext, bindingContext, model);
\r
64 if (OnModelUpdating(controllerContext, newBindingContext)) {
\r
65 BindProperties(controllerContext, newBindingContext);
\r
66 OnModelUpdated(controllerContext, newBindingContext);
\r
70 internal object BindComplexModel(ControllerContext controllerContext, ModelBindingContext bindingContext) {
\r
71 object model = bindingContext.Model;
\r
72 Type modelType = bindingContext.ModelType;
\r
74 // if we're being asked to create an array, create a list instead, then coerce to an array after the list is created
\r
75 if (model == null && modelType.IsArray) {
\r
76 Type elementType = modelType.GetElementType();
\r
77 Type listType = typeof(List<>).MakeGenericType(elementType);
\r
78 object collection = CreateModel(controllerContext, bindingContext, listType);
\r
80 ModelBindingContext arrayBindingContext = new ModelBindingContext() {
\r
81 ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => collection, listType),
\r
82 ModelName = bindingContext.ModelName,
\r
83 ModelState = bindingContext.ModelState,
\r
84 PropertyFilter = bindingContext.PropertyFilter,
\r
85 ValueProvider = bindingContext.ValueProvider
\r
87 IList list = (IList)UpdateCollection(controllerContext, arrayBindingContext, elementType);
\r
93 Array array = Array.CreateInstance(elementType, list.Count);
\r
94 list.CopyTo(array, 0);
\r
98 if (model == null) {
\r
99 model = CreateModel(controllerContext, bindingContext, modelType);
\r
102 // special-case IDictionary<,> and ICollection<>
\r
103 Type dictionaryType = TypeHelpers.ExtractGenericInterface(modelType, typeof(IDictionary<,>));
\r
104 if (dictionaryType != null) {
\r
105 Type[] genericArguments = dictionaryType.GetGenericArguments();
\r
106 Type keyType = genericArguments[0];
\r
107 Type valueType = genericArguments[1];
\r
109 ModelBindingContext dictionaryBindingContext = new ModelBindingContext() {
\r
110 ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, modelType),
\r
111 ModelName = bindingContext.ModelName,
\r
112 ModelState = bindingContext.ModelState,
\r
113 PropertyFilter = bindingContext.PropertyFilter,
\r
114 ValueProvider = bindingContext.ValueProvider
\r
116 object dictionary = UpdateDictionary(controllerContext, dictionaryBindingContext, keyType, valueType);
\r
120 Type enumerableType = TypeHelpers.ExtractGenericInterface(modelType, typeof(IEnumerable<>));
\r
121 if (enumerableType != null) {
\r
122 Type elementType = enumerableType.GetGenericArguments()[0];
\r
124 Type collectionType = typeof(ICollection<>).MakeGenericType(elementType);
\r
125 if (collectionType.IsInstanceOfType(model)) {
\r
126 ModelBindingContext collectionBindingContext = new ModelBindingContext() {
\r
127 ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, modelType),
\r
128 ModelName = bindingContext.ModelName,
\r
129 ModelState = bindingContext.ModelState,
\r
130 PropertyFilter = bindingContext.PropertyFilter,
\r
131 ValueProvider = bindingContext.ValueProvider
\r
133 object collection = UpdateCollection(controllerContext, collectionBindingContext, elementType);
\r
138 // otherwise, just update the properties on the complex type
\r
139 BindComplexElementalModel(controllerContext, bindingContext, model);
\r
143 public virtual object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) {
\r
144 if (bindingContext == null) {
\r
145 throw new ArgumentNullException("bindingContext");
\r
148 bool performedFallback = false;
\r
150 if (!String.IsNullOrEmpty(bindingContext.ModelName) && !bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName)) {
\r
151 // We couldn't find any entry that began with the prefix. If this is the top-level element, fall back
\r
152 // to the empty prefix.
\r
153 if (bindingContext.FallbackToEmptyPrefix) {
\r
154 bindingContext = new ModelBindingContext() {
\r
155 ModelMetadata = bindingContext.ModelMetadata,
\r
156 ModelState = bindingContext.ModelState,
\r
157 PropertyFilter = bindingContext.PropertyFilter,
\r
158 ValueProvider = bindingContext.ValueProvider
\r
160 performedFallback = true;
\r
167 // Simple model = int, string, etc.; determined by calling TypeConverter.CanConvertFrom(typeof(string))
\r
168 // or by seeing if a value in the request exactly matches the name of the model we're binding.
\r
169 // Complex type = everything else.
\r
170 if (!performedFallback) {
\r
171 ValueProviderResult vpResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
\r
172 if (vpResult != null) {
\r
173 return BindSimpleModel(controllerContext, bindingContext, vpResult);
\r
176 if (!bindingContext.ModelMetadata.IsComplexType) {
\r
180 return BindComplexModel(controllerContext, bindingContext);
\r
183 private void BindProperties(ControllerContext controllerContext, ModelBindingContext bindingContext) {
\r
184 IEnumerable<PropertyDescriptor> properties = GetFilteredModelProperties(controllerContext, bindingContext);
\r
185 foreach (PropertyDescriptor property in properties) {
\r
186 BindProperty(controllerContext, bindingContext, property);
\r
190 protected virtual void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor) {
\r
191 // need to skip properties that aren't part of the request, else we might hit a StackOverflowException
\r
192 string fullPropertyKey = CreateSubPropertyName(bindingContext.ModelName, propertyDescriptor.Name);
\r
193 if (!bindingContext.ValueProvider.ContainsPrefix(fullPropertyKey)) {
\r
197 // call into the property's model binder
\r
198 IModelBinder propertyBinder = Binders.GetBinder(propertyDescriptor.PropertyType);
\r
199 object originalPropertyValue = propertyDescriptor.GetValue(bindingContext.Model);
\r
200 ModelMetadata propertyMetadata = bindingContext.PropertyMetadata[propertyDescriptor.Name];
\r
201 propertyMetadata.Model = originalPropertyValue;
\r
202 ModelBindingContext innerBindingContext = new ModelBindingContext() {
\r
203 ModelMetadata = propertyMetadata,
\r
204 ModelName = fullPropertyKey,
\r
205 ModelState = bindingContext.ModelState,
\r
206 ValueProvider = bindingContext.ValueProvider
\r
208 object newPropertyValue = GetPropertyValue(controllerContext, innerBindingContext, propertyDescriptor, propertyBinder);
\r
209 propertyMetadata.Model = newPropertyValue;
\r
212 ModelState modelState = bindingContext.ModelState[fullPropertyKey];
\r
213 if (modelState == null || modelState.Errors.Count == 0) {
\r
214 if (OnPropertyValidating(controllerContext, bindingContext, propertyDescriptor, newPropertyValue)) {
\r
215 SetProperty(controllerContext, bindingContext, propertyDescriptor, newPropertyValue);
\r
216 OnPropertyValidated(controllerContext, bindingContext, propertyDescriptor, newPropertyValue);
\r
220 SetProperty(controllerContext, bindingContext, propertyDescriptor, newPropertyValue);
\r
222 // Convert FormatExceptions (type conversion failures) into InvalidValue messages
\r
223 foreach (ModelError error in modelState.Errors.Where(err => String.IsNullOrEmpty(err.ErrorMessage) && err.Exception != null).ToList()) {
\r
224 for (Exception exception = error.Exception; exception != null; exception = exception.InnerException) {
\r
225 if (exception is FormatException) {
\r
226 string displayName = propertyMetadata.GetDisplayName();
\r
227 string errorMessageTemplate = GetValueInvalidResource(controllerContext);
\r
228 string errorMessage = String.Format(CultureInfo.CurrentUICulture, errorMessageTemplate, modelState.Value.AttemptedValue, displayName);
\r
229 modelState.Errors.Remove(error);
\r
230 modelState.Errors.Add(errorMessage);
\r
238 internal object BindSimpleModel(ControllerContext controllerContext, ModelBindingContext bindingContext, ValueProviderResult valueProviderResult) {
\r
239 bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);
\r
241 // if the value provider returns an instance of the requested data type, we can just short-circuit
\r
242 // the evaluation and return that instance
\r
243 if (bindingContext.ModelType.IsInstanceOfType(valueProviderResult.RawValue)) {
\r
244 return valueProviderResult.RawValue;
\r
247 // since a string is an IEnumerable<char>, we want it to skip the two checks immediately following
\r
248 if (bindingContext.ModelType != typeof(string)) {
\r
250 // conversion results in 3 cases, as below
\r
251 if (bindingContext.ModelType.IsArray) {
\r
252 // case 1: user asked for an array
\r
253 // ValueProviderResult.ConvertTo() understands array types, so pass in the array type directly
\r
254 object modelArray = ConvertProviderResult(bindingContext.ModelState, bindingContext.ModelName, valueProviderResult, bindingContext.ModelType);
\r
258 Type enumerableType = TypeHelpers.ExtractGenericInterface(bindingContext.ModelType, typeof(IEnumerable<>));
\r
259 if (enumerableType != null) {
\r
260 // case 2: user asked for a collection rather than an array
\r
261 // need to call ConvertTo() on the array type, then copy the array to the collection
\r
262 object modelCollection = CreateModel(controllerContext, bindingContext, bindingContext.ModelType);
\r
263 Type elementType = enumerableType.GetGenericArguments()[0];
\r
264 Type arrayType = elementType.MakeArrayType();
\r
265 object modelArray = ConvertProviderResult(bindingContext.ModelState, bindingContext.ModelName, valueProviderResult, arrayType);
\r
267 Type collectionType = typeof(ICollection<>).MakeGenericType(elementType);
\r
268 if (collectionType.IsInstanceOfType(modelCollection)) {
\r
269 CollectionHelpers.ReplaceCollection(elementType, modelCollection, modelArray);
\r
271 return modelCollection;
\r
275 // case 3: user asked for an individual element
\r
276 object model = ConvertProviderResult(bindingContext.ModelState, bindingContext.ModelName, valueProviderResult, bindingContext.ModelType);
\r
280 private static bool CanUpdateReadonlyTypedReference(Type type) {
\r
281 // value types aren't strictly immutable, but because they have copy-by-value semantics
\r
282 // we can't update a value type that is marked readonly
\r
283 if (type.IsValueType) {
\r
287 // arrays are mutable, but because we can't change their length we shouldn't try
\r
288 // to update an array that is referenced readonly
\r
289 if (type.IsArray) {
\r
293 // special-case known common immutable types
\r
294 if (type == typeof(string)) {
\r
301 [SuppressMessage("Microsoft.Globalization", "CA1304:SpecifyCultureInfo", MessageId = "System.Web.Mvc.ValueProviderResult.ConvertTo(System.Type)",
\r
302 Justification = "The target object should make the correct culture determination, not this method.")]
\r
303 [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes",
\r
304 Justification = "We're recording this exception so that we can act on it later.")]
\r
305 private static object ConvertProviderResult(ModelStateDictionary modelState, string modelStateKey, ValueProviderResult valueProviderResult, Type destinationType) {
\r
307 object convertedValue = valueProviderResult.ConvertTo(destinationType);
\r
308 return convertedValue;
\r
310 catch (Exception ex) {
\r
311 modelState.AddModelError(modelStateKey, ex);
\r
316 internal ModelBindingContext CreateComplexElementalModelBindingContext(ControllerContext controllerContext, ModelBindingContext bindingContext, object model) {
\r
317 BindAttribute bindAttr = (BindAttribute)GetTypeDescriptor(controllerContext, bindingContext).GetAttributes()[typeof(BindAttribute)];
\r
318 Predicate<string> newPropertyFilter = (bindAttr != null)
\r
319 ? propertyName => bindAttr.IsPropertyAllowed(propertyName) && bindingContext.PropertyFilter(propertyName)
\r
320 : bindingContext.PropertyFilter;
\r
322 ModelBindingContext newBindingContext = new ModelBindingContext() {
\r
323 ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, bindingContext.ModelType),
\r
324 ModelName = bindingContext.ModelName,
\r
325 ModelState = bindingContext.ModelState,
\r
326 PropertyFilter = newPropertyFilter,
\r
327 ValueProvider = bindingContext.ValueProvider
\r
330 return newBindingContext;
\r
333 protected virtual object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType) {
\r
334 Type typeToCreate = modelType;
\r
336 // we can understand some collection interfaces, e.g. IList<>, IDictionary<,>
\r
337 if (modelType.IsGenericType) {
\r
338 Type genericTypeDefinition = modelType.GetGenericTypeDefinition();
\r
339 if (genericTypeDefinition == typeof(IDictionary<,>)) {
\r
340 typeToCreate = typeof(Dictionary<,>).MakeGenericType(modelType.GetGenericArguments());
\r
342 else if (genericTypeDefinition == typeof(IEnumerable<>) || genericTypeDefinition == typeof(ICollection<>) || genericTypeDefinition == typeof(IList<>)) {
\r
343 typeToCreate = typeof(List<>).MakeGenericType(modelType.GetGenericArguments());
\r
347 // fallback to the type's default constructor
\r
348 return Activator.CreateInstance(typeToCreate);
\r
351 protected static string CreateSubIndexName(string prefix, int index) {
\r
352 return String.Format(CultureInfo.InvariantCulture, "{0}[{1}]", prefix, index);
\r
355 protected static string CreateSubIndexName(string prefix, string index) {
\r
356 return String.Format(CultureInfo.InvariantCulture, "{0}[{1}]", prefix, index);
\r
359 protected internal static string CreateSubPropertyName(string prefix, string propertyName) {
\r
360 if (String.IsNullOrEmpty(prefix)) {
\r
361 return propertyName;
\r
363 else if (String.IsNullOrEmpty(propertyName)) {
\r
367 return prefix + "." + propertyName;
\r
371 protected IEnumerable<PropertyDescriptor> GetFilteredModelProperties(ControllerContext controllerContext, ModelBindingContext bindingContext) {
\r
372 PropertyDescriptorCollection properties = GetModelProperties(controllerContext, bindingContext);
\r
373 Predicate<string> propertyFilter = bindingContext.PropertyFilter;
\r
375 return from PropertyDescriptor property in properties
\r
376 where ShouldUpdateProperty(property, propertyFilter)
\r
380 [SuppressMessage("Microsoft.Globalization", "CA1304:SpecifyCultureInfo", MessageId = "System.Web.Mvc.ValueProviderResult.ConvertTo(System.Type)",
\r
381 Justification = "ValueProviderResult already handles culture conversion appropriately.")]
\r
382 private static void GetIndexes(ModelBindingContext bindingContext, out bool stopOnIndexNotFound, out IEnumerable<string> indexes) {
\r
383 string indexKey = CreateSubPropertyName(bindingContext.ModelName, "index");
\r
384 ValueProviderResult vpResult = bindingContext.ValueProvider.GetValue(indexKey);
\r
386 if (vpResult != null) {
\r
387 string[] indexesArray = vpResult.ConvertTo(typeof(string[])) as string[];
\r
388 if (indexesArray != null) {
\r
389 stopOnIndexNotFound = false;
\r
390 indexes = indexesArray;
\r
395 // just use a simple zero-based system
\r
396 stopOnIndexNotFound = true;
\r
397 indexes = GetZeroBasedIndexes();
\r
400 protected virtual PropertyDescriptorCollection GetModelProperties(ControllerContext controllerContext, ModelBindingContext bindingContext) {
\r
401 return GetTypeDescriptor(controllerContext, bindingContext).GetProperties();
\r
404 protected virtual object GetPropertyValue(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, IModelBinder propertyBinder) {
\r
405 object value = propertyBinder.BindModel(controllerContext, bindingContext);
\r
407 if (bindingContext.ModelMetadata.ConvertEmptyStringToNull && Object.Equals(value, String.Empty)) {
\r
414 protected virtual ICustomTypeDescriptor GetTypeDescriptor(ControllerContext controllerContext, ModelBindingContext bindingContext) {
\r
415 return TypeDescriptorHelper.Get(bindingContext.ModelType);
\r
418 // If the user specified a ResourceClassKey try to load the resource they specified.
\r
419 // If the class key is invalid, an exception will be thrown.
\r
420 // If the class key is valid but the resource is not found, it returns null, in which
\r
421 // case it will fall back to the MVC default error message.
\r
422 private static string GetUserResourceString(ControllerContext controllerContext, string resourceName) {
\r
423 string result = null;
\r
425 if (!String.IsNullOrEmpty(ResourceClassKey) && (controllerContext != null) && (controllerContext.HttpContext != null)) {
\r
426 result = controllerContext.HttpContext.GetGlobalResourceObject(ResourceClassKey, resourceName, CultureInfo.CurrentUICulture) as string;
\r
432 private static string GetValueInvalidResource(ControllerContext controllerContext) {
\r
433 return GetUserResourceString(controllerContext, "PropertyValueInvalid") ?? MvcResources.DefaultModelBinder_ValueInvalid;
\r
436 private static string GetValueRequiredResource(ControllerContext controllerContext) {
\r
437 return GetUserResourceString(controllerContext, "PropertyValueRequired") ?? MvcResources.DefaultModelBinder_ValueRequired;
\r
440 private static IEnumerable<string> GetZeroBasedIndexes() {
\r
441 for (int i = 0; ; i++) {
\r
442 yield return i.ToString(CultureInfo.InvariantCulture);
\r
446 protected static bool IsModelValid(ModelBindingContext bindingContext) {
\r
447 if (bindingContext == null) {
\r
448 throw new ArgumentNullException("bindingContext");
\r
450 if (String.IsNullOrEmpty(bindingContext.ModelName)) {
\r
451 return bindingContext.ModelState.IsValid;
\r
453 return bindingContext.ModelState.IsValidField(bindingContext.ModelName);
\r
456 protected virtual void OnModelUpdated(ControllerContext controllerContext, ModelBindingContext bindingContext) {
\r
457 Dictionary<string, bool> startedValid = new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
\r
459 foreach (ModelValidationResult validationResult in ModelValidator.GetModelValidator(bindingContext.ModelMetadata, controllerContext).Validate(null)) {
\r
460 string subPropertyName = CreateSubPropertyName(bindingContext.ModelName, validationResult.MemberName);
\r
462 if (!startedValid.ContainsKey(subPropertyName)) {
\r
463 startedValid[subPropertyName] = bindingContext.ModelState.IsValidField(subPropertyName);
\r
466 if (startedValid[subPropertyName]) {
\r
467 bindingContext.ModelState.AddModelError(subPropertyName, validationResult.Message);
\r
472 protected virtual bool OnModelUpdating(ControllerContext controllerContext, ModelBindingContext bindingContext) {
\r
473 // default implementation does nothing
\r
477 protected virtual void OnPropertyValidated(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, object value) {
\r
478 // default implementation does nothing
\r
481 protected virtual bool OnPropertyValidating(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, object value) {
\r
482 // default implementation does nothing
\r
486 [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes",
\r
487 Justification = "We're recording this exception so that we can act on it later.")]
\r
488 protected virtual void SetProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, object value) {
\r
490 ModelMetadata propertyMetadata = bindingContext.PropertyMetadata[propertyDescriptor.Name];
\r
491 propertyMetadata.Model = value;
\r
492 string modelStateKey = CreateSubPropertyName(bindingContext.ModelName, propertyMetadata.PropertyName);
\r
494 // If the value is null, and the validation system can find a Required validator for
\r
495 // us, we'd prefer to run it before we attempt to set the value; otherwise, property
\r
496 // setters which throw on null (f.e., Entity Framework properties which are backed by
\r
497 // non-nullable strings in the DB) will get their error message in ahead of us.
\r
499 // We are effectively using the special validator -- Required -- as a helper to the
\r
500 // binding system, which is why this code is here instead of in the Validating/Validated
\r
501 // methods, which are really the old-school validation hooks.
\r
502 if (value == null && bindingContext.ModelState.IsValidField(modelStateKey)) {
\r
503 ModelValidator requiredValidator = ModelValidatorProviders.Providers.GetValidators(propertyMetadata, controllerContext).Where(v => v.IsRequired).FirstOrDefault();
\r
504 if (requiredValidator != null) {
\r
505 foreach (ModelValidationResult validationResult in requiredValidator.Validate(bindingContext.Model)) {
\r
506 bindingContext.ModelState.AddModelError(modelStateKey, validationResult.Message);
\r
511 bool isNullValueOnNonNullableType =
\r
513 !TypeHelpers.TypeAllowsNullValue(propertyDescriptor.PropertyType);
\r
515 // Try to set a value into the property unless we know it will fail (read-only
\r
516 // properties and null values with non-nullable types)
\r
517 if (!propertyDescriptor.IsReadOnly && !isNullValueOnNonNullableType) {
\r
519 propertyDescriptor.SetValue(bindingContext.Model, value);
\r
521 catch (Exception ex) {
\r
522 // Only add if we're not already invalid
\r
523 if (bindingContext.ModelState.IsValidField(modelStateKey)) {
\r
524 bindingContext.ModelState.AddModelError(modelStateKey, ex);
\r
529 // Last chance for an error on null values with non-nullable types, we'll use
\r
530 // the default "A value is required." message.
\r
531 if (isNullValueOnNonNullableType && bindingContext.ModelState.IsValidField(modelStateKey)) {
\r
532 bindingContext.ModelState.AddModelError(modelStateKey, GetValueRequiredResource(controllerContext));
\r
536 private static bool ShouldUpdateProperty(PropertyDescriptor property, Predicate<string> propertyFilter) {
\r
537 if (property.IsReadOnly && !CanUpdateReadonlyTypedReference(property.PropertyType)) {
\r
541 // if this property is rejected by the filter, move on
\r
542 if (!propertyFilter(property.Name)) {
\r
546 // otherwise, allow
\r
550 internal object UpdateCollection(ControllerContext controllerContext, ModelBindingContext bindingContext, Type elementType) {
\r
551 bool stopOnIndexNotFound;
\r
552 IEnumerable<string> indexes;
\r
553 GetIndexes(bindingContext, out stopOnIndexNotFound, out indexes);
\r
554 IModelBinder elementBinder = Binders.GetBinder(elementType);
\r
556 // build up a list of items from the request
\r
557 List<object> modelList = new List<object>();
\r
558 foreach (string currentIndex in indexes) {
\r
559 string subIndexKey = CreateSubIndexName(bindingContext.ModelName, currentIndex);
\r
560 if (!bindingContext.ValueProvider.ContainsPrefix(subIndexKey)) {
\r
561 if (stopOnIndexNotFound) {
\r
562 // we ran out of elements to pull
\r
570 ModelBindingContext innerContext = new ModelBindingContext() {
\r
571 ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, elementType),
\r
572 ModelName = subIndexKey,
\r
573 ModelState = bindingContext.ModelState,
\r
574 PropertyFilter = bindingContext.PropertyFilter,
\r
575 ValueProvider = bindingContext.ValueProvider
\r
577 object thisElement = elementBinder.BindModel(controllerContext, innerContext);
\r
579 // we need to merge model errors up
\r
580 AddValueRequiredMessageToModelState(controllerContext, bindingContext.ModelState, subIndexKey, elementType, thisElement);
\r
581 modelList.Add(thisElement);
\r
584 // if there weren't any elements at all in the request, just return
\r
585 if (modelList.Count == 0) {
\r
589 // replace the original collection
\r
590 object collection = bindingContext.Model;
\r
591 CollectionHelpers.ReplaceCollection(elementType, collection, modelList);
\r
595 internal object UpdateDictionary(ControllerContext controllerContext, ModelBindingContext bindingContext, Type keyType, Type valueType) {
\r
596 bool stopOnIndexNotFound;
\r
597 IEnumerable<string> indexes;
\r
598 GetIndexes(bindingContext, out stopOnIndexNotFound, out indexes);
\r
600 IModelBinder keyBinder = Binders.GetBinder(keyType);
\r
601 IModelBinder valueBinder = Binders.GetBinder(valueType);
\r
603 // build up a list of items from the request
\r
604 List<KeyValuePair<object, object>> modelList = new List<KeyValuePair<object, object>>();
\r
605 foreach (string currentIndex in indexes) {
\r
606 string subIndexKey = CreateSubIndexName(bindingContext.ModelName, currentIndex);
\r
607 string keyFieldKey = CreateSubPropertyName(subIndexKey, "key");
\r
608 string valueFieldKey = CreateSubPropertyName(subIndexKey, "value");
\r
610 if (!(bindingContext.ValueProvider.ContainsPrefix(keyFieldKey) && bindingContext.ValueProvider.ContainsPrefix(valueFieldKey))) {
\r
611 if (stopOnIndexNotFound) {
\r
612 // we ran out of elements to pull
\r
621 ModelBindingContext keyBindingContext = new ModelBindingContext() {
\r
622 ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, keyType),
\r
623 ModelName = keyFieldKey,
\r
624 ModelState = bindingContext.ModelState,
\r
625 ValueProvider = bindingContext.ValueProvider
\r
627 object thisKey = keyBinder.BindModel(controllerContext, keyBindingContext);
\r
629 // we need to merge model errors up
\r
630 AddValueRequiredMessageToModelState(controllerContext, bindingContext.ModelState, keyFieldKey, keyType, thisKey);
\r
631 if (!keyType.IsInstanceOfType(thisKey)) {
\r
632 // we can't add an invalid key, so just move on
\r
637 ModelBindingContext valueBindingContext = new ModelBindingContext() {
\r
638 ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, valueType),
\r
639 ModelName = valueFieldKey,
\r
640 ModelState = bindingContext.ModelState,
\r
641 PropertyFilter = bindingContext.PropertyFilter,
\r
642 ValueProvider = bindingContext.ValueProvider
\r
644 object thisValue = valueBinder.BindModel(controllerContext, valueBindingContext);
\r
646 // we need to merge model errors up
\r
647 AddValueRequiredMessageToModelState(controllerContext, bindingContext.ModelState, valueFieldKey, valueType, thisValue);
\r
648 KeyValuePair<object, object> kvp = new KeyValuePair<object, object>(thisKey, thisValue);
\r
649 modelList.Add(kvp);
\r
652 // if there weren't any elements at all in the request, just return
\r
653 if (modelList.Count == 0) {
\r
657 // replace the original collection
\r
658 object dictionary = bindingContext.Model;
\r
659 CollectionHelpers.ReplaceDictionary(keyType, valueType, dictionary, modelList);
\r
663 // This helper type is used because we're working with strongly-typed collections, but we don't know the Ts
\r
664 // ahead of time. By using the generic methods below, we can consolidate the collection-specific code in a
\r
665 // single helper type rather than having reflection-based calls spread throughout the DefaultModelBinder type.
\r
666 // There is a single point of entry to each of the methods below, so they're fairly simple to maintain.
\r
668 private static class CollectionHelpers {
\r
670 private static readonly MethodInfo _replaceCollectionMethod = typeof(CollectionHelpers).GetMethod("ReplaceCollectionImpl", BindingFlags.Static | BindingFlags.NonPublic);
\r
671 private static readonly MethodInfo _replaceDictionaryMethod = typeof(CollectionHelpers).GetMethod("ReplaceDictionaryImpl", BindingFlags.Static | BindingFlags.NonPublic);
\r
673 [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
\r
674 public static void ReplaceCollection(Type collectionType, object collection, object newContents) {
\r
675 MethodInfo targetMethod = _replaceCollectionMethod.MakeGenericMethod(collectionType);
\r
676 targetMethod.Invoke(null, new object[] { collection, newContents });
\r
679 private static void ReplaceCollectionImpl<T>(ICollection<T> collection, IEnumerable newContents) {
\r
680 collection.Clear();
\r
681 if (newContents != null) {
\r
682 foreach (object item in newContents) {
\r
683 // if the item was not a T, some conversion failed. the error message will be propagated,
\r
684 // but in the meanwhile we need to make a placeholder element in the array.
\r
685 T castItem = (item is T) ? (T)item : default(T);
\r
686 collection.Add(castItem);
\r
691 [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
\r
692 public static void ReplaceDictionary(Type keyType, Type valueType, object dictionary, object newContents) {
\r
693 MethodInfo targetMethod = _replaceDictionaryMethod.MakeGenericMethod(keyType, valueType);
\r
694 targetMethod.Invoke(null, new object[] { dictionary, newContents });
\r
697 private static void ReplaceDictionaryImpl<TKey, TValue>(IDictionary<TKey, TValue> dictionary, IEnumerable<KeyValuePair<object, object>> newContents) {
\r
698 dictionary.Clear();
\r
699 foreach (KeyValuePair<object, object> item in newContents) {
\r
700 // if the item was not a T, some conversion failed. the error message will be propagated,
\r
701 // but in the meanwhile we need to make a placeholder element in the dictionary.
\r
702 TKey castKey = (TKey)item.Key; // this cast shouldn't fail
\r
703 TValue castValue = (item.Value is TValue) ? (TValue)item.Value : default(TValue);
\r
704 dictionary[castKey] = castValue;
\r