ASP.NET MVC 3: DefaultModelBinder с наследованием/полиморфизмом

Во-первых, извините за большой пост (сначала я попытался провести некоторое исследование) и за сочетание технологий по одному и тому же вопросу (ASP.NET MVC 3, Ninject и MvcContrib).

Я разрабатываю проект с ASP.NET MVC 3 для обработки некоторых клиентских заказов.

Вкратце: у меня есть некоторые объекты, унаследованные от абстрактного класса Order, и мне нужно проанализировать их, когда к моему контроллеру отправляется запрос POST. Как я могу определить правильный тип? Нужно ли мне переопределять класс DefaultModelBinder или есть другой способ сделать это? Может ли кто-нибудь предоставить мне код или другие ссылки о том, как это сделать? Любая помощь будет здорово! Если сообщение сбивает с толку, я могу внести любые изменения, чтобы прояснить его!

Итак, у меня есть следующее дерево наследования для заказов, которые мне нужно обрабатывать:

public abstract partial class Order {

    public Int32 OrderTypeId {get; set; }

    /* rest of the implementation ommited */
}

public class OrderBottling : Order { /* implementation ommited */ }

public class OrderFinishing : Order { /* implementation ommited */ }

Все эти классы созданы Entity Framework, поэтому я не буду их изменять, потому что мне нужно будет обновить модель (я знаю, что могу их расширить). Также будет больше заказов, но все производные от Order.

У меня есть общее представление (Create.aspx) для создания заказа, и это представление вызывает строго типизированное частичное представление для каждого из унаследованных заказов (в данном случае OrderBottling и OrderFinishing). Я определил метод Create() для запроса GET и другой метод для запроса POST на OrderControllerclass. Второй выглядит следующим образом:

public class OrderController : Controller
{
    /* rest of the implementation ommited */

    [HttpPost]
    public ActionResult Create(Order order) { /* implementation ommited */ }
}

Теперь проблема: когда я получаю запрос POST с данными из формы, связыватель MVC по умолчанию пытается создать экземпляр объекта Order, что нормально, поскольку тип метода такой. Но поскольку Order является абстрактным, его нельзя создать, что и предполагается делать.

Вопрос: как узнать, какой конкретный тип Order отправляется представлением?

Я уже искал здесь на Stack Overflow и много гуглил об этом (я работаю над этой проблемой уже около 3 дней!) И нашел несколько способов решить некоторые похожие проблемы, но я не смог найти ничего похожего на мой реальный проблема. Два варианта решения этого:

  • переопределить ASP.NET MVC DefaultModelBinder и использовать Direct Injection, чтобы узнать, какой тип является Order;
  • создайте метод для каждого заказа (не красиво и будет проблематично поддерживать).

Я не пробовал второй вариант, потому что не думаю, что это правильный способ решить проблему. Для первого варианта я попробовал Ninject, чтобы определить тип заказа и создать его экземпляр. Мой модуль Ninject выглядит следующим образом:

private class OrdersService : NinjectModule
{
    public override void Load()
    {
        Bind<Order>().To<OrderBottling>();
        Bind<Order>().To<OrderFinishing>();
    }
}

Я пытался получить один из типов с помощью метода Get<>() Ninject, но он говорит мне, что существует более чем один способ разрешения типа. Итак, я так понимаю, что модуль не очень хорошо реализован. Я также пытался реализовать подобное для обоих типов: Bind<Order>().To<OrderBottling>().WithPropertyInject("OrderTypeId", 2);, но у него та же проблема... Как правильно реализовать этот модуль?

Я также пробовал использовать MvcContrib Model Binder. Я сделал это:

[DerivedTypeBinderAware(typeof(OrderBottling))]
[DerivedTypeBinderAware(typeof(OrderFinishing))]
public abstract partial class Order { }

и Global.asax.cs я сделал это:

protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();

    RegisterRoutes(RouteTable.Routes);

    ModelBinders.Binders.Add(typeof(Order), new DerivedTypeModelBinder());
}

Но это вызывает исключение: System.MissingMethodException: невозможно создать абстрактный класс. Итак, я предполагаю, что связующее устройство не имеет или не может разрешить правильный тип.

Заранее огромное спасибо!

