Подтвердить что ты не робот

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?

4b9b3361

Ответ 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

Но пока я доволен этим для своих нужд. Надеюсь, кто-то найдет этот кусок полезным.