Параметр строки привязки модели, разделенный запятыми
Как я могу связать параметр строки запроса, значение которого разделено запятой
http://localhost/Action?ids=4783,5063,5305
для действия контроллера, ожидающего списка?
public ActionResult Action(List<long> ids)
{
return View();
}
Примечание! ids
в действии контроллера должен быть список (или что-то на основе IEnumerable), поэтому string ids
не принимается как ответ, потому что эти параметры передаются во многие действия и синтаксический анализ строки к массиву добавится нежелательный шум.
Ответы
Ответ 1
Ответы Archils дали некоторые идеи о том, как реализовать мое собственное связующее устройство. Я смог немного упростить исходный код, так как не было необходимости в очень общей поддержке CSV. Вместо того, чтобы устанавливать полученные данные на List<int>
, я помещаю его в класс.
Модельное связующее
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; }
}
Добавление настраиваемого связующего
System.Web.Mvc.ModelBinders.Binders.Add(typeof(FarmModel), new FarmModelBinder());
Ответ 2
Здесь моя улучшенная версия решения Nathan Taylors, используемая в архивных ответах.
- Связывание Натанса может связывать только суб-свойства сложных моделей,
в то время как мои могут также связывать отдельные аргументы контроллера.
- Мое связующее также дает вам правильную обработку пустых параметров, возвращая
фактический пустой экземпляр вашего массива или IEnumerable.
Чтобы связать это, вы можете либо привязать это к индивидуальному аргументу контроллера:
[ModelBinder(typeof(CommaSeparatedModelBinder))]
... или установите его как глобальное связующее по умолчанию в Application_Start в global.asax.cs:
ModelBinders.Binders.DefaultBinder = new CommaSeparatedModelBinder();
Во втором случае он попытается обработать все IEnumerables и вернуться к стандартной реализации ASP.NET MVC для всего остального.
Вот:
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;
}
}
Ответ 3
Связующее устройство по умолчанию ожидает, что простые типы списков будут в формате
name=value&name=value2&name=value3
Чтобы использовать встроенную привязку, вы должны изменить строку запроса на
Action?ids=4783&ids=5063&ids=5305
Или создать настраиваемое связующее устройство. Вы можете посмотреть следующую статью (код оттуда)
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);
}
}
Ответ 4
Взято из моего ответа:
Я покажу вам очень простое пользовательское связующее устройство, которое я только что написал (и протестировал в .Net Core 2.0):
Мое связующее:
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;
}
}
Моя модель (и заметьте, только одно свойство имеет мою собственную аннотацию привязки модели):
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
}
Он делает это: он получает некоторый текст, такой как "aaa, bbb, ccc", и преобразует его в массив и возвращает его в ViewModel.
Надеюсь, это поможет.
ОТКАЗ ОТ ОТВЕТСТВЕННОСТИ: Я не эксперт в написании связующих моделей, я узнал, что 15 минут назад, и я нашел ваш вопрос (без полезного ответа), поэтому я попытался помочь. Это очень базовая модель связующего, некоторые улучшения, безусловно, требуется. Я узнал, как писать его со страницы официальной документации.