PageRenderTime 53ms CodeModel.GetById 19ms RepoModel.GetById 1ms app.codeStats 0ms

/mcs/class/System.Web.Mvc2/System.Web.Mvc/DefaultModelBinder.cs

https://bitbucket.org/foobar22/mono
C# | 709 lines | 517 code | 106 blank | 86 comment | 113 complexity | 72ad651cfc1279c4e80f59dda181a3fb MD5 | raw file
Possible License(s): MPL-2.0-no-copyleft-exception, CC-BY-SA-3.0, GPL-2.0, Unlicense, Apache-2.0, LGPL-2.0
  1. /* ****************************************************************************
  2. *
  3. * Copyright (c) Microsoft Corporation. All rights reserved.
  4. *
  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.
  8. *
  9. * You must not remove this notice, or any other, from this software.
  10. *
  11. * ***************************************************************************/
  12. namespace System.Web.Mvc {
  13. using System;
  14. using System.Collections;
  15. using System.Collections.Generic;
  16. using System.ComponentModel;
  17. using System.Diagnostics.CodeAnalysis;
  18. using System.Globalization;
  19. using System.Linq;
  20. using System.Reflection;
  21. using System.Runtime.CompilerServices;
  22. using System.Web.Mvc.Resources;
  23. public class DefaultModelBinder : IModelBinder {
  24. private ModelBinderDictionary _binders;
  25. private static string _resourceClassKey;
  26. [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly",
  27. Justification = "Property is settable so that the dictionary can be provided for unit testing purposes.")]
  28. protected internal ModelBinderDictionary Binders {
  29. get {
  30. if (_binders == null) {
  31. _binders = ModelBinders.Binders;
  32. }
  33. return _binders;
  34. }
  35. set {
  36. _binders = value;
  37. }
  38. }
  39. public static string ResourceClassKey {
  40. get {
  41. return _resourceClassKey ?? String.Empty;
  42. }
  43. set {
  44. _resourceClassKey = value;
  45. }
  46. }
  47. private static void AddValueRequiredMessageToModelState(ControllerContext controllerContext, ModelStateDictionary modelState, string modelStateKey, Type elementType, object value) {
  48. if (value == null && !TypeHelpers.TypeAllowsNullValue(elementType) && modelState.IsValidField(modelStateKey)) {
  49. modelState.AddModelError(modelStateKey, GetValueRequiredResource(controllerContext));
  50. }
  51. }
  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. ModelBindingContext newBindingContext = CreateComplexElementalModelBindingContext(controllerContext, bindingContext, model);
  55. // validation
  56. if (OnModelUpdating(controllerContext, newBindingContext)) {
  57. BindProperties(controllerContext, newBindingContext);
  58. OnModelUpdated(controllerContext, newBindingContext);
  59. }
  60. }
  61. internal object BindComplexModel(ControllerContext controllerContext, ModelBindingContext bindingContext) {
  62. object model = bindingContext.Model;
  63. Type modelType = bindingContext.ModelType;
  64. // if we're being asked to create an array, create a list instead, then coerce to an array after the list is created
  65. if (model == null && modelType.IsArray) {
  66. Type elementType = modelType.GetElementType();
  67. Type listType = typeof(List<>).MakeGenericType(elementType);
  68. object collection = CreateModel(controllerContext, bindingContext, listType);
  69. ModelBindingContext arrayBindingContext = new ModelBindingContext() {
  70. ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => collection, listType),
  71. ModelName = bindingContext.ModelName,
  72. ModelState = bindingContext.ModelState,
  73. PropertyFilter = bindingContext.PropertyFilter,
  74. ValueProvider = bindingContext.ValueProvider
  75. };
  76. IList list = (IList)UpdateCollection(controllerContext, arrayBindingContext, elementType);
  77. if (list == null) {
  78. return null;
  79. }
  80. Array array = Array.CreateInstance(elementType, list.Count);
  81. list.CopyTo(array, 0);
  82. return array;
  83. }
  84. if (model == null) {
  85. model = CreateModel(controllerContext, bindingContext, modelType);
  86. }
  87. // special-case IDictionary<,> and ICollection<>
  88. Type dictionaryType = TypeHelpers.ExtractGenericInterface(modelType, typeof(IDictionary<,>));
  89. if (dictionaryType != null) {
  90. Type[] genericArguments = dictionaryType.GetGenericArguments();
  91. Type keyType = genericArguments[0];
  92. Type valueType = genericArguments[1];
  93. ModelBindingContext dictionaryBindingContext = new ModelBindingContext() {
  94. ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, modelType),
  95. ModelName = bindingContext.ModelName,
  96. ModelState = bindingContext.ModelState,
  97. PropertyFilter = bindingContext.PropertyFilter,
  98. ValueProvider = bindingContext.ValueProvider
  99. };
  100. object dictionary = UpdateDictionary(controllerContext, dictionaryBindingContext, keyType, valueType);
  101. return dictionary;
  102. }
  103. Type enumerableType = TypeHelpers.ExtractGenericInterface(modelType, typeof(IEnumerable<>));
  104. if (enumerableType != null) {
  105. Type elementType = enumerableType.GetGenericArguments()[0];
  106. Type collectionType = typeof(ICollection<>).MakeGenericType(elementType);
  107. if (collectionType.IsInstanceOfType(model)) {
  108. ModelBindingContext collectionBindingContext = new ModelBindingContext() {
  109. ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, modelType),
  110. ModelName = bindingContext.ModelName,
  111. ModelState = bindingContext.ModelState,
  112. PropertyFilter = bindingContext.PropertyFilter,
  113. ValueProvider = bindingContext.ValueProvider
  114. };
  115. object collection = UpdateCollection(controllerContext, collectionBindingContext, elementType);
  116. return collection;
  117. }
  118. }
  119. // otherwise, just update the properties on the complex type
  120. BindComplexElementalModel(controllerContext, bindingContext, model);
  121. return model;
  122. }
  123. public virtual object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) {
  124. if (bindingContext == null) {
  125. throw new ArgumentNullException("bindingContext");
  126. }
  127. bool performedFallback = false;
  128. if (!String.IsNullOrEmpty(bindingContext.ModelName) && !bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName)) {
  129. // We couldn't find any entry that began with the prefix. If this is the top-level element, fall back
  130. // to the empty prefix.
  131. if (bindingContext.FallbackToEmptyPrefix) {
  132. bindingContext = new ModelBindingContext() {
  133. ModelMetadata = bindingContext.ModelMetadata,
  134. ModelState = bindingContext.ModelState,
  135. PropertyFilter = bindingContext.PropertyFilter,
  136. ValueProvider = bindingContext.ValueProvider
  137. };
  138. performedFallback = true;
  139. }
  140. else {
  141. return null;
  142. }
  143. }
  144. // Simple model = int, string, etc.; determined by calling TypeConverter.CanConvertFrom(typeof(string))
  145. // or by seeing if a value in the request exactly matches the name of the model we're binding.
  146. // Complex type = everything else.
  147. if (!performedFallback) {
  148. ValueProviderResult vpResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
  149. if (vpResult != null) {
  150. return BindSimpleModel(controllerContext, bindingContext, vpResult);
  151. }
  152. }
  153. if (!bindingContext.ModelMetadata.IsComplexType) {
  154. return null;
  155. }
  156. return BindComplexModel(controllerContext, bindingContext);
  157. }
  158. private void BindProperties(ControllerContext controllerContext, ModelBindingContext bindingContext) {
  159. IEnumerable<PropertyDescriptor> properties = GetFilteredModelProperties(controllerContext, bindingContext);
  160. foreach (PropertyDescriptor property in properties) {
  161. BindProperty(controllerContext, bindingContext, property);
  162. }
  163. }
  164. protected virtual void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor) {
  165. // need to skip properties that aren't part of the request, else we might hit a StackOverflowException
  166. string fullPropertyKey = CreateSubPropertyName(bindingContext.ModelName, propertyDescriptor.Name);
  167. if (!bindingContext.ValueProvider.ContainsPrefix(fullPropertyKey)) {
  168. return;
  169. }
  170. // call into the property's model binder
  171. IModelBinder propertyBinder = Binders.GetBinder(propertyDescriptor.PropertyType);
  172. object originalPropertyValue = propertyDescriptor.GetValue(bindingContext.Model);
  173. ModelMetadata propertyMetadata = bindingContext.PropertyMetadata[propertyDescriptor.Name];
  174. propertyMetadata.Model = originalPropertyValue;
  175. ModelBindingContext innerBindingContext = new ModelBindingContext() {
  176. ModelMetadata = propertyMetadata,
  177. ModelName = fullPropertyKey,
  178. ModelState = bindingContext.ModelState,
  179. ValueProvider = bindingContext.ValueProvider
  180. };
  181. object newPropertyValue = GetPropertyValue(controllerContext, innerBindingContext, propertyDescriptor, propertyBinder);
  182. propertyMetadata.Model = newPropertyValue;
  183. // validation
  184. ModelState modelState = bindingContext.ModelState[fullPropertyKey];
  185. if (modelState == null || modelState.Errors.Count == 0) {
  186. if (OnPropertyValidating(controllerContext, bindingContext, propertyDescriptor, newPropertyValue)) {
  187. SetProperty(controllerContext, bindingContext, propertyDescriptor, newPropertyValue);
  188. OnPropertyValidated(controllerContext, bindingContext, propertyDescriptor, newPropertyValue);
  189. }
  190. }
  191. else {
  192. SetProperty(controllerContext, bindingContext, propertyDescriptor, newPropertyValue);
  193. // Convert FormatExceptions (type conversion failures) into InvalidValue messages
  194. foreach (ModelError error in modelState.Errors.Where(err => String.IsNullOrEmpty(err.ErrorMessage) && err.Exception != null).ToList()) {
  195. for (Exception exception = error.Exception; exception != null; exception = exception.InnerException) {
  196. if (exception is FormatException) {
  197. string displayName = propertyMetadata.GetDisplayName();
  198. string errorMessageTemplate = GetValueInvalidResource(controllerContext);
  199. string errorMessage = String.Format(CultureInfo.CurrentUICulture, errorMessageTemplate, modelState.Value.AttemptedValue, displayName);
  200. modelState.Errors.Remove(error);
  201. modelState.Errors.Add(errorMessage);
  202. break;
  203. }
  204. }
  205. }
  206. }
  207. }
  208. internal object BindSimpleModel(ControllerContext controllerContext, ModelBindingContext bindingContext, ValueProviderResult valueProviderResult) {
  209. bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);
  210. // if the value provider returns an instance of the requested data type, we can just short-circuit
  211. // the evaluation and return that instance
  212. if (bindingContext.ModelType.IsInstanceOfType(valueProviderResult.RawValue)) {
  213. return valueProviderResult.RawValue;
  214. }
  215. // since a string is an IEnumerable<char>, we want it to skip the two checks immediately following
  216. if (bindingContext.ModelType != typeof(string)) {
  217. // conversion results in 3 cases, as below
  218. if (bindingContext.ModelType.IsArray) {
  219. // case 1: user asked for an array
  220. // ValueProviderResult.ConvertTo() understands array types, so pass in the array type directly
  221. object modelArray = ConvertProviderResult(bindingContext.ModelState, bindingContext.ModelName, valueProviderResult, bindingContext.ModelType);
  222. return modelArray;
  223. }
  224. Type enumerableType = TypeHelpers.ExtractGenericInterface(bindingContext.ModelType, typeof(IEnumerable<>));
  225. if (enumerableType != null) {
  226. // case 2: user asked for a collection rather than an array
  227. // need to call ConvertTo() on the array type, then copy the array to the collection
  228. object modelCollection = CreateModel(controllerContext, bindingContext, bindingContext.ModelType);
  229. Type elementType = enumerableType.GetGenericArguments()[0];
  230. Type arrayType = elementType.MakeArrayType();
  231. object modelArray = ConvertProviderResult(bindingContext.ModelState, bindingContext.ModelName, valueProviderResult, arrayType);
  232. Type collectionType = typeof(ICollection<>).MakeGenericType(elementType);
  233. if (collectionType.IsInstanceOfType(modelCollection)) {
  234. CollectionHelpers.ReplaceCollection(elementType, modelCollection, modelArray);
  235. }
  236. return modelCollection;
  237. }
  238. }
  239. // case 3: user asked for an individual element
  240. object model = ConvertProviderResult(bindingContext.ModelState, bindingContext.ModelName, valueProviderResult, bindingContext.ModelType);
  241. return model;
  242. }
  243. private static bool CanUpdateReadonlyTypedReference(Type type) {
  244. // value types aren't strictly immutable, but because they have copy-by-value semantics
  245. // we can't update a value type that is marked readonly
  246. if (type.IsValueType) {
  247. return false;
  248. }
  249. // arrays are mutable, but because we can't change their length we shouldn't try
  250. // to update an array that is referenced readonly
  251. if (type.IsArray) {
  252. return false;
  253. }
  254. // special-case known common immutable types
  255. if (type == typeof(string)) {
  256. return false;
  257. }
  258. return true;
  259. }
  260. [SuppressMessage("Microsoft.Globalization", "CA1304:SpecifyCultureInfo", MessageId = "System.Web.Mvc.ValueProviderResult.ConvertTo(System.Type)",
  261. Justification = "The target object should make the correct culture determination, not this method.")]
  262. [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes",
  263. Justification = "We're recording this exception so that we can act on it later.")]
  264. private static object ConvertProviderResult(ModelStateDictionary modelState, string modelStateKey, ValueProviderResult valueProviderResult, Type destinationType) {
  265. try {
  266. object convertedValue = valueProviderResult.ConvertTo(destinationType);
  267. return convertedValue;
  268. }
  269. catch (Exception ex) {
  270. modelState.AddModelError(modelStateKey, ex);
  271. return null;
  272. }
  273. }
  274. internal ModelBindingContext CreateComplexElementalModelBindingContext(ControllerContext controllerContext, ModelBindingContext bindingContext, object model) {
  275. BindAttribute bindAttr = (BindAttribute)GetTypeDescriptor(controllerContext, bindingContext).GetAttributes()[typeof(BindAttribute)];
  276. Predicate<string> newPropertyFilter = (bindAttr != null)
  277. ? propertyName => bindAttr.IsPropertyAllowed(propertyName) && bindingContext.PropertyFilter(propertyName)
  278. : bindingContext.PropertyFilter;
  279. ModelBindingContext newBindingContext = new ModelBindingContext() {
  280. ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, bindingContext.ModelType),
  281. ModelName = bindingContext.ModelName,
  282. ModelState = bindingContext.ModelState,
  283. PropertyFilter = newPropertyFilter,
  284. ValueProvider = bindingContext.ValueProvider
  285. };
  286. return newBindingContext;
  287. }
  288. protected virtual object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType) {
  289. Type typeToCreate = modelType;
  290. // we can understand some collection interfaces, e.g. IList<>, IDictionary<,>
  291. if (modelType.IsGenericType) {
  292. Type genericTypeDefinition = modelType.GetGenericTypeDefinition();
  293. if (genericTypeDefinition == typeof(IDictionary<,>)) {
  294. typeToCreate = typeof(Dictionary<,>).MakeGenericType(modelType.GetGenericArguments());
  295. }
  296. else if (genericTypeDefinition == typeof(IEnumerable<>) || genericTypeDefinition == typeof(ICollection<>) || genericTypeDefinition == typeof(IList<>)) {
  297. typeToCreate = typeof(List<>).MakeGenericType(modelType.GetGenericArguments());
  298. }
  299. }
  300. // fallback to the type's default constructor
  301. return Activator.CreateInstance(typeToCreate);
  302. }
  303. protected static string CreateSubIndexName(string prefix, int index) {
  304. return String.Format(CultureInfo.InvariantCulture, "{0}[{1}]", prefix, index);
  305. }
  306. protected static string CreateSubIndexName(string prefix, string index) {
  307. return String.Format(CultureInfo.InvariantCulture, "{0}[{1}]", prefix, index);
  308. }
  309. protected internal static string CreateSubPropertyName(string prefix, string propertyName) {
  310. if (String.IsNullOrEmpty(prefix)) {
  311. return propertyName;
  312. }
  313. else if (String.IsNullOrEmpty(propertyName)) {
  314. return prefix;
  315. }
  316. else {
  317. return prefix + "." + propertyName;
  318. }
  319. }
  320. protected IEnumerable<PropertyDescriptor> GetFilteredModelProperties(ControllerContext controllerContext, ModelBindingContext bindingContext) {
  321. PropertyDescriptorCollection properties = GetModelProperties(controllerContext, bindingContext);
  322. Predicate<string> propertyFilter = bindingContext.PropertyFilter;
  323. return from PropertyDescriptor property in properties
  324. where ShouldUpdateProperty(property, propertyFilter)
  325. select property;
  326. }
  327. [SuppressMessage("Microsoft.Globalization", "CA1304:SpecifyCultureInfo", MessageId = "System.Web.Mvc.ValueProviderResult.ConvertTo(System.Type)",
  328. Justification = "ValueProviderResult already handles culture conversion appropriately.")]
  329. private static void GetIndexes(ModelBindingContext bindingContext, out bool stopOnIndexNotFound, out IEnumerable<string> indexes) {
  330. string indexKey = CreateSubPropertyName(bindingContext.ModelName, "index");
  331. ValueProviderResult vpResult = bindingContext.ValueProvider.GetValue(indexKey);
  332. if (vpResult != null) {
  333. string[] indexesArray = vpResult.ConvertTo(typeof(string[])) as string[];
  334. if (indexesArray != null) {
  335. stopOnIndexNotFound = false;
  336. indexes = indexesArray;
  337. return;
  338. }
  339. }
  340. // just use a simple zero-based system
  341. stopOnIndexNotFound = true;
  342. indexes = GetZeroBasedIndexes();
  343. }
  344. protected virtual PropertyDescriptorCollection GetModelProperties(ControllerContext controllerContext, ModelBindingContext bindingContext) {
  345. return GetTypeDescriptor(controllerContext, bindingContext).GetProperties();
  346. }
  347. protected virtual object GetPropertyValue(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, IModelBinder propertyBinder) {
  348. object value = propertyBinder.BindModel(controllerContext, bindingContext);
  349. if (bindingContext.ModelMetadata.ConvertEmptyStringToNull && Object.Equals(value, String.Empty)) {
  350. return null;
  351. }
  352. return value;
  353. }
  354. protected virtual ICustomTypeDescriptor GetTypeDescriptor(ControllerContext controllerContext, ModelBindingContext bindingContext) {
  355. return TypeDescriptorHelper.Get(bindingContext.ModelType);
  356. }
  357. // If the user specified a ResourceClassKey try to load the resource they specified.
  358. // If the class key is invalid, an exception will be thrown.
  359. // If the class key is valid but the resource is not found, it returns null, in which
  360. // case it will fall back to the MVC default error message.
  361. private static string GetUserResourceString(ControllerContext controllerContext, string resourceName) {
  362. string result = null;
  363. if (!String.IsNullOrEmpty(ResourceClassKey) && (controllerContext != null) && (controllerContext.HttpContext != null)) {
  364. result = controllerContext.HttpContext.GetGlobalResourceObject(ResourceClassKey, resourceName, CultureInfo.CurrentUICulture) as string;
  365. }
  366. return result;
  367. }
  368. private static string GetValueInvalidResource(ControllerContext controllerContext) {
  369. return GetUserResourceString(controllerContext, "PropertyValueInvalid") ?? MvcResources.DefaultModelBinder_ValueInvalid;
  370. }
  371. private static string GetValueRequiredResource(ControllerContext controllerContext) {
  372. return GetUserResourceString(controllerContext, "PropertyValueRequired") ?? MvcResources.DefaultModelBinder_ValueRequired;
  373. }
  374. private static IEnumerable<string> GetZeroBasedIndexes() {
  375. for (int i = 0; ; i++) {
  376. yield return i.ToString(CultureInfo.InvariantCulture);
  377. }
  378. }
  379. protected static bool IsModelValid(ModelBindingContext bindingContext) {
  380. if (bindingContext == null) {
  381. throw new ArgumentNullException("bindingContext");
  382. }
  383. if (String.IsNullOrEmpty(bindingContext.ModelName)) {
  384. return bindingContext.ModelState.IsValid;
  385. }
  386. return bindingContext.ModelState.IsValidField(bindingContext.ModelName);
  387. }
  388. protected virtual void OnModelUpdated(ControllerContext controllerContext, ModelBindingContext bindingContext) {
  389. Dictionary<string, bool> startedValid = new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
  390. foreach (ModelValidationResult validationResult in ModelValidator.GetModelValidator(bindingContext.ModelMetadata, controllerContext).Validate(null)) {
  391. string subPropertyName = CreateSubPropertyName(bindingContext.ModelName, validationResult.MemberName);
  392. if (!startedValid.ContainsKey(subPropertyName)) {
  393. startedValid[subPropertyName] = bindingContext.ModelState.IsValidField(subPropertyName);
  394. }
  395. if (startedValid[subPropertyName]) {
  396. bindingContext.ModelState.AddModelError(subPropertyName, validationResult.Message);
  397. }
  398. }
  399. }
  400. protected virtual bool OnModelUpdating(ControllerContext controllerContext, ModelBindingContext bindingContext) {
  401. // default implementation does nothing
  402. return true;
  403. }
  404. protected virtual void OnPropertyValidated(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, object value) {
  405. // default implementation does nothing
  406. }
  407. protected virtual bool OnPropertyValidating(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, object value) {
  408. // default implementation does nothing
  409. return true;
  410. }
  411. [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes",
  412. Justification = "We're recording this exception so that we can act on it later.")]
  413. protected virtual void SetProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, object value) {
  414. ModelMetadata propertyMetadata = bindingContext.PropertyMetadata[propertyDescriptor.Name];
  415. propertyMetadata.Model = value;
  416. string modelStateKey = CreateSubPropertyName(bindingContext.ModelName, propertyMetadata.PropertyName);
  417. // If the value is null, and the validation system can find a Required validator for
  418. // us, we'd prefer to run it before we attempt to set the value; otherwise, property
  419. // setters which throw on null (f.e., Entity Framework properties which are backed by
  420. // non-nullable strings in the DB) will get their error message in ahead of us.
  421. //
  422. // We are effectively using the special validator -- Required -- as a helper to the
  423. // binding system, which is why this code is here instead of in the Validating/Validated
  424. // methods, which are really the old-school validation hooks.
  425. if (value == null && bindingContext.ModelState.IsValidField(modelStateKey)) {
  426. ModelValidator requiredValidator = ModelValidatorProviders.Providers.GetValidators(propertyMetadata, controllerContext).Where(v => v.IsRequired).FirstOrDefault();
  427. if (requiredValidator != null) {
  428. foreach (ModelValidationResult validationResult in requiredValidator.Validate(bindingContext.Model)) {
  429. bindingContext.ModelState.AddModelError(modelStateKey, validationResult.Message);
  430. }
  431. }
  432. }
  433. bool isNullValueOnNonNullableType =
  434. value == null &&
  435. !TypeHelpers.TypeAllowsNullValue(propertyDescriptor.PropertyType);
  436. // Try to set a value into the property unless we know it will fail (read-only
  437. // properties and null values with non-nullable types)
  438. if (!propertyDescriptor.IsReadOnly && !isNullValueOnNonNullableType) {
  439. try {
  440. propertyDescriptor.SetValue(bindingContext.Model, value);
  441. }
  442. catch (Exception ex) {
  443. // Only add if we're not already invalid
  444. if (bindingContext.ModelState.IsValidField(modelStateKey)) {
  445. bindingContext.ModelState.AddModelError(modelStateKey, ex);
  446. }
  447. }
  448. }
  449. // Last chance for an error on null values with non-nullable types, we'll use
  450. // the default "A value is required." message.
  451. if (isNullValueOnNonNullableType && bindingContext.ModelState.IsValidField(modelStateKey)) {
  452. bindingContext.ModelState.AddModelError(modelStateKey, GetValueRequiredResource(controllerContext));
  453. }
  454. }
  455. private static bool ShouldUpdateProperty(PropertyDescriptor property, Predicate<string> propertyFilter) {
  456. if (property.IsReadOnly && !CanUpdateReadonlyTypedReference(property.PropertyType)) {
  457. return false;
  458. }
  459. // if this property is rejected by the filter, move on
  460. if (!propertyFilter(property.Name)) {
  461. return false;
  462. }
  463. // otherwise, allow
  464. return true;
  465. }
  466. internal object UpdateCollection(ControllerContext controllerContext, ModelBindingContext bindingContext, Type elementType) {
  467. bool stopOnIndexNotFound;
  468. IEnumerable<string> indexes;
  469. GetIndexes(bindingContext, out stopOnIndexNotFound, out indexes);
  470. IModelBinder elementBinder = Binders.GetBinder(elementType);
  471. // build up a list of items from the request
  472. List<object> modelList = new List<object>();
  473. foreach (string currentIndex in indexes) {
  474. string subIndexKey = CreateSubIndexName(bindingContext.ModelName, currentIndex);
  475. if (!bindingContext.ValueProvider.ContainsPrefix(subIndexKey)) {
  476. if (stopOnIndexNotFound) {
  477. // we ran out of elements to pull
  478. break;
  479. }
  480. else {
  481. continue;
  482. }
  483. }
  484. ModelBindingContext innerContext = new ModelBindingContext() {
  485. ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, elementType),
  486. ModelName = subIndexKey,
  487. ModelState = bindingContext.ModelState,
  488. PropertyFilter = bindingContext.PropertyFilter,
  489. ValueProvider = bindingContext.ValueProvider
  490. };
  491. object thisElement = elementBinder.BindModel(controllerContext, innerContext);
  492. // we need to merge model errors up
  493. AddValueRequiredMessageToModelState(controllerContext, bindingContext.ModelState, subIndexKey, elementType, thisElement);
  494. modelList.Add(thisElement);
  495. }
  496. // if there weren't any elements at all in the request, just return
  497. if (modelList.Count == 0) {
  498. return null;
  499. }
  500. // replace the original collection
  501. object collection = bindingContext.Model;
  502. CollectionHelpers.ReplaceCollection(elementType, collection, modelList);
  503. return collection;
  504. }
  505. internal object UpdateDictionary(ControllerContext controllerContext, ModelBindingContext bindingContext, Type keyType, Type valueType) {
  506. bool stopOnIndexNotFound;
  507. IEnumerable<string> indexes;
  508. GetIndexes(bindingContext, out stopOnIndexNotFound, out indexes);
  509. IModelBinder keyBinder = Binders.GetBinder(keyType);
  510. IModelBinder valueBinder = Binders.GetBinder(valueType);
  511. // build up a list of items from the request
  512. List<KeyValuePair<object, object>> modelList = new List<KeyValuePair<object, object>>();
  513. foreach (string currentIndex in indexes) {
  514. string subIndexKey = CreateSubIndexName(bindingContext.ModelName, currentIndex);
  515. string keyFieldKey = CreateSubPropertyName(subIndexKey, "key");
  516. string valueFieldKey = CreateSubPropertyName(subIndexKey, "value");
  517. if (!(bindingContext.ValueProvider.ContainsPrefix(keyFieldKey) && bindingContext.ValueProvider.ContainsPrefix(valueFieldKey))) {
  518. if (stopOnIndexNotFound) {
  519. // we ran out of elements to pull
  520. break;
  521. }
  522. else {
  523. continue;
  524. }
  525. }
  526. // bind the key
  527. ModelBindingContext keyBindingContext = new ModelBindingContext() {
  528. ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, keyType),
  529. ModelName = keyFieldKey,
  530. ModelState = bindingContext.ModelState,
  531. ValueProvider = bindingContext.ValueProvider
  532. };
  533. object thisKey = keyBinder.BindModel(controllerContext, keyBindingContext);
  534. // we need to merge model errors up
  535. AddValueRequiredMessageToModelState(controllerContext, bindingContext.ModelState, keyFieldKey, keyType, thisKey);
  536. if (!keyType.IsInstanceOfType(thisKey)) {
  537. // we can't add an invalid key, so just move on
  538. continue;
  539. }
  540. // bind the value
  541. ModelBindingContext valueBindingContext = new ModelBindingContext() {
  542. ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, valueType),
  543. ModelName = valueFieldKey,
  544. ModelState = bindingContext.ModelState,
  545. PropertyFilter = bindingContext.PropertyFilter,
  546. ValueProvider = bindingContext.ValueProvider
  547. };
  548. object thisValue = valueBinder.BindModel(controllerContext, valueBindingContext);
  549. // we need to merge model errors up
  550. AddValueRequiredMessageToModelState(controllerContext, bindingContext.ModelState, valueFieldKey, valueType, thisValue);
  551. KeyValuePair<object, object> kvp = new KeyValuePair<object, object>(thisKey, thisValue);
  552. modelList.Add(kvp);
  553. }
  554. // if there weren't any elements at all in the request, just return
  555. if (modelList.Count == 0) {
  556. return null;
  557. }
  558. // replace the original collection
  559. object dictionary = bindingContext.Model;
  560. CollectionHelpers.ReplaceDictionary(keyType, valueType, dictionary, modelList);
  561. return dictionary;
  562. }
  563. // This helper type is used because we're working with strongly-typed collections, but we don't know the Ts
  564. // ahead of time. By using the generic methods below, we can consolidate the collection-specific code in a
  565. // single helper type rather than having reflection-based calls spread throughout the DefaultModelBinder type.
  566. // There is a single point of entry to each of the methods below, so they're fairly simple to maintain.
  567. private static class CollectionHelpers {
  568. private static readonly MethodInfo _replaceCollectionMethod = typeof(CollectionHelpers).GetMethod("ReplaceCollectionImpl", BindingFlags.Static | BindingFlags.NonPublic);
  569. private static readonly MethodInfo _replaceDictionaryMethod = typeof(CollectionHelpers).GetMethod("ReplaceDictionaryImpl", BindingFlags.Static | BindingFlags.NonPublic);
  570. [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
  571. public static void ReplaceCollection(Type collectionType, object collection, object newContents) {
  572. MethodInfo targetMethod = _replaceCollectionMethod.MakeGenericMethod(collectionType);
  573. targetMethod.Invoke(null, new object[] { collection, newContents });
  574. }
  575. private static void ReplaceCollectionImpl<T>(ICollection<T> collection, IEnumerable newContents) {
  576. collection.Clear();
  577. if (newContents != null) {
  578. foreach (object item in newContents) {
  579. // if the item was not a T, some conversion failed. the error message will be propagated,
  580. // but in the meanwhile we need to make a placeholder element in the array.
  581. T castItem = (item is T) ? (T)item : default(T);
  582. collection.Add(castItem);
  583. }
  584. }
  585. }
  586. [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
  587. public static void ReplaceDictionary(Type keyType, Type valueType, object dictionary, object newContents) {
  588. MethodInfo targetMethod = _replaceDictionaryMethod.MakeGenericMethod(keyType, valueType);
  589. targetMethod.Invoke(null, new object[] { dictionary, newContents });
  590. }
  591. private static void ReplaceDictionaryImpl<TKey, TValue>(IDictionary<TKey, TValue> dictionary, IEnumerable<KeyValuePair<object, object>> newContents) {
  592. dictionary.Clear();
  593. foreach (KeyValuePair<object, object> item in newContents) {
  594. // if the item was not a T, some conversion failed. the error message will be propagated,
  595. // but in the meanwhile we need to make a placeholder element in the dictionary.
  596. TKey castKey = (TKey)item.Key; // this cast shouldn't fail
  597. TValue castValue = (item.Value is TValue) ? (TValue)item.Value : default(TValue);
  598. dictionary[castKey] = castValue;
  599. }
  600. }
  601. }
  602. }
  603. }