【问题标题】:Custom model binder for a property属性的自定义模型绑定器
【发布时间】:2017-12-08 04:21:49
【问题描述】:

我有以下控制器操作:

[HttpPost]
public ViewResult DoSomething(MyModel model)
{
    // do something
    return View();
}

MyModel 看起来像这样:

public class MyModel
{
    public string PropertyA {get; set;}
    public IList<int> PropertyB {get; set;}
}

所以 DefaultModelBinder 应该毫无问题地绑定它。唯一的事情是我想使用特殊/自定义活页夹来绑定PropertyB,并且我还想重用这个活页夹。所以我认为解决方案是在 PropertyB 之前放置一个 ModelBinder 属性,这当然不起作用(属性上不允许有 ModelBinder 属性)。我看到了两种解决方案:

  1. 在每个属性而不是整个模型上使用动作参数(我不喜欢这样做,因为模型有很多属性),如下所示:

    public ViewResult DoSomething(string propertyA, [ModelBinder(typeof(MyModelBinder))] propertyB)
    
  2. 要创建一个新类型,让我们说MyCustomType: List&lt;int&gt; 并为此类型注册模型绑定器(这是一个选项)

  3. 也许要为 MyModel 创建一个活页夹,覆盖 BindProperty,如果属性是 "PropertyB",则将属性与我的自定义活页夹绑定。这可能吗?

还有其他解决方案吗?

