ASP.NET MVC 3: DefaultModelBinder с наследованием / полиморфизмом - PullRequest
17 голосов
/ 28 марта 2011

Во-первых, извините за большой пост (сначала я попытался провести некоторые исследования) и за сочетание технологий по одному и тому же вопросу (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 отправляется представлением?

Я уже искал здесь в 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>();
    }
}

Я пытался получить один из типов, используя метод 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);

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

Ответы [ 5 ]

16 голосов
/ 28 марта 2011

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

Опция, с которой я пошел, состояла в том, чтобы создать мою собственную подшивку модели (хотя и унаследованную от значения по умолчанию, поэтому ее не слишком много). Он искал значение обратной ссылки с именем типа с именем 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")
7 голосов
/ 29 марта 2011

Вы можете создать привычный 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());
5 голосов
/ 22 сентября 2011

Вы также можете создать общий 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);
    }
}
2 голосов
/ 11 мая 2012

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

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

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

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

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

0 голосов
/ 21 февраля 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;
                }
            }

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

...