// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Linq; #if DNXCORE50 using System.Reflection; #endif using System.Threading.Tasks; using Microsoft.Framework.Internal; namespace Microsoft.AspNet.Mvc.ModelBinding { /// /// implementation for binding collection values. /// /// Type of elements in the collection. public class CollectionModelBinder : ICollectionModelBinder { /// public virtual async Task BindModelAsync([NotNull] ModelBindingContext bindingContext) { ModelBindingHelper.ValidateBindingContext(bindingContext); var model = bindingContext.Model; if (!bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName)) { // If this is the fallback case and we failed to find data for a top-level model, then generate a // default 'empty' model (or use existing Model) and return it. if (!bindingContext.IsFirstChanceBinding && bindingContext.IsTopLevelObject) { if (model == null) { model = CreateEmptyCollection(bindingContext.ModelType); } var validationNode = new ModelValidationNode( bindingContext.ModelName, bindingContext.ModelMetadata, model); return new ModelBindingResult( model, bindingContext.ModelName, isModelSet: true, validationNode: validationNode); } return null; } var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName); CollectionResult result; if (valueProviderResult == ValueProviderResult.None) { result = await BindComplexCollection(bindingContext); } else { result = await BindSimpleCollection(bindingContext, valueProviderResult); } var boundCollection = result.Model; if (model == null) { model = ConvertToCollectionType(bindingContext.ModelType, boundCollection); } else { // Special case for TryUpdateModelAsync(collection, ...) scenarios. Model is null in all other cases. CopyToModel(model, boundCollection); } if (valueProviderResult != ValueProviderResult.None) { // If we did simple binding, then modelstate should be updated to reflect what we bound for ModelName. // If we did complex binding, there will already be an entry for each index. bindingContext.ModelState.SetModelValue( bindingContext.ModelName, valueProviderResult); } return new ModelBindingResult( model, bindingContext.ModelName, isModelSet: true, validationNode: result.ValidationNode); } /// public virtual bool CanCreateInstance(Type targetType) { return CreateEmptyCollection(targetType) != null; } /// /// Create an assignable to . /// /// of the model. /// An assignable to . /// Called when creating a default 'empty' model for a top level bind. protected virtual object CreateEmptyCollection(Type targetType) { if (targetType.IsAssignableFrom(typeof(List))) { // Simple case such as ICollection, IEnumerable and IList. return new List(); } return CreateInstance(targetType); } /// /// Create an instance of . /// /// of the model. /// An instance of . protected object CreateInstance(Type targetType) { try { return Activator.CreateInstance(targetType); } catch (Exception) { // Details of exception are not important. return null; } } // Used when the ValueProvider contains the collection to be bound as a single element, e.g. the raw value // is [ "1", "2" ] and needs to be converted to an int[]. // Internal for testing. internal async Task BindSimpleCollection( ModelBindingContext bindingContext, ValueProviderResult values) { var boundCollection = new List(); var metadataProvider = bindingContext.OperationBindingContext.MetadataProvider; var elementMetadata = metadataProvider.GetMetadataForType(typeof(TElement)); var validationNode = new ModelValidationNode( bindingContext.ModelName, bindingContext.ModelMetadata, boundCollection); foreach (var value in values) { var innerBindingContext = ModelBindingContext.GetChildModelBindingContext( bindingContext, bindingContext.ModelName, elementMetadata); innerBindingContext.ValueProvider = new CompositeValueProvider { // our temporary provider goes at the front of the list new ElementalValueProvider(bindingContext.ModelName, value, values.Culture), bindingContext.ValueProvider }; object boundValue = null; var result = await bindingContext.OperationBindingContext.ModelBinder.BindModelAsync(innerBindingContext); if (result != null && result.IsModelSet) { boundValue = result.Model; if (result.ValidationNode != null) { validationNode.ChildNodes.Add(result.ValidationNode); } boundCollection.Add(ModelBindingHelper.CastOrDefault(boundValue)); } } return new CollectionResult { ValidationNode = validationNode, Model = boundCollection }; } // Used when the ValueProvider contains the collection to be bound as multiple elements, e.g. foo[0], foo[1]. private async Task BindComplexCollection(ModelBindingContext bindingContext) { var indexPropertyName = ModelNames.CreatePropertyModelName(bindingContext.ModelName, "index"); var valueProviderResultIndex = bindingContext.ValueProvider.GetValue(indexPropertyName); var indexNames = GetIndexNamesFromValueProviderResult(valueProviderResultIndex); return await BindComplexCollectionFromIndexes(bindingContext, indexNames); } // Internal for testing. internal async Task BindComplexCollectionFromIndexes( ModelBindingContext bindingContext, IEnumerable indexNames) { bool indexNamesIsFinite; if (indexNames != null) { indexNamesIsFinite = true; } else { indexNamesIsFinite = false; indexNames = Enumerable.Range(0, int.MaxValue) .Select(i => i.ToString(CultureInfo.InvariantCulture)); } var metadataProvider = bindingContext.OperationBindingContext.MetadataProvider; var elementMetadata = metadataProvider.GetMetadataForType(typeof(TElement)); var boundCollection = new List(); var validationNode = new ModelValidationNode( bindingContext.ModelName, bindingContext.ModelMetadata, boundCollection); foreach (var indexName in indexNames) { var fullChildName = ModelNames.CreateIndexModelName(bindingContext.ModelName, indexName); var childBindingContext = ModelBindingContext.GetChildModelBindingContext( bindingContext, fullChildName, elementMetadata); var didBind = false; object boundValue = null; var result = await bindingContext.OperationBindingContext.ModelBinder.BindModelAsync(childBindingContext); if (result != null && result.IsModelSet) { didBind = true; boundValue = result.Model; if (result.ValidationNode != null) { validationNode.ChildNodes.Add(result.ValidationNode); } } // infinite size collection stops on first bind failure if (!didBind && !indexNamesIsFinite) { break; } boundCollection.Add(ModelBindingHelper.CastOrDefault(boundValue)); } return new CollectionResult { ValidationNode = validationNode, Model = boundCollection }; } // Internal for testing. internal class CollectionResult { public ModelValidationNode ValidationNode { get; set; } public IEnumerable Model { get; set; } } /// /// Gets an assignable to that contains members from /// . /// /// of the model. /// /// Collection of values retrieved from value providers. Or null if nothing was bound. /// /// /// An assignable to . Or null if nothing was bound. /// /// /// Extensibility point that allows the bound collection to be manipulated or transformed before being /// returned from the binder. /// protected virtual object ConvertToCollectionType(Type targetType, IEnumerable collection) { if (collection == null) { return null; } if (targetType.IsAssignableFrom(typeof(List))) { // Depends on fact BindSimpleCollection() and BindComplexCollection() always return a List // instance or null. return collection; } var newCollection = CreateInstance(targetType); CopyToModel(newCollection, collection); return newCollection; } /// /// Adds values from to given . /// /// into which values are copied. /// /// Collection of values retrieved from value providers. Or null if nothing was bound. /// protected virtual void CopyToModel([NotNull] object target, IEnumerable sourceCollection) { var targetCollection = target as ICollection; Debug.Assert(targetCollection != null, "This binder is instantiated only for ICollection model types."); if (sourceCollection != null && targetCollection != null && !targetCollection.IsReadOnly) { targetCollection.Clear(); foreach (var element in sourceCollection) { targetCollection.Add(element); } } } private static object[] RawValueToObjectArray(object rawValue) { // precondition: rawValue is not null // Need to special-case String so it's not caught by the IEnumerable check which follows if (rawValue is string) { return new[] { rawValue }; } var rawValueAsObjectArray = rawValue as object[]; if (rawValueAsObjectArray != null) { return rawValueAsObjectArray; } var rawValueAsEnumerable = rawValue as IEnumerable; if (rawValueAsEnumerable != null) { return rawValueAsEnumerable.Cast().ToArray(); } // fallback return new[] { rawValue }; } private static IEnumerable GetIndexNamesFromValueProviderResult(ValueProviderResult valueProviderResult) { IEnumerable indexNames = null; if (valueProviderResult != null) { var indexes = (string[])valueProviderResult; if (indexes != null && indexes.Length > 0) { indexNames = indexes; } } return indexNames; } } }