【问题标题】:Asp.Net MVC 2 - Bind a model's property to a different named valueAsp.Net MVC 2 - 将模型的属性绑定到不同的命名值
【发布时间】:2010-11-30 16:55:52
【问题描述】:

更新(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

【问题讨论】:

  • 嘿,我最初来到这里时想知道如何为 asp.net 核心解决完全相同的问题。事情发生了变化,所以我打开了a different SO question。链接在这里。
  • @NathanCooper 酷 :)
  • @AndrasZoltan:非常感谢您提出的精彩问答。我认为您自己的答案应该是公认的答案......对于像我这样遇到这篇文章并希望通过快速查看找到正确答案的人来说,这将不会那么混乱。

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


【解决方案1】:

响应 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);
  }
}

这代理了通常由DefaultModelBinder 找到的“正确”PropertyDescriptor,但将其名称显示为别名。

接下来我们有了新的模型绑定器类:

已更新 @jsabrooke 的建议 below

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)
            && !string.Equals(p.Name, attr.Alias, StringComparison.OrdinalIgnoreCase)))
        {
            bindingContext.PropertyMetadata.Add(
                attr.Alias,
                bindingContext.PropertyMetadata[p.Name]);
        }
      }
    }

    return new PropertyDescriptorCollection
      (toReturn.Cast<PropertyDescriptor>().Concat(additional).ToArray());
  }
}

然后,从技术上讲,这就是它的全部内容。您现在可以使用在此 SO 中作为答案发布的解决方案将此 DefaultModelBinderEx 类注册为默认值:Change the default model binder in 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 包含多个别名的值,或者一个别名及其名称的属性,则可能会产生一个有趣且有点烦人的副作用。在这种情况下,将只使用检索到的值之一。但是,当您只使用objects 时,很难想出一种以类型安全的方式将它们全部合并的方法。不过,这类似于在表单帖子和查询字符串中提供值——我不确定 MVC 在那种情况下究竟做了什么——但我不认为这是推荐的做法。

当然,另一个问题是,您不能创建一个等于另一个别名的别名,或者实际上是一个实际属性的名称。

一般来说,我喜欢使用CustomModelBinderAttribute 类来应用我的模型绑定器。唯一的问题是,如果您需要从模型类型派生并更改其绑定行为 - 因为 CustomModelBinderAttribute 是在 MVC 执行的属性搜索中继承的。

在我的情况下,这没关系,我正在开发一个新的站点框架,并且能够使用其他机制将新的可扩展性推送到我的基本绑定器中以满足这些新类型;但并非所有人都这样。

【讨论】:

  • 如何生成传出 url,考虑到您的 BindAlias 属性?
  • @hival 我承认这是我不需要过多担心的一件事。每当我需要时,我通常会手动探测路由值。我确实考虑过一个解决方案(一个自定义的RouteValueDictionary,它可能使用第一个BindAlias 属性作为名称)但它只是一个不完美的解决方案,我放弃了它。
  • 效果很好,谢谢。这应该是框架中默认内容的一部分。
  • 感谢这个解决方案,它完全符合我的需要,能够扩展我们为支持这个而准备的自定义 3rd 方活页夹。
  • 嘿@AndrasZoltan 感谢您的回答。我今天大部分时间都在尝试在 ApiController 中进行相同的工作(请参阅下面的答案),看起来已经快 10 年了,但我很想看看你对这次更新的看法。
【解决方案2】:

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

【讨论】:

  • 啊,是的 - 但这是动作方法的参数 - 这是动作方法的参数的属性...
  • 不可能将BindAttribute 放在属性上,因为它仅对类或参数声明有效。不过,这将是一个完美的解决方案——正是我正在寻找的那种东西。
  • @AndrasZoltan 哦,果然。我的错误,我不确定那个。手动组合模型参数和 longPropertyName 参数可能是您最快的选择。自定义模型绑定器可能是“最干净的”。
  • 最后,我通过在它和DefaultModelBinder 之间注入一个基础来扩展已经在类型上放置的模型绑定器,该基础从应用于的属性中添加了额外的PropertyDescriptors任何要“别名”的属性。同时,它还将同一属性的 PropertyMetaData 克隆回 BindingContext 中,这样当模型绑定逻辑开始通过别名读取/写入值时,一切正常。我现在可以将它重用于任何其他类型 - 而且它只说了大约 50 行代码 - 哦,我的 q 字符串又是合法的了!
【解决方案3】:

这会是与您的 Andras 类似的解决方案吗?我希望你也可以发布你的答案。

控制器方法

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。

【讨论】:

  • 是的,它是相似的。我将在不久的将来发布我的答案,以便我们进行比较和对比:)
  • 我已经发布了我的代码。我的版本与 Request.Form 无关,因为它向模型的元数据添加了属性,因此仍然使用所有其他绑定机制(值提供者等)。它适用于所有类型,可用作全局活页夹的替代品以及您自己的自定义活页夹的基础。
【解决方案4】:

这可能应该是对 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))

【讨论】:

  • 好建议 - 我注意到你建议这是一个编辑,不幸的是,它被 2:1 拒绝了。我已批准对其进行编辑并链接到此答案,以便人们可以看到它来自您。
【解决方案5】:

所以我花了一天的大部分时间试图弄清楚为什么我不能让它工作。由于我是从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 调用的一部分运行,您还必须在 WebApiConfig 的注册器部分添加 config.BindParameter(typeof(TestModelType), new AliasModelBinder());

如果您使用此方法,您还必须从方法签名中删除 [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 和 ApiControllers 中工作

但现在我满足于我自己的需要。希望有人觉得这篇文章有用。

【讨论】:

  • 很高兴为 ApiController 和新堆栈提供新的解决方案 :)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-04-30
  • 2011-03-03
  • 2015-09-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多