Редактировать: во-первых, спасибо Мартину и Джейсону за ответы и извините за задержку! Я попробовал оба подхода, и оба сработали! Я отметил ответ Мартина как правильный, потому что он более гибкий и отвечает некоторым потребностям моего проекта. В частности, идентификаторы для каждого запроса хранятся в базе данных, и размещение их в классе может привести к поломке программного обеспечения, если я изменю идентификатор только в одном месте (базе данных или в классе). В этом отношении подход Мартина очень гибкий.

@Martin: в моем коде я изменил строку

var concreteType = Assembly.GetExecutingAssembly().GetType(concreteTypeValue.AttemptedValue);

to

var concreteType = Assembly.GetAssembly(typeof(Order)).GetType(concreteTypeValue.AttemptedValue);

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


person jmpcm    schedule 28.03.2011    source источник
comment
полная трассировка стека всегда облегчает диагностику проблем.   -  person Mauricio Scheffer    schedule 28.03.2011
comment
@jmpcm - Просто чтобы убедиться, сработает ли это, если вы удалите модификатор abstract из Order?   -  person Sergi Papaseit    schedule 28.03.2011
comment
@Sergi: нет, это тоже не работает. В один из предыдущих раз, когда я сгенерировал модель, я не поставил Order как абстрактный, и результат был тем же, но с другой ошибкой (не могу вспомнить, что это было).   -  person jmpcm    schedule 28.03.2011
comment
@Sergi, без модификатора abstract связующее устройство модели по умолчанию создавало бы экземпляр объекта Order и заполнило бы свойства объекта Order; это не будет связывать свойства подтипа   -  person Martin Booth    schedule 28.03.2011
comment
@Martin - свойства подтипа все равно теряются, потому что действие принимает Order в качестве аргумента, не так ли? ModelBinder попытается понять объект Order, основываясь на том, какие свойства ему могут соответствовать.   -  person Sergi Papaseit    schedule 28.03.2011
comment
@Sergi: да, ты прав, но свойства для определенного заказа не могут быть потеряны! Вот почему Order будет, скажем так, общим типом для всех заказов. Если связыватель модели может исправить тип заказа, проблема решена.   -  person jmpcm    schedule 28.03.2011
comment
@jmpcm - я понимаю вашу проблему, спасибо за разъяснение. Я просто на мгновение вырвал комментарий @Martin (неправильно) из контекста вашего вопроса; отсюда и моя скидка :)   -  person Sergi Papaseit    schedule 28.03.2011
comment
Без проблем! Спасибо за интерес :)   -  person jmpcm    schedule 28.03.2011
comment
Нашел этот пост, когда пытался сделать то же самое. Единственное, что я хотел бы добавить, так как это была и моя проблема, если типы исходят из отдельной сборки, но подклассы находятся в той же сборке, вы могли бы вместо этого использовать modelType.Assembly.GetType(concreteTypeValue.AttemptedValue), это сохранит абстракцию между связующим и вашим бизнесом модель.   -  person Jay    schedule 07.09.2012
comment
Я хотел бы отметить, что в Ninject вы не можете связать один и тот же абстрактный тип с несколькими конкретными типами, хотя для достижения того же эффекта вы можете связать абстрактный тип с фабричным методом для достижения того же. Это не решит вашу настоящую проблему, но устранит ошибку, связанную с Ninject.   -  person Umar Farooq Khawaja    schedule 07.12.2012


Ответы (5)


Я пытался сделать что-то подобное раньше, и я пришел к выводу, что нет ничего встроенного, что справится с этим.

Вариант, который я выбрал, состоял в том, чтобы создать свою собственную привязку модели (хотя и унаследованную от по умолчанию, поэтому кода не так уж много). Он искал значение обратного сообщения с именем типа, называемого xxxConcreteType, где xxx был другим типом, к которому он был привязан. Это означает, что поле должно быть отправлено обратно со значением типа, который вы пытаетесь связать; в этом случае OrderConcreteType со значением OrderBottling или OrderFinishing.

Другой альтернативой является использование UpdateModel или TryUpdateModel и исключение параметра из вашего метода. Вам нужно будет определить, какой тип модели вы обновляете, прежде чем вызывать это (либо с помощью параметра, либо иным образом) и заранее создать экземпляр класса, после чего вы можете использовать любой метод для его заполнения.

Редактировать:

Вот код..

public class AbstractBindAttribute : CustomModelBinderAttribute
{
    public string ConcreteTypeParameter { get; set; }

    public override IModelBinder GetBinder()
    {
        return new AbstractModelBinder(ConcreteTypeParameter);
    }

