// 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; 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 : IModelBinder { /// public virtual async Task BindModelAsync([NotNull] ModelBindingContext bindingContext) { ModelBindingHelper.ValidateBindingContext(bindingContext); if (!await bindingContext.ValueProvider.ContainsPrefixAsync(bindingContext.ModelName)) { return null; } var valueProviderResult = await bindingContext.ValueProvider.GetValueAsync(bindingContext.ModelName); IEnumerable boundCollection; CollectionResult result; if (valueProviderResult == null) { result = await BindComplexCollection(bindingContext); boundCollection = result.Model; } else { result = await BindSimpleCollection( bindingContext, valueProviderResult.RawValue, valueProviderResult.Culture); boundCollection = result.Model; } var model = bindingContext.Model; if (model == null) { model = GetModel(boundCollection); } else { // Special case for TryUpdateModelAsync(collection, ...) scenarios. Model is null in all other cases. CopyToModel(model, boundCollection); } return new ModelBindingResult( model, bindingContext.ModelName, isModelSet: true, validationNode: result?.ValidationNode); } // 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 async Task BindSimpleCollection( ModelBindingContext bindingContext, object rawValue, CultureInfo culture) { if (rawValue == null) { return null; // nothing to do } var boundCollection = new List(); var metadataProvider = bindingContext.OperationBindingContext.MetadataProvider; var elementMetadata = metadataProvider.GetMetadataForType(typeof(TElement)); var validationNode = new ModelValidationNode( bindingContext.ModelName, bindingContext.ModelMetadata, boundCollection); var rawValueArray = RawValueToObjectArray(rawValue); foreach (var rawValueElement in rawValueArray) { 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, rawValueElement, 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 = await bindingContext.ValueProvider.GetValueAsync(indexPropertyName); var indexNames = GetIndexNamesFromValueProviderResult(valueProviderResultIndex); return await BindComplexCollectionFromIndexes(bindingContext, indexNames); } internal async Task BindComplexCollectionFromIndexes( ModelBindingContext bindingContext, IEnumerable indexNames) { bool indexNamesIsFinite; if (indexNames != null) { indexNamesIsFinite = true; } else { indexNamesIsFinite = false; indexNames = Enumerable.Range(0, Int32.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 modelType = bindingContext.ModelType; 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 class CollectionResult { public ModelValidationNode ValidationNode { get; set; } public IEnumerable Model { get; set; } } /// /// Gets an assignable to the collection property. /// /// /// Collection of values retrieved from value providers. Or null if nothing was bound. /// /// /// assignable to the collection property. 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 GetModel(IEnumerable newCollection) { // Depends on fact BindSimpleCollection() and BindComplexCollection() always return a List // instance or null. In addition GenericModelBinder confirms a List is assignable to the // property prior to instantiating this binder and subclass binders do not call this method. 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. /// /// Called only in TryUpdateModelAsync(collection, ...) scenarios. 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); } } } internal 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.ConvertTo(typeof(string[])); if (indexes != null && indexes.Length > 0) { indexNames = indexes; } } return indexNames; } } }