Fix bugs in sizing TableLayoutPanel (Xamarin bug 18638)
[mono.git] / mcs / class / System.Web.Mvc2 / System.Web.Mvc / DefaultModelBinder.cs
1 /* ****************************************************************************\r
2  *\r
3  * Copyright (c) Microsoft Corporation. All rights reserved.\r
4  *\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
8  *\r
9  * You must not remove this notice, or any other, from this software.\r
10  *\r
11  * ***************************************************************************/\r
12 \r
13 namespace System.Web.Mvc {\r
14     using System;\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
20     using System.Linq;\r
21     using System.Reflection;\r
22     using System.Runtime.CompilerServices;\r
23     using System.Web.Mvc.Resources;\r
24 \r
25     public class DefaultModelBinder : IModelBinder {\r
26 \r
27         private ModelBinderDictionary _binders;\r
28         private static string _resourceClassKey;\r
29 \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
33             get {\r
34                 if (_binders == null) {\r
35                     _binders = ModelBinders.Binders;\r
36                 }\r
37                 return _binders;\r
38             }\r
39             set {\r
40                 _binders = value;\r
41             }\r
42         }\r
43 \r
44         public static string ResourceClassKey {\r
45             get {\r
46                 return _resourceClassKey ?? String.Empty;\r
47             }\r
48             set {\r
49                 _resourceClassKey = value;\r
50             }\r
51         }\r
52 \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
56             }\r
57         }\r
58 \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
62 \r
63             // validation\r
64             if (OnModelUpdating(controllerContext, newBindingContext)) {\r
65                 BindProperties(controllerContext, newBindingContext);\r
66                 OnModelUpdated(controllerContext, newBindingContext);\r
67             }\r
68         }\r
69 \r
70         internal object BindComplexModel(ControllerContext controllerContext, ModelBindingContext bindingContext) {\r
71             object model = bindingContext.Model;\r
72             Type modelType = bindingContext.ModelType;\r
73 \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
79 \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
86                 };\r
87                 IList list = (IList)UpdateCollection(controllerContext, arrayBindingContext, elementType);\r
88 \r
89                 if (list == null) {\r
90                     return null;\r
91                 }\r
92 \r
93                 Array array = Array.CreateInstance(elementType, list.Count);\r
94                 list.CopyTo(array, 0);\r
95                 return array;\r
96             }\r
97 \r
98             if (model == null) {\r
99                 model = CreateModel(controllerContext, bindingContext, modelType);\r
100             }\r
101 \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
108 \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
115                 };\r
116                 object dictionary = UpdateDictionary(controllerContext, dictionaryBindingContext, keyType, valueType);\r
117                 return dictionary;\r
118             }\r
119 \r
120             Type enumerableType = TypeHelpers.ExtractGenericInterface(modelType, typeof(IEnumerable<>));\r
121             if (enumerableType != null) {\r
122                 Type elementType = enumerableType.GetGenericArguments()[0];\r
123 \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
132                     };\r
133                     object collection = UpdateCollection(controllerContext, collectionBindingContext, elementType);\r
134                     return collection;\r
135                 }\r
136             }\r
137 \r
138             // otherwise, just update the properties on the complex type\r
139             BindComplexElementalModel(controllerContext, bindingContext, model);\r
140             return model;\r
141         }\r
142 \r
143         public virtual object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) {\r
144             if (bindingContext == null) {\r
145                 throw new ArgumentNullException("bindingContext");\r
146             }\r
147 \r
148             bool performedFallback = false;\r
149 \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
159                     };\r
160                     performedFallback = true;\r
161                 }\r
162                 else {\r
163                     return null;\r
164                 }\r
165             }\r
166 \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
174                 }\r
175             }\r
176             if (!bindingContext.ModelMetadata.IsComplexType) {\r
177                 return null;\r
178             }\r
179 \r
180             return BindComplexModel(controllerContext, bindingContext);\r
181         }\r
182 \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
187             }\r
188         }\r
189 \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
194                 return;\r
195             }\r
196 \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
207             };\r
208             object newPropertyValue = GetPropertyValue(controllerContext, innerBindingContext, propertyDescriptor, propertyBinder);\r
209             propertyMetadata.Model = newPropertyValue;\r
210 \r
211             // validation\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
217                 }\r
218             }\r
219             else {\r
220                 SetProperty(controllerContext, bindingContext, propertyDescriptor, newPropertyValue);\r
221 \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
231                             break;\r
232                         }\r
233                     }\r
234                 }\r
235             }\r
236         }\r
237 \r
238         internal object BindSimpleModel(ControllerContext controllerContext, ModelBindingContext bindingContext, ValueProviderResult valueProviderResult) {\r
239             bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);\r
240 \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
245             }\r
246 \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
249 \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
255                     return modelArray;\r
256                 }\r
257 \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
266 \r
267                     Type collectionType = typeof(ICollection<>).MakeGenericType(elementType);\r
268                     if (collectionType.IsInstanceOfType(modelCollection)) {\r
269                         CollectionHelpers.ReplaceCollection(elementType, modelCollection, modelArray);\r
270                     }\r
271                     return modelCollection;\r
272                 }\r
273             }\r
274 \r
275             // case 3: user asked for an individual element\r
276             object model = ConvertProviderResult(bindingContext.ModelState, bindingContext.ModelName, valueProviderResult, bindingContext.ModelType);\r
277             return model;\r
278         }\r
279 \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
284                 return false;\r
285             }\r
286 \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
290                 return false;\r
291             }\r
292 \r
293             // special-case known common immutable types\r
294             if (type == typeof(string)) {\r
295                 return false;\r
296             }\r
297 \r
298             return true;\r
299         }\r
300 \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
306             try {\r
307                 object convertedValue = valueProviderResult.ConvertTo(destinationType);\r
308                 return convertedValue;\r
309             }\r
310             catch (Exception ex) {\r
311                 modelState.AddModelError(modelStateKey, ex);\r
312                 return null;\r
313             }\r
314         }\r
315 \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
321 \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
328             };\r
329 \r
330             return newBindingContext;\r
331         }\r
332 \r
333         protected virtual object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType) {\r
334             Type typeToCreate = modelType;\r
335 \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
341                 }\r
342                 else if (genericTypeDefinition == typeof(IEnumerable<>) || genericTypeDefinition == typeof(ICollection<>) || genericTypeDefinition == typeof(IList<>)) {\r
343                     typeToCreate = typeof(List<>).MakeGenericType(modelType.GetGenericArguments());\r
344                 }\r
345             }\r
346 \r
347             // fallback to the type's default constructor\r
348             return Activator.CreateInstance(typeToCreate);\r
349         }\r
350 \r
351         protected static string CreateSubIndexName(string prefix, int index) {\r
352             return String.Format(CultureInfo.InvariantCulture, "{0}[{1}]", prefix, index);\r
353         }\r
354 \r
355         protected static string CreateSubIndexName(string prefix, string index) {\r
356             return String.Format(CultureInfo.InvariantCulture, "{0}[{1}]", prefix, index);\r
357         }\r
358 \r
359         protected internal static string CreateSubPropertyName(string prefix, string propertyName) {\r
360             if (String.IsNullOrEmpty(prefix)) {\r
361                 return propertyName;\r
362             }\r
363             else if (String.IsNullOrEmpty(propertyName)) {\r
364                 return prefix;\r
365             }\r
366             else {\r
367                 return prefix + "." + propertyName;\r
368             }\r
369         }\r
370 \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
374 \r
375             return from PropertyDescriptor property in properties\r
376                    where ShouldUpdateProperty(property, propertyFilter)\r
377                    select property;\r
378         }\r
379 \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
385 \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
391                     return;\r
392                 }\r
393             }\r
394 \r
395             // just use a simple zero-based system\r
396             stopOnIndexNotFound = true;\r
397             indexes = GetZeroBasedIndexes();\r
398         }\r
399 \r
400         protected virtual PropertyDescriptorCollection GetModelProperties(ControllerContext controllerContext, ModelBindingContext bindingContext) {\r
401             return GetTypeDescriptor(controllerContext, bindingContext).GetProperties();\r
402         }\r
403 \r
404         protected virtual object GetPropertyValue(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, IModelBinder propertyBinder) {\r
405             object value = propertyBinder.BindModel(controllerContext, bindingContext);\r
406 \r
407             if (bindingContext.ModelMetadata.ConvertEmptyStringToNull && Object.Equals(value, String.Empty)) {\r
408                 return null;\r
409             }\r
410 \r
411             return value;\r
412         }\r
413 \r
414         protected virtual ICustomTypeDescriptor GetTypeDescriptor(ControllerContext controllerContext, ModelBindingContext bindingContext) {\r
415             return TypeDescriptorHelper.Get(bindingContext.ModelType);\r
416         }\r
417 \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
424 \r
425             if (!String.IsNullOrEmpty(ResourceClassKey) && (controllerContext != null) && (controllerContext.HttpContext != null)) {\r
426                 result = controllerContext.HttpContext.GetGlobalResourceObject(ResourceClassKey, resourceName, CultureInfo.CurrentUICulture) as string;\r
427             }\r
428 \r
429             return result;\r
430         }\r
431 \r
432         private static string GetValueInvalidResource(ControllerContext controllerContext) {\r
433             return GetUserResourceString(controllerContext, "PropertyValueInvalid") ?? MvcResources.DefaultModelBinder_ValueInvalid;\r
434         }\r
435 \r
436         private static string GetValueRequiredResource(ControllerContext controllerContext) {\r
437             return GetUserResourceString(controllerContext, "PropertyValueRequired") ?? MvcResources.DefaultModelBinder_ValueRequired;\r
438         }\r
439 \r
440         private static IEnumerable<string> GetZeroBasedIndexes() {\r
441             for (int i = 0; ; i++) {\r
442                 yield return i.ToString(CultureInfo.InvariantCulture);\r
443             }\r
444         }\r
445 \r
446         protected static bool IsModelValid(ModelBindingContext bindingContext) {\r
447             if (bindingContext == null) {\r
448                 throw new ArgumentNullException("bindingContext");\r
449             }\r
450             if (String.IsNullOrEmpty(bindingContext.ModelName)) {\r
451                 return bindingContext.ModelState.IsValid;\r
452             }\r
453             return bindingContext.ModelState.IsValidField(bindingContext.ModelName);\r
454         }\r
455 \r
456         protected virtual void OnModelUpdated(ControllerContext controllerContext, ModelBindingContext bindingContext) {\r
457             Dictionary<string, bool> startedValid = new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase);\r
458 \r
459             foreach (ModelValidationResult validationResult in ModelValidator.GetModelValidator(bindingContext.ModelMetadata, controllerContext).Validate(null)) {\r
460                 string subPropertyName = CreateSubPropertyName(bindingContext.ModelName, validationResult.MemberName);\r
461 \r
462                 if (!startedValid.ContainsKey(subPropertyName)) {\r
463                     startedValid[subPropertyName] = bindingContext.ModelState.IsValidField(subPropertyName);\r
464                 }\r
465 \r
466                 if (startedValid[subPropertyName]) {\r
467                     bindingContext.ModelState.AddModelError(subPropertyName, validationResult.Message);\r
468                 }\r
469             }\r
470         }\r
471 \r
472         protected virtual bool OnModelUpdating(ControllerContext controllerContext, ModelBindingContext bindingContext) {\r
473             // default implementation does nothing\r
474             return true;\r
475         }\r
476 \r
477         protected virtual void OnPropertyValidated(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, object value) {\r
478             // default implementation does nothing\r
479         }\r
480 \r
481         protected virtual bool OnPropertyValidating(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, object value) {\r
482             // default implementation does nothing\r
483             return true;\r
484         }\r
485 \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
489 \r
490             ModelMetadata propertyMetadata = bindingContext.PropertyMetadata[propertyDescriptor.Name];\r
491             propertyMetadata.Model = value;\r
492             string modelStateKey = CreateSubPropertyName(bindingContext.ModelName, propertyMetadata.PropertyName);\r
493 \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
498             //\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
507                     }\r
508                 }\r
509             }\r
510 \r
511             bool isNullValueOnNonNullableType =\r
512                 value == null &&\r
513                 !TypeHelpers.TypeAllowsNullValue(propertyDescriptor.PropertyType);\r
514 \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
518                 try {\r
519                     propertyDescriptor.SetValue(bindingContext.Model, value);\r
520                 }\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
525                     }\r
526                 }\r
527             }\r
528 \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
533             }\r
534         }\r
535 \r
536         private static bool ShouldUpdateProperty(PropertyDescriptor property, Predicate<string> propertyFilter) {\r
537             if (property.IsReadOnly && !CanUpdateReadonlyTypedReference(property.PropertyType)) {\r
538                 return false;\r
539             }\r
540 \r
541             // if this property is rejected by the filter, move on\r
542             if (!propertyFilter(property.Name)) {\r
543                 return false;\r
544             }\r
545 \r
546             // otherwise, allow\r
547             return true;\r
548         }\r
549 \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
555 \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
563                         break;\r
564                     }\r
565                     else {\r
566                         continue;\r
567                     }\r
568                 }\r
569 \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
576                 };\r
577                 object thisElement = elementBinder.BindModel(controllerContext, innerContext);\r
578 \r
579                 // we need to merge model errors up\r
580                 AddValueRequiredMessageToModelState(controllerContext, bindingContext.ModelState, subIndexKey, elementType, thisElement);\r
581                 modelList.Add(thisElement);\r
582             }\r
583 \r
584             // if there weren't any elements at all in the request, just return\r
585             if (modelList.Count == 0) {\r
586                 return null;\r
587             }\r
588 \r
589             // replace the original collection\r
590             object collection = bindingContext.Model;\r
591             CollectionHelpers.ReplaceCollection(elementType, collection, modelList);\r
592             return collection;\r
593         }\r
594 \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
599 \r
600             IModelBinder keyBinder = Binders.GetBinder(keyType);\r
601             IModelBinder valueBinder = Binders.GetBinder(valueType);\r
602 \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
609 \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
613                         break;\r
614                     }\r
615                     else {\r
616                         continue;\r
617                     }\r
618                 }\r
619 \r
620                 // bind the key\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
626                 };\r
627                 object thisKey = keyBinder.BindModel(controllerContext, keyBindingContext);\r
628 \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
633                     continue;\r
634                 }\r
635 \r
636                 // bind the value\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
643                 };\r
644                 object thisValue = valueBinder.BindModel(controllerContext, valueBindingContext);\r
645 \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
650             }\r
651 \r
652             // if there weren't any elements at all in the request, just return\r
653             if (modelList.Count == 0) {\r
654                 return null;\r
655             }\r
656 \r
657             // replace the original collection\r
658             object dictionary = bindingContext.Model;\r
659             CollectionHelpers.ReplaceDictionary(keyType, valueType, dictionary, modelList);\r
660             return dictionary;\r
661         }\r
662 \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
667 \r
668         private static class CollectionHelpers {\r
669 \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
672 \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
677             }\r
678 \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
687                     }\r
688                 }\r
689             }\r
690 \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
695             }\r
696 \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
705                 }\r
706             }\r
707         }\r
708     }\r
709 }\r