Asp.Net MVC 2 - 将模型的属性绑定到不同的命名值

时间:2010-11-30 16:55:52

标签: c# asp.net-mvc asp.net-mvc-2

更新(2016年9月21日) - 感谢Digbyswift评论此解决方案仍可在MVC5中运行。

更新(2012年4月30日) - 请注意那些在搜索等问题上遇到此问题的人 - 接受的答案不是我最终如何做到这一点 - 但我接受了它,因为它可能在某些情况下有效。 My own answer contains the final solution I used,可重复使用,适用于任何项目。

它也被证实可以在MVC框架的v3和v4中工作。

我有以下模型类型(类的名称及其属性已更改以保护其身份):

public class MyExampleModel
{
  public string[] LongPropertyName { get; set; }
}

然后将此属性绑定到一堆(> 150)复选框,其中每个输入名称当然是LongPropertyName

表单使用HTTP GET提交到url,并说用户选择其中三个复选框 - 该url将具有查询字符串?LongPropertyName=a&LongPropertyName=b&LongPropertyName=c

那么大问题是,如果我选中所有(甚至只有一半以上!)的复选框,我会超过IIS上请求过滤器强制执行的最大查询字符串长度!

我不想扩展它 - 所以我想要一种方法来减少这个查询字符串(我知道我可以切换到POST - 但即便如此,我仍然希望最小化发送的数据中的绒毛数量客户)。

我想要做的是将LongPropertyName绑定到简单的“L”,以便查询字符串变为?L=a&L=b&L=c 而不更改代码中的属性名称

有问题的类型已经有一个自定义模型绑定器(从DefaultModelBinder派生),但它附加到它的基类 - 所以我不想在那里为派生类放置代码。所有属性绑定当前都由标准的DefaultModelBinder逻辑执行,我知道它使用System.ComponentModel中的TypeDescriptors和Property Descriptors等。

我有点希望可能有一个属性我可以申请该物业来完成这项工作 - 是吗?或者我应该考虑实施ICustomTypeDescriptor

5 个答案:

答案 0 :(得分:82)

回应michaelalm的回答和要求 - 这就是我最终做的事情。我已经离开了原来的答案,主要是出于礼貌,因为Nathan建议的解决方案之一会起作用。

此输出是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找到,但是将其名称作为别名。

接下来我们有了新的模型binder类:

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());
  }
}

然而,从技术上讲,这就是它的全部内容。现在,您可以使用在此SO Change the default model binder in asp.net MVC中作为答案发布的解决方案将此DefaultModelBinderEx类注册为默认类,或者您可以将其用作您自己的模型装订器的基础。

一旦你选择了你想要活页夹的模式,你只需将它应用于模型类型,如下所示:

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执行的属性搜索中继承的。

在我的情况下,这是可以的,我正在开发一个新的站点框架,并且能够使用其他机制将新的可扩展性推入我的基本绑定器以满足这些新类型;但对每个人来说都不是这样。

答案 1 :(得分:20)

您可以使用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;
    }
}

另一种选择是实现自定义模型绑定器,手动执行参数值赋值(如上所述),但这很可能是矫枉过正。以下是一个例子,如果您有兴趣:Flags Enumeration Model Binder

答案 2 :(得分:4)

这会是一个类似于你的安德拉斯的解决方案吗?我希望你也可以发布你的答案。

控制器方法

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()
    {

    }
}

视图模型

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。

答案 3 :(得分:3)

这可能是对安德拉斯·佐尔坦(Andras Zoltan)的回答的简短评论,但没有足够的声誉,对不起。

感谢您的解决方案,我刚刚使用了它,但是效果仍然不错!但是,我的某些属性的别名相同,但大小写不同,例如

[BindAlias("signature")]
public string Signature { get; set; }

当自定义模型联编程序尝试将别名添加到 基本模型绑定程序已经添加了PropertyMetadata字典,因为它们的主要属性名称版本已经添加,并且模型绑定不区分大小写。

要解决此问题,只需执行不区分大小写的检查-

替换

if (bindingContext.PropertyMetadata.ContainsKey(p.Name))

使用

if (bindingContext.PropertyMetadata.ContainsKey(p.Name)
    && !string.Equals(p.Name, attr.Alias, StringComparison.OrdinalIgnoreCase))

答案 4 :(得分:1)

所以我花了大部分时间试图弄清楚为什么我不能使它起作用。由于我是从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在Controller和ApiController中均可使用

但是现在我对满足自己的需求感到满意。希望有人觉得这有用。