【问题讨论】:

    标签: c# asp.net-mvc model-binding custom-model-binder


    【解决方案1】:

    覆盖 BindProperty,如果 属性为“PropertyB”绑定 属性与我的自定义活页夹

    这是一个很好的解决方案,尽管您最好检查您自己的定义属性级绑定器的自定义属性,而不是检查“是 PropertyB”,例如

    [PropertyBinder(typeof(PropertyBBinder))]
    public IList<int> PropertyB {get; set;}
    

    您可以查看 BindProperty 覆盖 here 的示例。

    【讨论】:

      【解决方案2】:

      我实际上喜欢您的第三个解决方案,只是,我会将它放在一个继承自 DefaultModelBinder 并配置为您的 MVC 应用程序的默认模型绑定器的自定义绑定器中,从而使其成为所有 ModelBinders 的通用解决方案。

      然后,您将使用参数中提供的类型,让这个新的DefaultModelBinder 自动绑定任何用PropertyBinder 属性修饰的属性。

      我从这篇出色的文章中得到了这个想法:http://aboutcode.net/2011/03/12/mvc-property-binder.html

      我还将向您展示我对解决方案的看法:

      我的DefaultModelBinder

      namespace MyApp.Web.Mvc
      {
          public class DefaultModelBinder : System.Web.Mvc.DefaultModelBinder
          {
              protected override void BindProperty(
                  ControllerContext controllerContext, 
                  ModelBindingContext bindingContext, 
                  PropertyDescriptor propertyDescriptor)
              {
                  var propertyBinderAttribute = TryFindPropertyBinderAttribute(propertyDescriptor);
                  if (propertyBinderAttribute != null)
                  {
                      var binder = CreateBinder(propertyBinderAttribute);
                      var value = binder.BindModel(controllerContext, bindingContext, propertyDescriptor);
                      propertyDescriptor.SetValue(bindingContext.Model, value);
                  }
                  else // revert to the default behavior.
                  {
                      base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
                  }
              }
      
              IPropertyBinder CreateBinder(PropertyBinderAttribute propertyBinderAttribute)
              {
                  return (IPropertyBinder)DependencyResolver.Current.GetService(propertyBinderAttribute.BinderType);
              }
      
              PropertyBinderAttribute TryFindPropertyBinderAttribute(PropertyDescriptor propertyDescriptor)
              {
                  return propertyDescriptor.Attributes
                    .OfType<PropertyBinderAttribute>()
                    .FirstOrDefault();
              }
          }
      }
      

      我的IPropertyBinder界面:

      namespace MyApp.Web.Mvc
      {
          interface IPropertyBinder
          {
              object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext, MemberDescriptor memberDescriptor);
          }
      }
      

      我的PropertyBinderAttribute

      namespace MyApp.Web.Mvc
      {
          public class PropertyBinderAttribute : Attribute
          {
              public PropertyBinderAttribute(Type binderType)
              {
                  BinderType = binderType;
              }
      
              public Type BinderType { get; private set; }
          }
      }
      

      属性绑定器示例:

      namespace MyApp.Web.Mvc.PropertyBinders
      {
          public class TimeSpanBinder : IPropertyBinder
          {
              readonly HttpContextBase _httpContext;
      
              public TimeSpanBinder(HttpContextBase httpContext)
              {
                  _httpContext = httpContext;
              }
      
              public object BindModel(
                  ControllerContext controllerContext,
                  ModelBindingContext bindingContext,
                  MemberDescriptor memberDescriptor)
              {
                  var timeString = _httpContext.Request.Form[memberDescriptor.Name].ToLower();
                  var timeParts = timeString.Replace("am", "").Replace("pm", "").Trim().Split(':');
                  return
                      new TimeSpan(
                          int.Parse(timeParts[0]) + (timeString.Contains("pm") ? 12 : 0),
                          int.Parse(timeParts[1]),
                          0);
              }
          }
      }
      

      使用上述属性绑定器的示例:

      namespace MyApp.Web.Models
      {
          public class MyModel
          {
              [PropertyBinder(typeof(TimeSpanBinder))]
              public TimeSpan InspectionDate { get; set; }
          }
      }
      

      【讨论】:

      • 我想这种方法可以通过覆盖GetPropertyValue来改进。详情请见my answer
      • 是否有可能为 Web API 提供类似的 PropertyBinder 实现?
      • CreateBinder 不适用于此构造函数,因此我删除了 HttpContext 并使用 var value = bindingContext.ValueProvider.GetValue(memberDescriptor.Name); 获取属性值。
      【解决方案3】:

      @jonathanconway 的回答很好,但我想补充一点细节。

      最好覆盖GetPropertyValue 方法而不是BindProperty,以便让DefaultBinder 的验证机制有机会发挥作用。

      protected override object GetPropertyValue(
          ControllerContext controllerContext,
          ModelBindingContext bindingContext,
          PropertyDescriptor propertyDescriptor,
          IModelBinder propertyBinder)
      {
          PropertyBinderAttribute propertyBinderAttribute =
              TryFindPropertyBinderAttribute(propertyDescriptor);
          if (propertyBinderAttribute != null)
          {
              propertyBinder = CreateBinder(propertyBinderAttribute);
          }
      
          return base.GetPropertyValue(
              controllerContext,
              bindingContext,
              propertyDescriptor,
              propertyBinder);
      }
      

      【讨论】:

      • 根据您所需的用途,这可能对您有用,也可能不适用。请记住,GetPropertyValue 仅在入站对象(在本例中为请求表单/查询字符串)中的项目与属性匹配时触发。如果您想要一个能够搜索具有不同名称的元素的自定义模型绑定器,例如填充字典,那么您将不走运。例如,映射“field1=test&field2=ing&field3=now”将映射到一个字典,其中“1”是键,“test”是值。 Jonathan Conway 的解决方案确实允许这样做。也许是一个混合体,允许两者都更好。
      【解决方案4】:

      问这个问题已经6年了,我宁愿借此空间总结更新,而不是提供一个全新的解决方案。在撰写本文时,MVC 5 已经存在了很长一段时间,而 ASP.NET Core 刚刚问世。

      我遵循了 Vijaya Anand(顺便说一句,感谢 Vijaya)所写的帖子中检查的方法:http://www.prideparrot.com/blog/archive/2012/6/customizing_property_binding_through_attributes。还有一点值得注意的是,数据绑定逻辑放在了自定义属性类中,也就是 Vijaya Anand 示例中 StringArrayPropertyBindAttribute 类的 BindProperty 方法。

      然而,在我读过的所有其他关于这个主题的文章中(包括@jonathanconway 的解决方案),自定义属性类只是引导框架找到正确的自定义模型绑定器以应用的踏脚石;并且绑定逻辑放置在该自定义模型绑定器中,该绑定器通常是一个 IModelBinder 对象。

      第一种方法对我来说更简单。第一种方法可能存在一些我还不知道的缺点,因为目前我对 MVC 框架还很陌生。

      另外,我发现 Vijaya Anand 的例子中的 ExtendedModelBinder 类在 MVC 5 中是不需要的。看来 MVC 5 自带的 DefaultModelBinder 类很聪明,可以配合自定义模型绑定属性。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-08-29
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多