PageRenderTime 48ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 1ms

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

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