    private class AbstractModelBinder : DefaultModelBinder
    {
        private readonly string concreteTypeParameterName;

        public AbstractModelBinder(string concreteTypeParameterName)
        {
            this.concreteTypeParameterName = concreteTypeParameterName;
        }

        protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
        {
            var concreteTypeValue = bindingContext.ValueProvider.GetValue(concreteTypeParameterName);

            if (concreteTypeValue == null)
                throw new Exception("Concrete type value not specified for abstract class binding");

            var concreteType = Assembly.GetExecutingAssembly().GetType(concreteTypeValue.AttemptedValue);

            if (concreteType == null)
                throw new Exception("Cannot create abstract model");

            if (!concreteType.IsSubclassOf(modelType))
                throw new Exception("Incorrect model type specified");

            var concreteInstance = Activator.CreateInstance(concreteType);

            bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => concreteInstance, concreteType);

            return concreteInstance;
        }
    }
}

Измените метод действия, чтобы он выглядел следующим образом:

public ActionResult Create([AbstractBind(ConcreteTypeParameter = "orderType")] Order order) { /* implementation ommited */ }

Вам нужно будет указать следующее:

@Html.Hidden("orderType, "Namespace.xxx.OrderBottling")
person Martin Booth    schedule 28.03.2011
comment
Привет Мартин! Спасибо за идею! То, что вы описываете, было одним из вариантов, которые я видел во время DefaultModelBinder исследования, которое я провел. Я не пробовал этот способ, но если он лучший, обязательно попробую! Я был бы очень благодарен, если бы вы могли разместить здесь какой-нибудь код! :) - person jmpcm; 28.03.2011
comment
Кто-нибудь смог заставить это работать в более сложных сценариях? Я успешно использовал это, изменив var concreteType = Assembly.GetExecutingAssembly().GetType(concreteTypeValue.AttemptedValue); для поиска всех загруженных сборок, но мне нужно иметь родительскую‹==›дочернюю модель представления, где дочерняя модель является моделью представления с изменяемым свойством абстрактного типа... похоже, это не очень нравится хорошо... Есть намеки? - person Richard B; 19.01.2012
comment
См. мой дополнительный ответ ниже для версии, которая проверяет все загруженные сборки. - person Corey Cole; 21.02.2012
comment
Ваше решение не поддерживает множественное наследование, ни коллекции, ни общие классы. Проверьте мое решение, которое хорошо работает с все виды моделей. - person MaciejLisCK; 11.05.2012

Вы можете создать пользовательский ModelBinder, который работает, когда ваше действие принимает определенный тип, и может создавать объект любого типа, который вы хотите вернуть. Метод CreateModel() принимает ControllerContext и ModelBindingContext, которые предоставляют вам доступ к параметрам, передаваемым маршрутом, строкой запроса URL и сообщением, которые вы можете использовать для заполнения вашего объекта значениями. Реализация связывателя модели по умолчанию преобразует значения для свойств с тем же именем, чтобы поместить их в поля объекта.

Здесь я просто проверяю одно из значений, чтобы определить, какой тип создать, а затем вызываю метод DefaultModelBinder.CreateModel(), переключающий тип, который он должен создать, на соответствующий тип.

public class OrderModelBinder : DefaultModelBinder
{
    protected override object CreateModel(
        ControllerContext controllerContext,
        ModelBindingContext bindingContext,
        Type modelType)
    {
        // get the parameter OrderTypeId
        ValueProviderResult result;
        result = bindingContext.ValueProvider.GetValue("OrderTypeId");
        if (result == null)
            return null; // OrderTypeId must be specified

        // I'm assuming 1 for Bottling, 2 for Finishing
        if (result.AttemptedValue.Equals("1"))
            return base.CreateModel(controllerContext,
                    bindingContext,
                    typeof(OrderBottling));
        else if (result.AttemptedValue.Equals("2"))
            return base.CreateModel(controllerContext,
                    bindingContext,
                    typeof(OrderFinishing));
        return null; // unknown OrderTypeId
    }
}

Установите его для использования, когда у вас есть параметр Order в ваших действиях, добавив его в Application_Start() в Global.asax.cs:

ModelBinders.Binders.Add(typeof(Order), new OrderModelBinder());
person Jason Goemaat    schedule 29.03.2011

