Asp.Net MVC 2 - привязать свойство модели к другому названному значению - PullRequest
47 голосов
/ 30 ноября 2010

Обновление (21 сентября 2016 г.) - Спасибо Diggyswift за комментарий, что это решение по-прежнему работает и в 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?

Ответы [ 3 ]

83 голосов
/ 30 ноября 2010

В ответ на ответ и запрос Михаэля - вот что я в итоге сделал. Я оставил исходный ответ в основном из вежливости, поскольку одно из предложенных Натаном решений сработало бы.

Вывод этого является заменой для класса 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 s. Однако это похоже на предоставление значения как в форме сообщения, так и в строке запроса - и я не уверен, что именно MVC делает в этом сценарии, - но я не думаю, что это рекомендуемая практика.

Другая проблема, конечно, заключается в том, что вы не должны создавать псевдоним, равный другому псевдониму, или имя действительного свойства.

Мне нравится применять мои связующие модели, в общем, используя класс CustomModelBinderAttribute. Единственная проблема с этим может быть, если вам нужно извлечь из типа модели и изменить ее поведение привязки - так как CustomModelBinderAttribute наследуется в поиске атрибутов, выполняемом MVC.

В моем случае это нормально, я разрабатываю новую структуру сайта и могу внедрить новую расширяемость в мои базовые связующие, используя другие механизмы для удовлетворения этих новых типов; но это будет не для всех.

20 голосов
/ 30 ноября 2010

Для этого можно использовать BindAttribute .

public ActionResult Submit([Bind(Prefix = "L")] string[] longPropertyName) {

}

Обновление

Поскольку параметр longPropertyName является частью объекта модели, а не независимым параметром действия контроллера, у вас есть несколько других вариантов.

Вы можете сохранить модель и свойство как независимые параметры для вашего действия, а затем вручную объединить данные в методе действия.

public ActionResult Submit(MyModel myModel, [Bind(Prefix = "L")] string[] longPropertyName) {
    if(myModel != null) {
        myModel.LongPropertyName = longPropertyName;
    }
}

Другой вариант - реализация пользовательского связывателя модели, который выполняет назначение значения параметра (как указано выше) вручную, но это, скорее всего, излишне. Вот один из примеров, если вам интересно: Переплет модели флагов .

4 голосов
/ 18 января 2011

это решение будет похоже на ваш Андрас? Я надеюсь, что вы также можете опубликовать свой ответ.

метод управления

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.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...