Подтвердить что ты не робот

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 в классе OrderController. Второй вариант выглядит следующим образом:

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

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

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

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

Я уже искал здесь в Qaru и много рассказывал об этом (я работаю над этой проблемой уже около 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>();
    }
}

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

к

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

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

4b9b3361

Ответ 1

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

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

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

Edit:

Вот код.

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")

Ответ 2

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

Что я здесь делаю, просто проверьте одно из значений, чтобы определить, какой тип создать, затем вызовите метод 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());

Ответ 3

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

В вашем приложении Global.asax.cs Application_Start():

ModelBinders.Binders.DefaultBinder = new CustomModelBinder();

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

Ответ 4

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

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

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

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

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

Ответ 5

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

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

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