PageRenderTime 52ms CodeModel.GetById 21ms RepoModel.GetById 1ms app.codeStats 0ms

/src/SystemWebMvc/Mvc/DefaultModelBinder.cs

https://bitbucket.org/markhneedham/aspnet-mvc
C# | 440 lines | 333 code | 63 blank | 44 comment | 95 complexity | 646dcc8827a0f2b6068a791a2e2a55ef MD5 | raw file
  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.Web;
  11. using System.Web.Mvc.Resources;
  12. [AspNetHostingPermission(System.Security.Permissions.SecurityAction.InheritanceDemand, Level = AspNetHostingPermissionLevel.Minimal)]
  13. [AspNetHostingPermission(System.Security.Permissions.SecurityAction.LinkDemand, Level = AspNetHostingPermissionLevel.Minimal)]
  14. public class DefaultModelBinder : IModelBinder {
  15. public virtual ModelBinderResult BindModel(ModelBindingContext bindingContext) {
  16. if (bindingContext == null) {
  17. throw new ArgumentNullException("bindingContext");
  18. }
  19. // see if the value provider already returns an instance of the requested data type; if so, we can short-circuit
  20. // the evaluation and just return that instance.
  21. if (!String.IsNullOrEmpty(bindingContext.ModelName)) {
  22. ValueProviderResult result = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
  23. if (result != null && bindingContext.ModelType.IsInstanceOfType(result.RawValue)) {
  24. bindingContext.ModelState.SetAttemptedValue(bindingContext.ModelName, result.AttemptedValue);
  25. return new ModelBinderResult(result.RawValue);
  26. }
  27. }
  28. if (IsSimpleType(bindingContext.ModelType)) {
  29. // basic types (int, etc.) generally have string -> type converters, so just use those converters
  30. ModelBinderResult simpleResult = GetSimpleType(bindingContext);
  31. if (simpleResult != null || !bindingContext.ModelType.IsArray) {
  32. return simpleResult;
  33. }
  34. }
  35. if (bindingContext.ModelType.IsArray) {
  36. return new ModelBinderResult(CreateArray(bindingContext, bindingContext.ModelType.GetElementType()));
  37. }
  38. // the new context creates the model if one doesn't already exist
  39. Func<object> modelProvider = () => bindingContext.Model ?? CreateModel(bindingContext, bindingContext.ModelType);
  40. ModelBindingContext newContext = new ModelBindingContext(bindingContext, bindingContext.ValueProvider, bindingContext.ModelType, bindingContext.ModelName, modelProvider, bindingContext.ModelState, bindingContext.ShouldUpdateProperty);
  41. // if ICollection<T> where T is a simple type, use simple list binding logic rather than custom list binding logic
  42. object simpleCollection = TryUpdateSimpleCollection(newContext);
  43. if (simpleCollection != null) {
  44. return new ModelBinderResult(simpleCollection);
  45. }
  46. // the BindModelCore() method contains the user's custom logic (or our own, if not subclassed)
  47. return BindModelCore(newContext);
  48. }
  49. [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes",
  50. Justification = "We note that the property setter threw and invalidate the ModelState as a result.")]
  51. protected virtual ModelBinderResult BindModelCore(ModelBindingContext bindingContext) {
  52. // special-case IDictionary<,> and ICollection<>
  53. Type dictionaryType = bindingContext.ModelType.GetInterfaces().FirstOrDefault(IsDictionaryInterface);
  54. if (dictionaryType != null) {
  55. Type[] genericArguments = dictionaryType.GetGenericArguments();
  56. Type keyType = genericArguments[0];
  57. Type valueType = genericArguments[1];
  58. ModelBinderResult dictionary = UpdateDictionary(bindingContext, keyType, valueType);
  59. return dictionary;
  60. }
  61. Type collectionType = bindingContext.ModelType.GetInterfaces().FirstOrDefault(IsCollectionInterface);
  62. if (collectionType != null) {
  63. Type itemType = collectionType.GetGenericArguments()[0];
  64. ModelBinderResult collection = UpdateCollection(bindingContext, itemType);
  65. return collection;
  66. }
  67. Predicate<string> propFilter;
  68. BindAttribute bindAttr = (BindAttribute)TypeDescriptor.GetAttributes(bindingContext.ModelType)[typeof(BindAttribute)];
  69. if (bindAttr != null) {
  70. propFilter = property => bindAttr.IsPropertyAllowed(property) && bindingContext.ShouldUpdateProperty(property);
  71. }
  72. else {
  73. propFilter = bindingContext.ShouldUpdateProperty;
  74. }
  75. bool triedToSetModelProperty = false;
  76. PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(bindingContext.ModelType);
  77. foreach (PropertyDescriptor property in properties) {
  78. // compare the current property against the list of allowed properties for this type
  79. if (!propFilter(property.Name)) {
  80. continue;
  81. }
  82. // we can't update value type properties that are read-only
  83. if (property.PropertyType.IsValueType && property.IsReadOnly) {
  84. continue;
  85. }
  86. // use PropertyType rather than GetType() since we only want to update properties as
  87. // seen by the typed reference to the model
  88. ModelStateDictionary propertyState = new ModelStateDictionary();
  89. ModelBindingContext propertyContext = new ModelBindingContext(bindingContext, bindingContext.ValueProvider, bindingContext.ModelType, bindingContext.ModelName, null /* modelProvider */, propertyState, null /* propertyFilter */);
  90. ModelBinderResult propertyResult = BindProperty(propertyContext, property.PropertyType, () => property.GetValue(bindingContext.Model), property.Name);
  91. bindingContext.ModelState.Merge(propertyState);
  92. // if there wasn't even a form value for this property, just move on
  93. if (propertyResult == null) {
  94. continue;
  95. }
  96. triedToSetModelProperty = true;
  97. bool shouldCallSetValue = false;
  98. string subPropertyName = CreateSubPropertyName(bindingContext.ModelName, property.Name);
  99. if (propertyResult.Value != null) {
  100. if (!property.IsReadOnly) {
  101. shouldCallSetValue = true;
  102. }
  103. }
  104. else {
  105. if (TypeHelpers.TypeAllowsNullValue(property.PropertyType)) {
  106. // user probably explicitly wanted this value to be set to null
  107. shouldCallSetValue = true;
  108. }
  109. else {
  110. if (propertyState.IsValid) {
  111. // user didn't type a value, but we can't set a value type to null
  112. bindingContext.ModelState.AddModelError(subPropertyName, MvcResources.DefaultModelBinder_ValueRequired);
  113. }
  114. }
  115. }
  116. if (shouldCallSetValue) {
  117. try {
  118. property.SetValue(bindingContext.Model, propertyResult.Value);
  119. }
  120. catch {
  121. // note that there was an error and just move on to the next property
  122. // use CurrentCulture since this error message is displayed to the user browsing the site
  123. string message = String.Format(CultureInfo.CurrentCulture, MvcResources.Common_ValueNotValidForProperty, propertyResult.Value);
  124. bindingContext.ModelState.AddModelError(subPropertyName, message);
  125. }
  126. }
  127. }
  128. return (triedToSetModelProperty) ? new ModelBinderResult(bindingContext.Model) : null;
  129. }
  130. protected ModelBinderResult BindProperty(ModelBindingContext parentContext, Type propertyType, Func<object> propertyValueProvider, string propertyName) {
  131. // the property name as understood by the value provider
  132. string newName = CreateSubPropertyName(parentContext.ModelName, propertyName);
  133. IModelBinder binder = GetBinder(propertyType);
  134. ModelBindingContext newContext = new ModelBindingContext(parentContext, parentContext.ValueProvider, propertyType, newName, propertyValueProvider, parentContext.ModelState, null /* propertyFilter */);
  135. ModelBinderResult result = binder.BindModel(newContext);
  136. return result;
  137. }
  138. protected static object ConvertSimpleArrayType(CultureInfo culture, object value, Type destinationType) {
  139. if (value == null || destinationType.IsInstanceOfType(value)) {
  140. return value;
  141. }
  142. // array conversion results in four cases, as below
  143. Array valueAsArray = value as Array;
  144. if (destinationType.IsArray) {
  145. Type destinationElementType = destinationType.GetElementType();
  146. if (valueAsArray != null) {
  147. // case 1: both destination + source type are arrays, so convert each element
  148. IList converted = Array.CreateInstance(destinationElementType, valueAsArray.Length);
  149. for (int i = 0; i < valueAsArray.Length; i++) {
  150. converted[i] = ConvertSimpleType(culture, valueAsArray.GetValue(i), destinationElementType);
  151. }
  152. return converted;
  153. }
  154. else {
  155. // case 2: destination type is array but source is single element, so wrap element in array + convert
  156. object element = ConvertSimpleType(culture, value, destinationElementType);
  157. IList converted = Array.CreateInstance(destinationElementType, 1);
  158. converted[0] = element;
  159. return converted;
  160. }
  161. }
  162. else if (valueAsArray != null) {
  163. // case 3: destination type is single element but source is array, so extract first element + convert
  164. if (valueAsArray.Length > 0) {
  165. value = valueAsArray.GetValue(0);
  166. return ConvertSimpleType(culture, value, destinationType);
  167. }
  168. else {
  169. // case 3(a): source is empty array, so can't perform conversion
  170. return null;
  171. }
  172. }
  173. // case 4: both destination + source type are single elements, so convert
  174. return ConvertSimpleType(culture, value, destinationType);
  175. }
  176. protected static object ConvertSimpleType(CultureInfo culture, object value, Type destinationType) {
  177. if (value == null || destinationType.IsInstanceOfType(value)) {
  178. return value;
  179. }
  180. // if this is a user-input value but the user didn't type anything, return no value
  181. string valueAsString = value as string;
  182. if (valueAsString != null && valueAsString.Length == 0) {
  183. return null;
  184. }
  185. TypeConverter converter = TypeDescriptor.GetConverter(destinationType);
  186. bool canConvertFrom = converter.CanConvertFrom(value.GetType());
  187. if (!canConvertFrom) {
  188. converter = TypeDescriptor.GetConverter(value.GetType());
  189. }
  190. if (!(canConvertFrom || converter.CanConvertTo(destinationType))) {
  191. string message = String.Format(CultureInfo.CurrentUICulture, MvcResources.DefaultModelBinder_NoConverterExists,
  192. value.GetType().FullName, destinationType.FullName);
  193. throw new InvalidOperationException(message);
  194. }
  195. try {
  196. object convertedValue = (canConvertFrom) ?
  197. converter.ConvertFrom(null /* context */, culture, value) :
  198. converter.ConvertTo(null /* context */, culture, value, destinationType);
  199. return convertedValue;
  200. }
  201. catch (Exception ex) {
  202. string message = String.Format(CultureInfo.CurrentUICulture, MvcResources.DefaultModelBinder_ConversionThrew,
  203. value.GetType().FullName, destinationType.FullName);
  204. throw new InvalidOperationException(message, ex);
  205. }
  206. }
  207. // this method is specifically for creating an array of types using the index wire format
  208. private Array CreateArray(ModelBindingContext bindingContext, Type elementType) {
  209. // special support for creating arrays - create and bind a list, then coerce it to an array
  210. Type listType = typeof(List<>).MakeGenericType(elementType);
  211. IList list = (IList)CreateModel(bindingContext, listType);
  212. ModelBindingContext newContext = new ModelBindingContext(bindingContext, bindingContext.ValueProvider, listType, bindingContext.ModelName, () => list, bindingContext.ModelState, null /* propertyFilter */);
  213. BindModelCore(newContext);
  214. Array array = Array.CreateInstance(elementType, list.Count);
  215. for (int i = 0; i < list.Count; i++) {
  216. array.SetValue(list[i], i);
  217. }
  218. return array;
  219. }
  220. protected virtual object CreateModel(ModelBindingContext bindingContext, Type modelType) {
  221. Type typeToCreate = modelType;
  222. // we can understand some collection interfaces, e.g. IList<>, IDictionary<,>
  223. if (modelType.IsGenericType) {
  224. Type genericTypeDefinition = modelType.GetGenericTypeDefinition();
  225. if (genericTypeDefinition == typeof(IDictionary<,>)) {
  226. typeToCreate = typeof(Dictionary<,>).MakeGenericType(modelType.GetGenericArguments());
  227. }
  228. else if (genericTypeDefinition == typeof(ICollection<>) || genericTypeDefinition == typeof(IList<>)) {
  229. typeToCreate = typeof(List<>).MakeGenericType(modelType.GetGenericArguments());
  230. }
  231. }
  232. // fallback to the type's default constructor
  233. return Activator.CreateInstance(typeToCreate);
  234. }
  235. protected static string CreateSubIndexName(string prefix, string indexName) {
  236. return (!String.IsNullOrEmpty(prefix)) ? prefix + "[" + indexName + "]" : "[" + indexName + "]";
  237. }
  238. protected static string CreateSubPropertyName(string prefix, string propertyName) {
  239. return (!String.IsNullOrEmpty(prefix)) ? prefix + "." + propertyName : propertyName;
  240. }
  241. protected virtual IModelBinder GetBinder(Type modelType) {
  242. IModelBinder binder = ModelBinders.GetBinder(modelType);
  243. return binder;
  244. }
  245. private static Type GetElementType(Type type) {
  246. // currently this method handles only T[] and T?
  247. if (type.IsArray) {
  248. return type.GetElementType();
  249. }
  250. // Nullable.GetUnderlyingType() returns null if the provided type is not nullable
  251. return Nullable.GetUnderlyingType(type) ?? type;
  252. }
  253. [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes",
  254. Justification = "We want to replace the exception text with our own error message.")]
  255. protected static ModelBinderResult GetSimpleType(ModelBindingContext bindingContext) {
  256. ValueProviderResult result = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
  257. if (result == null) {
  258. return null;
  259. }
  260. bindingContext.ModelState.SetAttemptedValue(bindingContext.ModelName, result.AttemptedValue);
  261. try {
  262. object convertedValue = ConvertSimpleArrayType(result.Culture, result.RawValue, bindingContext.ModelType);
  263. return new ModelBinderResult(convertedValue);
  264. }
  265. catch {
  266. // need to use current culture since this message is potentially displayed to the end user
  267. string message = String.Format(CultureInfo.CurrentCulture, MvcResources.Common_ValueNotValidForProperty,
  268. result.AttemptedValue);
  269. bindingContext.ModelState.AddModelError(bindingContext.ModelName, message);
  270. return new ModelBinderResult(null);
  271. }
  272. }
  273. private static bool IsCollectionInterface(Type type) {
  274. return (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(ICollection<>));
  275. }
  276. private static bool IsDictionaryInterface(Type type) {
  277. return (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IDictionary<,>));
  278. }
  279. protected static bool IsSimpleType(Type type) {
  280. // simple data types are structs (which likely have their own type converters) and strings
  281. Type elementType = GetElementType(type);
  282. return (elementType.IsValueType || elementType == typeof(string));
  283. }
  284. private static object TryUpdateSimpleCollection(ModelBindingContext bindingContext) {
  285. Type genericCollectionType = bindingContext.ModelType.GetInterfaces().FirstOrDefault(IsCollectionInterface);
  286. if (genericCollectionType == null) {
  287. // there is no ICollection<T> interface we can use
  288. return null;
  289. }
  290. Type elementType = genericCollectionType.GetGenericArguments()[0];
  291. if (!(elementType == typeof(string) || elementType.IsValueType)) {
  292. // T is a complex type
  293. return null;
  294. }
  295. ModelBindingContext newContext = new ModelBindingContext(bindingContext, bindingContext.ValueProvider, elementType.MakeArrayType(), bindingContext.ModelName, null /* modelProvider */, bindingContext.ModelState, null /* propertyFilter */);
  296. ModelBinderResult convertedResult = GetSimpleType(newContext);
  297. IList convertedValues = (convertedResult != null) ? convertedResult.Value as IList : null;
  298. if (convertedValues == null || convertedValues.Count == 0) {
  299. // we couldn't find any values using the simple list binding algorithm
  300. return null;
  301. }
  302. // if we got any items, put them in the collection and report success
  303. MethodInfo addMethod = genericCollectionType.GetMethod("Add");
  304. MethodInfo clearMethod = genericCollectionType.GetMethod("Clear");
  305. object collection = bindingContext.Model;
  306. clearMethod.Invoke(collection, null /* parameters */);
  307. foreach (object convertedValue in convertedValues) {
  308. addMethod.Invoke(collection, new object[] { convertedValue });
  309. }
  310. return collection;
  311. }
  312. private ModelBinderResult UpdateCollection(ModelBindingContext bindingContext, Type itemType) {
  313. // first, need to read the set of all unique indices
  314. string indicesFieldName = CreateSubPropertyName(bindingContext.ModelName, "index");
  315. ModelBindingContext indicesContext = new ModelBindingContext(bindingContext, bindingContext.ValueProvider, typeof(string[]), indicesFieldName, null /* modelProvider */, bindingContext.ModelState, null /* propertyFilter */);
  316. ModelBinderResult indicesResult = GetSimpleType(indicesContext);
  317. string[] indices = (indicesResult != null) ? indicesResult.Value as string[] : null;
  318. // loop through entries
  319. List<object> convertedValues = new List<object>();
  320. if (indices != null) {
  321. IModelBinder itemBinder = GetBinder(itemType);
  322. foreach (string index in indices) {
  323. string itemName = CreateSubIndexName(bindingContext.ModelName, index);
  324. ModelBindingContext itemContext = new ModelBindingContext(bindingContext, bindingContext.ValueProvider, itemType, itemName, null /* modelProvider */, bindingContext.ModelState, null /* propertyFilter */);
  325. object convertedValue = itemBinder.BindModel(itemContext).Value;
  326. convertedValues.Add(convertedValue);
  327. }
  328. }
  329. // if there weren't any entries in the list, return that we did nothing
  330. if (convertedValues.Count == 0) {
  331. return null;
  332. }
  333. // if there were entries in the list, replace the collection
  334. Type collectionType = typeof(ICollection<>).MakeGenericType(itemType);
  335. MethodInfo addMethod = collectionType.GetMethod("Add");
  336. MethodInfo clearMethod = collectionType.GetMethod("Clear");
  337. object collection = bindingContext.Model;
  338. clearMethod.Invoke(collection, null /* parameters */);
  339. foreach (object convertedValue in convertedValues) {
  340. addMethod.Invoke(collection, new object[] { convertedValue });
  341. }
  342. return new ModelBinderResult(collection);
  343. }
  344. private ModelBinderResult UpdateDictionary(ModelBindingContext bindingContext, Type keyType, Type valueType) {
  345. // first, need to read the set of all unique indices
  346. string indicesFieldName = CreateSubPropertyName(bindingContext.ModelName, "index");
  347. ModelBindingContext indicesContext = new ModelBindingContext(bindingContext, bindingContext.ValueProvider, typeof(string[]), indicesFieldName, null /* modelProvider */, bindingContext.ModelState, null /* propertyFilter */);
  348. ModelBinderResult indicesResult = GetSimpleType(indicesContext);
  349. string[] indices = (indicesResult != null) ? indicesResult.Value as string[] : null;
  350. Type kvpType = typeof(KeyValuePair<,>).MakeGenericType(keyType, valueType);
  351. // loop through entries
  352. List<KeyValuePair<object, object>> convertedValues = new List<KeyValuePair<object, object>>();
  353. if (indices != null) {
  354. foreach (string index in indices) {
  355. string itemName = CreateSubIndexName(bindingContext.ModelName, index);
  356. ModelBindingContext itemContext = new ModelBindingContext(bindingContext, bindingContext.ValueProvider, kvpType, itemName, null /* modelProvider */, bindingContext.ModelState, null /* propertyFilter */);
  357. object key = BindProperty(itemContext, keyType, null /* propertyValueProvider */, "key").Value;
  358. object value = BindProperty(itemContext, valueType, null /* propertyValueProvider */, "value").Value;
  359. convertedValues.Add(new KeyValuePair<object, object>(key, value));
  360. }
  361. }
  362. // if there weren't any entries in the list, return that we did nothing
  363. if (convertedValues.Count == 0) {
  364. return null;
  365. }
  366. // if there were entries in the list, replace the collection
  367. MethodInfo clearMethod = typeof(ICollection<>).MakeGenericType(kvpType).GetMethod("Clear");
  368. PropertyInfo itemProperty = typeof(IDictionary<,>).MakeGenericType(keyType, valueType).GetProperty("Item");
  369. object dictionary = bindingContext.Model;
  370. clearMethod.Invoke(dictionary, null /* parameters */);
  371. foreach (var convertedValue in convertedValues) {
  372. itemProperty.SetValue(dictionary, convertedValue.Value, new object[] { convertedValue.Key });
  373. }
  374. return new ModelBinderResult(dictionary);
  375. }
  376. }
  377. }