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