Пользовательское связующее устройство для объекта

У меня есть следующее действие контроллера:

[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, и я также хочу повторно использовать это связующее. Поэтому я решил, что это решение будет включать атрибут ModelBinder перед PropertyB, который, конечно, не работает (атрибут ModelBinder не допускается для свойств). Я вижу два решения:

  • Чтобы использовать параметры действия для каждого отдельного свойства вместо всей модели (что я бы не хотел, поскольку модель обладает множеством свойств):

    public ViewResult DoSomething(string propertyA, [ModelBinder(typeof(MyModelBinder))] propertyB)
    
  • Чтобы создать новый тип, скажем MyCustomType: List<int> и зарегистрируйте привязку модели для этого типа (это опция)

  • Возможно создание связующего для MyModel, переопределить BindProperty, и если свойство "PropertyB" связывает свойство с моим настраиваемым связующим. Возможно ли это?

Есть ли другое решение?

Ответ 1

переопределить BindProperty, и если свойство "PropertyB" связывает свойство с моим пользовательским связующим

Это хорошее решение, хотя вместо проверки "это свойство" вы лучше проверяете свои собственные атрибуты, которые определяют связывание на уровне свойств, например

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

Вы можете увидеть пример BindProperty переопределить здесь.

Ответ 2

Мне действительно нравится ваше третье решение, только я бы сделал это универсальным решением для всех моделей ModelBinders, поместив его в настраиваемое связующее, которое наследует от DefaultModelBinder и настроено как стандартное связующее устройство для вашего приложения MVC.

Затем вы сделаете это новое 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; }
    }
}

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

Ответ 4

Прошло шесть лет с тех пор, как был задан этот вопрос, я предпочел бы взять это пространство, чтобы подвести итоги, вместо того, чтобы предлагать совершенно новое решение. На момент написания статьи MVC 5 уже давно существует, и ASP.NET Core только что вышел.

Я следил за подходом, рассмотренным в сообщении, написанном Виджайей Анандом (кстати, благодаря Виджае): http://www.prideparrot.com/blog/archive/2012/6/customizing_property_binding_through_attributes. И стоит отметить, что логика привязки данных помещается в пользовательский класс атрибутов, который является методом BindProperty класса StringArrayPropertyBindAttribute в примере Vijaya Anand.

Тем не менее, во всех других статьях по этой теме, которые я прочитал (включая решение @jonathanconway), пользовательский класс атрибутов - это всего лишь шаг, который заставляет фреймворк найти правильное связующее устройство для конкретной модели; и логика привязки помещается в это настраиваемое связующее устройство, которое обычно является объектом IModelBinder.

Первый подход для меня проще. Могут быть некоторые недостатки 1-го подхода, которые я еще не знал, хотя, пожалуй, я довольно новичок в области MVC на данный момент.

Кроме того, я обнаружил, что класс ExtendedModelBinder в примере Vijaya Anand не нужен в MVC 5. Кажется, класс DefaultModelBinder, который поставляется с MVC 5, достаточно умен, чтобы взаимодействовать с атрибутами привязки пользовательской модели.