Asp.Net MVC 2 - привязать свойство модели к другому имени
Обновление (21 сентября 2016 г.). Благодаря Digbyswift для комментариев, что это решение по-прежнему работает и в MVC5.
Обновление (30 апреля 2012 г.). Примечание для людей, которые спотыкаются об этом вопросе от поисков и т.д. - принятый ответ - это не то, как я это сделал, но я оставил его принятым, потому что он, возможно, сработал в некоторых случаях. Мой собственный ответ содержит окончательное решение, которое я использовал, которое можно использовать повторно и будет применяться к любому проекту.
Он также подтвердил работу в v3 и v4 структуры MVC.
У меня есть следующий тип модели (имена класса и его свойства были изменены для защиты их идентификаторов):
public class MyExampleModel
{
public string[] LongPropertyName { get; set; }
}
Затем это свойство привязано к набору ( > 150) флажков, где каждое одно имя ввода, конечно, LongPropertyName
.
Форма отправляет URL-адрес с HTTP GET и говорит, что пользователь выбирает три из этих флажков - URL-адрес будет содержать строку запроса ?LongPropertyName=a&LongPropertyName=b&LongPropertyName=c
Большая проблема заключается в том, что если я выберем все (или даже чуть более половины!) флажков, я превышу максимальную длину строки запроса, установленную фильтром запросов в IIS!
Я не хочу распространять это - так что я хочу, чтобы обрезать эту строку запроса (я знаю, что могу просто переключиться на POST), но даже в этом случае я все же хочу свести к минимуму количество пуха в данных, отправленных клиент).
То, что я хочу сделать, это привязать LongPropertyName
к простому "L", чтобы строка запроса стала ?L=a&L=b&L=c
, но без изменения имени свойства в коде.
В рассматриваемом типе уже есть настраиваемое связующее устройство (исходящее из DefaultModelBinder), но оно привязано к его базовому классу, поэтому я не хочу размещать там код для производного класса. В настоящее время вся привязка свойств выполняется стандартной логикой DefaultModelBinder, которая, как я знаю, использует TypeDescriptors и дескрипторы свойств и т.д. Из System.ComponentModel.
Я как бы надеялся, что может быть атрибут, который я могу применить к свойству, чтобы сделать эту работу - есть ли? Или я должен смотреть на реализацию ICustomTypeDescriptor
?
Ответы
Ответ 1
Вы можете использовать BindAttribute, чтобы выполнить это.
public ActionResult Submit([Bind(Prefix = "L")] string[] longPropertyName) {
}
Update
Так как параметр longPropertyName является частью объекта модели, а не независимым параметром действия контроллера, у вас есть несколько других вариантов.
Вы можете сохранить модель и свойство как независимые параметры для своего действия, а затем вручную объединить данные вместе в методе действий.
public ActionResult Submit(MyModel myModel, [Bind(Prefix = "L")] string[] longPropertyName) {
if(myModel != null) {
myModel.LongPropertyName = longPropertyName;
}
}
Другим вариантом будет внедрение настраиваемого связующего объекта, которое выполняет присвоение значения параметра (как указано выше) вручную, но это, скорее всего, избыточное. Вот пример одного из них, если вам интересно: Флажок Перечислительная модель привязки.
Ответ 2
В ответ на michaelalm ответ и запрос - вот то, что я закончил делать. Я оставил исходный ответ, отмеченный главным образом из вежливости, поскольку одно из решений, предложенных Нафаном, сработало бы.
Результатом этого является замена класса DefaultModelBinder
, который вы можете либо зарегистрировать глобально (тем самым позволяя всем типам моделей использовать преимущества сглаживания), либо выборочно наследовать для пользовательских привязок моделей.
Все начинается, предсказуемо с помощью:
/// <summary>
/// Allows you to create aliases that can be used for model properties at
/// model binding time (i.e. when data comes in from a request).
///
/// The type needs to be using the DefaultModelBinderEx model binder in
/// order for this to work.
/// </summary>
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)]
public class BindAliasAttribute : Attribute
{
public BindAliasAttribute(string alias)
{
//ommitted: parameter checking
Alias = alias;
}
public string Alias { get; private set; }
}
И тогда мы получим этот класс:
internal sealed class AliasedPropertyDescriptor : PropertyDescriptor
{
public PropertyDescriptor Inner { get; private set; }
public AliasedPropertyDescriptor(string alias, PropertyDescriptor inner)
: base(alias, null)
{
Inner = inner;
}
public override bool CanResetValue(object component)
{
return Inner.CanResetValue(component);
}
public override Type ComponentType
{
get { return Inner.ComponentType; }
}
public override object GetValue(object component)
{
return Inner.GetValue(component);
}
public override bool IsReadOnly
{
get { return Inner.IsReadOnly; }
}
public override Type PropertyType
{
get { return Inner.PropertyType; }
}
public override void ResetValue(object component)
{
Inner.ResetValue(component);
}
public override void SetValue(object component, object value)
{
Inner.SetValue(component, value);
}
public override bool ShouldSerializeValue(object component)
{
return Inner.ShouldSerializeValue(component);
}
}
Это проксирует "собственный" PropertyDescriptor, который обычно находится в DefaultModelBinder
, но представляет его имя как псевдоним.
Далее у нас есть новый класс связующего класса:
public class DefaultModelBinderEx : DefaultModelBinder
{
protected override System.ComponentModel.PropertyDescriptorCollection
GetModelProperties(ControllerContext controllerContext,
ModelBindingContext bindingContext)
{
var toReturn = base.GetModelProperties(controllerContext, bindingContext);
List<PropertyDescriptor> additional = new List<PropertyDescriptor>();
//now look for any aliasable properties in here
foreach (var p in
this.GetTypeDescriptor(controllerContext, bindingContext)
.GetProperties().Cast<PropertyDescriptor>())
{
foreach (var attr in p.Attributes.OfType<BindAliasAttribute>())
{
additional.Add(new AliasedPropertyDescriptor(attr.Alias, p));
if (bindingContext.PropertyMetadata.ContainsKey(p.Name))
bindingContext.PropertyMetadata.Add(attr.Alias,
bindingContext.PropertyMetadata[p.Name]);
}
}
return new PropertyDescriptorCollection
(toReturn.Cast<PropertyDescriptor>().Concat(additional).ToArray());
}
}
И, тогда технически, что все есть. Теперь вы можете зарегистрировать этот класс DefaultModelBinderEx
по умолчанию, используя решение, отправленное как ответ в этом SO: Измените стандартное связывание модели в asp.net MVC, или вы может использовать его в качестве базы для вашего собственного связующего устройства.
После того, как вы выбрали свой шаблон для того, как вы хотите, чтобы вставка была нажата, вы просто применяете его к типу модели следующим образом:
public class TestModelType
{
[BindAlias("LPN")]
//and you can add multiple aliases
[BindAlias("L")]
//.. ad infinitum
public string LongPropertyName { get; set; }
}
Причина, по которой я выбрал этот код, заключалась в том, что мне хотелось что-то, что работало бы с дескрипторами пользовательского типа, а также с возможностью работать с любым типом. В равной степени я хотел, чтобы система поставщика стоимости использовалась еще при поиске значений свойств модели. Поэтому я изменил метаданные, которые видит DefaultModelBinder
, когда он начинает связываться. Это несколько более длинный подход, но концептуально он делает на уровне метаданных именно то, что вы хотите.
Один потенциально интересный и слегка раздражающий побочный эффект будет, если ValueProvider
содержит значения для более чем одного псевдонима или псевдоним и свойство по его имени. В этом случае будет использоваться только одно из полученных значений. Трудно думать о способе слияния их всех безопасным типом, когда вы только работаете с object
. Это похоже, однако, на поставку значения как в столбце формы, так и в строке запроса - и я не уверен, что именно MVC делает в этом сценарии, но я не думаю, что он рекомендовал практику.
Другая проблема, конечно, в том, что вы не должны создавать псевдоним, равный другому псевдониму, или действительно имя фактического свойства.
Мне нравится применять мои привязки к модели, в общем, используя класс CustomModelBinderAttribute
. Единственная проблема с этим может быть, если вам нужно получить тип модели и изменить ее поведение привязки - поскольку CustomModelBinderAttribute
наследуется в поиске атрибутов, выполняемом MVC.
В моем случае это нормально, я разрабатываю новую инфраструктуру сайта и могу выдвинуть новую расширяемость в свои базовые связующие, используя другие механизмы для удовлетворения этих новых типов; но это не относится ко всем.
Ответ 3
это решение будет похоже на ваш Андрас? Я надеюсь, что вы также можете опубликовать свой ответ.
метод контроллера
public class MyPropertyBinder : DefaultModelBinder
{
protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor)
{
base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
for (int i = 0; i < propertyDescriptor.Attributes.Count; i++)
{
if (propertyDescriptor.Attributes[i].GetType() == typeof(BindingNameAttribute))
{
// set property value.
propertyDescriptor.SetValue(bindingContext.Model, controllerContext.HttpContext.Request.Form[(propertyDescriptor.Attributes[i] as BindingNameAttribute).Name]);
break;
}
}
}
}
Атрибут
public class BindingNameAttribute : Attribute
{
public string Name { get; set; }
public BindingNameAttribute()
{
}
}
ViewModel
public class EmployeeViewModel
{
[BindingName(Name = "txtName")]
public string TestProperty
{
get;
set;
}
}
затем использовать Binder в контроллере
[HttpPost]
public ActionResult SaveEmployee(int Id, [ModelBinder(typeof(MyPropertyBinder))] EmployeeViewModel viewModel)
{
// do stuff here
}
значение формы txtName должно быть установлено в TestProperty.
Ответ 4
Поэтому я провел большую часть дня, пытаясь понять, почему я не мог заставить это работать. Поскольку я делаю свои звонки из System.Web.Http.ApiController
, получается, что вы не можете использовать решение DefaultPropertyBinder
, как упомянуто выше, но вместо этого должны использовать класс IModelBinder
.
класс, который я написал для замены основополагающей работы @AndreasZoltan, как написано выше, выглядит следующим образом:
using System.Reflection;
using System.Web;
using System.Web.Http.Controllers;
using System.Web.Http.ModelBinding;
using QueryStringAlias.Attributes;
namespace QueryStringAlias.ModelBinders
{
public class AliasModelBinder : IModelBinder
{
private bool TryAdd(PropertyInfo pi, NameValueCollection nvc, string key, ref object model)
{
if (nvc[key] != null)
{
try
{
pi.SetValue(model, Convert.ChangeType(nvc[key], pi.PropertyType));
return true;
}
catch (Exception e)
{
Debug.WriteLine($"Skipped: {pi.Name}\nReason: {e.Message}");
}
}
return false;
}
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
Type bt = bindingContext.ModelType;
object model = Activator.CreateInstance(bt);
string QueryBody = actionContext.Request.Content.ReadAsStringAsync().Result;
NameValueCollection nvc = HttpUtility.ParseQueryString(QueryBody);
foreach (PropertyInfo pi in bt.GetProperties())
{
if (TryAdd(pi, nvc, pi.Name, ref model))
{
continue;
};
foreach (BindAliasAttribute cad in pi.GetCustomAttributes<BindAliasAttribute>())
{
if (TryAdd(pi, nvc, cad.Alias, ref model))
{
break;
}
}
}
bindingContext.Model = model;
return true;
}
}
}
Чтобы убедиться, что это выполняется как часть вызова WebAPI, вы также должны добавить config.BindParameter(typeof(TestModelType), new AliasModelBinder());
в часть Regiser вашего WebApiConfig
.
Если вы используете этот метод, вы также должны удалить [FromBody]
из подписи вашего метода.
[HttpPost]
[Route("mytestendpoint")]
[System.Web.Mvc.ValidateAntiForgeryToken]
public async Task<MyApiCallResult> Signup(TestModelType tmt) // note that [FromBody] does not appear in the signature
{
// code happens here
}
Обратите внимание, что эта работа основана на ответе выше с использованием примеров QueryStringAlias
.
На данный момент это, скорее всего, не получится в случае, когда TestModelType имеет сложные вложенные типы. В идеале есть еще несколько вещей:
- надежно обрабатывать сложные вложенные типы
- включить атрибут в классе, чтобы активировать IModelBuilder, в отличие от регистрации
- включить один и тот же IModelBuilder для работы как с контроллерами, так и с ApiControllers
Но пока я доволен этим для своих нужд. Надеюсь, кто-то найдет этот кусок полезным.