Вы также можете создать универсальный ModelBinder, который работает для всех ваших абстрактных моделей. Мое решение требует, чтобы вы добавили в представление скрытое поле под названием «ModelTypeName» со значением, равным имени конкретного типа, который вы хотите. Однако должна быть возможность сделать эту вещь умнее и выбрать конкретный тип, сопоставив свойства типа с полями в представлении.

В вашем Global.asax.cs Application_Start():

ModelBinders.Binders.DefaultBinder = new CustomModelBinder();

Кастомоделбиндер:

public class CustomModelBinder : DefaultModelBinder 
{
    protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
    {
        if (modelType.IsAbstract)
        {
            var modelTypeValue = controllerContext.Controller.ValueProvider.GetValue("ModelTypeName");
            if (modelTypeValue == null)
                throw new Exception("View does not contain ModelTypeName");

            var modelTypeName = modelTypeValue.AttemptedValue;

            var type = modelType.Assembly.GetTypes().SingleOrDefault(x => x.IsSubclassOf(modelType) && x.Name == modelTypeName);
            if(type == null)
                throw new Exception("Invalid ModelTypeName");

            var concreteInstance = Activator.CreateInstance(type);

            bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => concreteInstance, type);

            return concreteInstance;

        }

        return base.CreateModel(controllerContext, bindingContext, modelType);
    }
}
person Kelly    schedule 22.09.2011

Мое решение этой проблемы поддерживает сложные модели, которые могут содержать другие абстрактные классы, множественное наследование, коллекции или общие классы.

public class EnhancedModelBinder : DefaultModelBinder
{
    protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
    {
        Type type = modelType;
        if (modelType.IsGenericType)
        {
            Type genericTypeDefinition = modelType.GetGenericTypeDefinition();
            if (genericTypeDefinition == typeof(IDictionary<,>))
            {
                type = typeof(Dictionary<,>).MakeGenericType(modelType.GetGenericArguments());
            }
            else if (((genericTypeDefinition == typeof(IEnumerable<>)) || (genericTypeDefinition == typeof(ICollection<>))) || (genericTypeDefinition == typeof(IList<>)))
            {
                type = typeof(List<>).MakeGenericType(modelType.GetGenericArguments());
            }
            return Activator.CreateInstance(type);            
        }
        else if(modelType.IsAbstract)
        {
            string concreteTypeName = bindingContext.ModelName + ".Type";
            var concreteTypeResult = bindingContext.ValueProvider.GetValue(concreteTypeName);

            if (concreteTypeResult == null)
                throw new Exception("Concrete type for abstract class not specified");

            type = Assembly.GetExecutingAssembly().GetTypes().SingleOrDefault(t => t.IsSubclassOf(modelType) && t.Name == concreteTypeResult.AttemptedValue);

            if (type == null)
                throw new Exception(String.Format("Concrete model type {0} not found", concreteTypeResult.AttemptedValue));

            var instance = Activator.CreateInstance(type);
            bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => instance, type);
            return instance;
        }
        else
        {
            return Activator.CreateInstance(modelType);
        }
    }
}

Как видите, вам нужно добавить поле (с именем Type), которое содержит информацию о том, какой конкретный класс, наследующий от абстрактного класса, должен быть создан. Например, классы: class abstract Content, class TextContent, Content должен иметь тип, установленный на "TextContent". Не забудьте переключить связыватель модели по умолчанию в global.asax:

protected void Application_Start()
{
    ModelBinders.Binders.DefaultBinder = new EnhancedModelBinder();
    [...]

Для получения дополнительной информации и примера проекта перейдите по ссылке.

person MaciejLisCK    schedule 11.05.2012

Измените строку:

var concreteType = Assembly.GetExecutingAssembly().GetType(concreteTypeValue.AttemptedValue);

К этому:

            Type concreteType = null;
            var loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies();
            foreach (var assembly in loadedAssemblies)
            {
                concreteType = assembly.GetType(concreteTypeValue.AttemptedValue);
                if (null != concreteType)
                {
                    break;
                }
            }

Это наивная реализация, которая проверяет каждую сборку на тип. Я уверен, что есть более умные способы сделать это, но это работает достаточно хорошо.

person Corey Cole    schedule 21.02.2012
comment
Привет @Кори! Спасибо за подсказку, но, как вы можете видеть в моем редактировании вопроса, я уже это сделал. Это более краткий способ написать то, что вы предложили. Программное обеспечение, которое имеет этот код и для которого я задал этот вопрос, работает почти год без нареканий :) Спасибо за интерес! - person jmpcm; 22.02.2012