Model binding comma separated query string parameter

前端 未结 4 1738
逝去的感伤
逝去的感伤 2020-12-02 13:45

How can I bind query string parameter that is comma separated value

http://localhost/Action?ids=4783,5063,5305

to a controller action expec

相关标签:
4条回答
  • 2020-12-02 13:59

    Here's my improved version of Nathan Taylor’s solution used in archil's answer.

    1. Nathan’s binder could only bind sub-properties of complex models, while mine can also bind individual controller arguments.
    2. My binder also gives you correct handling of empty parameters, by returning an actual empty instance of your array or IEnumerable.

    To wire this up, you can either attach this to an individual Controller argument:

    [ModelBinder(typeof(CommaSeparatedModelBinder))]
    

    …or set it as the global default binder in Application_Start in global.asax.cs:

    ModelBinders.Binders.DefaultBinder = new CommaSeparatedModelBinder();
    

    In the second case it will try and handle all IEnumerables and fall back to ASP.NET MVC standard implementation for everything else.

    Behold:

    public class CommaSeparatedModelBinder : DefaultModelBinder
    {
        private static readonly MethodInfo ToArrayMethod = typeof(Enumerable).GetMethod("ToArray");
    
        public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            return BindCsv(bindingContext.ModelType, bindingContext.ModelName, bindingContext)
                    ?? base.BindModel(controllerContext, bindingContext);
        }
    
        protected override object GetPropertyValue(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor, IModelBinder propertyBinder)
        {
            return BindCsv(propertyDescriptor.PropertyType, propertyDescriptor.Name, bindingContext)
                    ?? base.GetPropertyValue(controllerContext, bindingContext, propertyDescriptor, propertyBinder);
        }
    
        private object BindCsv(Type type, string name, ModelBindingContext bindingContext)
        {
            if (type.GetInterface(typeof(IEnumerable).Name) != null)
            {
                var actualValue = bindingContext.ValueProvider.GetValue(name);
    
                if (actualValue != null)
                {
                    var valueType = type.GetElementType() ?? type.GetGenericArguments().FirstOrDefault();
    
                    if (valueType != null && valueType.GetInterface(typeof(IConvertible).Name) != null)
                    {
                        var list = (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(valueType));
    
                        foreach (var splitValue in actualValue.AttemptedValue.Split(new[] { ',' }))
                        {
                                if(!String.IsNullOrWhiteSpace(splitValue))
                                    list.Add(Convert.ChangeType(splitValue, valueType));
                        }
    
                        if (type.IsArray)
                            return ToArrayMethod.MakeGenericMethod(valueType).Invoke(this, new[] { list });
                        else
                            return list;
                    }
                }
            }
    
            return null;
        }
    }
    
    0 讨论(0)
  • 2020-12-02 13:59

    Taken from my answer:

    I will show you here a very simple custom model binder I have just written (and tested in .Net Core 2.0):

    My model binder:

    public class CustomModelBinder : IModelBinder
    {
        public Task BindModelAsync(ModelBindingContext bindingContext)
        {
            var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
            var value = valueProviderResult.FirstValue; // get the value as string
    
            var model = value?.Split(",");
            bindingContext.Result = ModelBindingResult.Success(model);
    
            return Task.CompletedTask;
        }
    }
    

    My model (and notice, only one property has my custom model binder annotation):

    public class CreatePostViewModel
    {
        [Display(Name = nameof(ContentText))]
        [MinLength(10, ErrorMessage = ValidationErrors.MinLength)]
        public string ContentText { get; set; }
    
        [BindProperty(BinderType = typeof(CustomModelBinder))]
        public IEnumerable<string> Categories { get; set; } // <<<<<< THIS IS WHAT YOU ARE INTERESTER IN
    
        #region View Data
        public string PageTitle { get; set; }
        public string TitlePlaceHolder { get; set; }
        #endregion
    }
    

    What it does is: it receives some text like "aaa,bbb,ccc", and converts it into array, and return it to the ViewModel.

    I hope that helps.

    DISCLAIMER: I am not an expert in model binders writing, I have learn that 15 minutes ago, and I found your question (with no helpful answer), so I tried to help. This is a very basic model binder, some improvements are surely required. I learned how to write it from the official documentation page.

    0 讨论(0)
  • 2020-12-02 14:06

    Archils answer gave some ideas how to implement my own model binder. I was able to slightly simplify the source code as there wasn't need for very generic CSV support. Instead of setting the received data to List<int> I am putting it to the class.

    Model binder

    public class FarmModelBinder : DefaultModelBinder
    {
        public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            if (bindingContext.ModelType == typeof(FarmModel))
            {
                var newBindingContext = new ModelBindingContext()
                {
                    ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(
                    () => CreateFarmModel(controllerContext, bindingContext),
                    typeof(FarmModel)
                    ),
                    ModelState = bindingContext.ModelState,
                    ValueProvider = bindingContext.ValueProvider
                };
    
                return base.BindModel(controllerContext, newBindingContext);
            }
    
            return base.BindModel(controllerContext, bindingContext);
        }
    
        private FarmModel CreateFarmModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            var farmsIds = new List<int>();
    
            var value = bindingContext.ValueProvider.GetValue("farmData");
            if(value != null && value.AttemptedValue != null)
            {
                var array = value.AttemptedValue.Split(new [] {','});
                foreach (var s in array)
                {
                    int result;
                    if(int.TryParse(s, out result))
                    {
                        farmsIds.Add(result);
                    }
                }
            }
            return new FarmModel() { FarmIds = farmsIds };
        }
    }
    

    Model

    public class FarmModel
    {
        public IEnumerable<int> FarmIds { get; set; }
    }
    

    Adding of the custom binder

    System.Web.Mvc.ModelBinders.Binders.Add(typeof(FarmModel), new FarmModelBinder());
    
    0 讨论(0)
  • 2020-12-02 14:07

    Default model binder expects simple type lists to be in the format

    name=value&name=value2&name=value3
    

    To use builtin binding, you should change your query string to

    Action?ids=4783&ids=5063&ids=5305
    

    Or create custom model binder. You may take a look at following article (code from there)

    public class CommaSeparatedValuesModelBinder : DefaultModelBinder
    {
        private static readonly MethodInfo ToArrayMethod = typeof(Enumerable).GetMethod("ToArray");
    
        protected override object GetPropertyValue(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor, IModelBinder propertyBinder)
        {
            if (propertyDescriptor.PropertyType.GetInterface(typeof(IEnumerable).Name) != null)
            {
                var actualValue = bindingContext.ValueProvider.GetValue(propertyDescriptor.Name);
    
                if (actualValue != null && !String.IsNullOrWhiteSpace(actualValue.AttemptedValue) && actualValue.AttemptedValue.Contains(","))
                {
                    var valueType = propertyDescriptor.PropertyType.GetElementType() ?? propertyDescriptor.PropertyType.GetGenericArguments().FirstOrDefault();
    
                    if (valueType != null && valueType.GetInterface(typeof(IConvertible).Name) != null)
                    {
                        var list = (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(valueType));
    
                        foreach (var splitValue in actualValue.AttemptedValue.Split(new[] { ',' }))
                        {
                            list.Add(Convert.ChangeType(splitValue, valueType));
                        }
    
                        if (propertyDescriptor.PropertyType.IsArray)
                        {
                            return ToArrayMethod.MakeGenericMethod(valueType).Invoke(this, new[] { list });
                        }
                        else
                        {
                            return list;
                        }
                    }
                }
            }
    
            return base.GetPropertyValue(controllerContext, bindingContext, propertyDescriptor, propertyBinder);
        }
    }
    
    0 讨论(0)
提交回复
热议问题