简体   繁体   English

ASP.NET MVC 3:具有继承/多态的DefaultModelBinder

[英]ASP.NET MVC 3: DefaultModelBinder with inheritance/polymorphism

First, sorry for the big post (I've tried to do some research first) and for the mix of technologies on the same question (ASP.NET MVC 3, Ninject and MvcContrib). 首先,对于大帖子(我先尝试做一些研究)和同一问题的混合技术(ASP.NET MVC 3,Ninject和MvcContrib),我感到很遗憾。

I'm developing a project with ASP.NET MVC 3 to handle some client orders. 我正在使用ASP.NET MVC 3开发一个项目来处理一些客户端订单。

In short: I have some objects inherited from and abstract class Order and I need to parse them when a POST request is made to my controller. 简而言之:我有一些继承自的对象和抽象类Order ,当我向控制器发出POST请求时,我需要解析它们。 How can I resolve the correct type? 我该如何解决正确的类型? Do I need to override the DefaultModelBinder class or there are some other way to do this? 我是否需要覆盖DefaultModelBinder类或有其他方法来执行此操作? Can somebody provide me some code or other links on how to do this? 有人可以提供一些代码或其他链接,如何做到这一点? Any help would be great! 任何帮助都会很棒! If the post is confusing I can do any change to make it clear! 如果帖子令人困惑,我可以做任何改变以表明清楚!

So, I have the following inheritance tree for the orders I need to handle: 所以,对于我需要处理的订单,我有以下继承树:

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 */ }

This classes are all generated by Entity Framework, so I won't modify them because I will need to update the model (I know I can extend them). 这些类都是由Entity Framework生成的,所以我不会修改它们,因为我需要更新模型(我知道我可以扩展它们)。 Also, there will be more orders, but all derived from Order . 此外,还会有更多订单,但都来自Order

I have a generic view ( Create.aspx ) in order to create a order and this view calls a strongly-typed partial view for each of the inherited orders (in this case OrderBottling and OrderFinishing ). 我有一个通用视图( Create.aspx )来创建一个订单,这个视图调用每个继承订单的强类型部分视图(在本例中为OrderBottlingOrderFinishing )。 I defined a Create() method for a GET request and other for a POST request on OrderController class. 我为OrderController类的POST请求定义了GET请求的Create()方法和其他方法。 The second is like the following: 第二个是如下:

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

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

Now the problem: when I receive the POST request with the data from the form, MVC's default binder tries to instantiate an Order object, which is OK since the type of the method is that. 现在的问题是:当我收到带有表单数据的POST请求时,MVC的默认绑定器尝试实例化一个Order对象,这是正常的,因为方法的类型就是这样。 But because Order is abstract, it cannot be instantiated, which is what is supposed to do. 但是因为Order是抽象的,所以它无法实例化,这应该是应该做的。

The question: how can I discover which concrete Order type is sent by the view? 问题:如何查看视图发送的具体Order类型?

I've already searched here on Stack Overflow and googled a lot about this (I'm working on this problem for about 3 days now!) and found some ways to solve some similar problems, but I couldn't find anything like my real problem. 我已经在这里搜索了Stack Overflow并搜索了很多关于这个问题(我现在正在处理这个问题大约3天!)并找到了一些方法来解决一些类似的问题,但我找不到像我这样的问题问题。 Two options for solving this: 解决此问题的两个选项:

  • override ASP.NET MVC DefaultModelBinder and use Direct Injection to discover which type is the Order ; 覆盖ASP.NET MVC DefaultModelBinder并使用Direct Injection来发现Order是哪种类型;
  • create a method for each order (not beautiful and would be problematic to maintain). 为每个订单创建一个方法(不是很漂亮,维护起来会有问题)。

I haven't tried the second option because I don't think it's the right way to solve the problem. 我没有尝试过第二种选择,因为我不认为这是解决问题的正确方法。 For the first option I've tried Ninject to resolve the type of the order and instantiate it. 对于第一个选项,我尝试了Ninject来解决订单的类型并实例化它。 My Ninject module is like the following: 我的Ninject模块如下所示:

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

I've have tried to get one of the types throught Ninject's Get<>() method, but it tells me that the are more then one way to resolve the type. 我已经尝试通过Ninject的Get<>()方法获得其中一种类型,但它告诉我这是解决类型的方法之一。 So, I understand the module is not well implemented. 所以,我理解该模块没有很好地实现。 I've also tried to implement like this for both types: Bind<Order>().To<OrderBottling>().WithPropertyInject("OrderTypeId", 2); 我也尝试过这两种类型的实现: Bind<Order>().To<OrderBottling>().WithPropertyInject("OrderTypeId", 2); , but it has the same problem... What would be the right way to implement this module? ,但它有同样的问题......实施这个模块的正确方法是什么?

I've also tried use MvcContrib Model Binder. 我也尝试过使用MvcContrib Model Binder。 I've done this: 我这样做了:

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

and on Global.asax.cs I've done this: Global.asax.cs我做到了:

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

    RegisterRoutes(RouteTable.Routes);

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

But this throws an exception: System.MissingMethodException: Cannot create an abstract class . 但是这会引发异常: System.MissingMethodException:无法创建抽象类 So, I presume the binder isn't or can't resolve to the correct type. 所以,我认为活页夹没有或无法解析为正确的类型。

Many many thanks in advance! 非常感谢提前!

Edit: first of all, thank you Martin and Jason for your answers and sorry for the delay! 编辑:首先,谢谢Martin和Jason的回答,并对延迟感到抱歉! I tried both approaches and both worked! 我尝试了两种方法并且都工作了! I marked Martin's answer as correct because it is more flexible and meets some of the needs for my project. 我认为Martin的答案是正确的,因为它更灵活,满足了我项目的一些需求。 Specifically, the IDs for each request are stored on a database and putting them on the class can break the software if I change the ID only in one place (database or on the class). 具体来说,每个请求的ID都存储在数据库中,如果我只在一个地方(数据库或类)更改ID,则将它们放在类上会破坏软件。 Martin's approach is very flexible in that point. 马丁的方法在这一点上非常灵活。

@Martin: on my code I changed the line @Martin:在我的代码上我更改了这一行

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

to

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

because my classes where on another project (and so, on a different assembly). 因为我的课程在另一个项目上(在不同的程序集上)。 I'm sharing this because it's seems a like more flexible than getting only the executing assembly that cannot resolve types on external assemblies. 我正在分享这个,因为它似乎比只获得无法解析外部程序集类型的执行程序集更灵活。 In my case all the order classes are on the same assembly. 在我的例子中,所有订单类都在同一个程序集中。 It's not better nor a magic formula, but I think is interesting to share this ;) 这不是更好,也不是一个神奇的公式,但我觉得分享这个很有意思;)

I've tried to do something similar before and I came to the conclusion that there's nothing built in which will handle this. 我之前尝试过类似的事情,我得出的结论是,没有任何内置可以解决这个问题。

The option I went with was to create my own model binder (though inherited from the default so its not too much code). 我选择的选项是创建我自己的模型绑定器(虽然从默认继承,所以它没有太多的代码)。 It looked for a post back value with the name of the type called xxxConcreteType where xxx was another type it was binding to. 它查找了一个post back值,其名称为xxxConcreteType,其中xxx是它绑定的另一种类型。 This means that a field must be posted back with the value of the type you're trying to bind; 这意味着必须使用您尝试绑定的类型的值回发字段; in this case OrderConcreteType with a value of either OrderBottling or OrderFinishing. 在这种情况下,OrderConcreteType的值为OrderBottling或OrderFinishing。

Your other alternative is to use UpdateModel or TryUpdateModel and ommit the parameter from your method. 您的另一种选择是使用UpdateModel或TryUpdateModel并从您的方法中省略该参数。 You will need to determine which kind of model you're updating before calling this (either by a parameter or otherwise) and instantiate the class beforehand, then you can use either method to popuplate it 您需要在调用之前确定要更新的模型类型(通过参数或其他方式)并事先实例化该类,然后您可以使用任一方法来填充它

Edit: 编辑:

Here is the code.. 这是代码..

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

Change your action method to look like this: 将您的操作方法更改为如下所示:

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

You would need to put the following in your view: 您需要在视图中添加以下内容:

@Html.Hidden("orderType, "Namespace.xxx.OrderBottling")

You can create a custome ModelBinder that operates when your action accepts a certain type, and it can create an object of whatever type you want to return. 您可以创建一个在您的操作接受某种类型时操作的ModelBinder,它可以创建您想要返回的任何类型的对象。 The CreateModel() method takes a ControllerContext and ModelBindingContext that give you access to the parameters passed by route, url querystring and post that you can use to populate your object with values. CreateModel()方法采用ControllerContext和ModelBindingContext,使您可以访问route,url querystring和post传递的参数,您可以使用这些参数用值填充对象。 The default model binder implementation converts values for properties of the same name to put them in the fields of the object. 默认模型绑定器实现转换同名属性的值,以将它们放在对象的字段中。

What I do here is simply check one of the values to determine what type to create, then call the DefaultModelBinder.CreateModel() method switching the type it is to create to the appropriate type. 我在这里做的只是检查其中一个值以确定要创建的类型,然后调用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
    }
}

Set it to be used when you have an Order parameter on your actions by adding this to Application_Start() in Global.asax.cs: 通过将其添加到Global.asax.cs中的Application_Start(),将其设置为在操作上具有Order参数时使用:

ModelBinders.Binders.Add(typeof(Order), new OrderModelBinder());

You can also build a generic ModelBinder that works for all of your abstract models. 您还可以构建适用于所有抽象模型的通用ModelBinder。 My solution requires you to add a hidden field to your view called 'ModelTypeName' with the value set to the name of the concrete type that you want. 我的解决方案要求您在视图中添加一个名为“ModelTypeName”的隐藏字段,其值设置为您想要的具体类型的名称。 However, it should be possible to make this thing smarter and pick a concrete type by matching type properties to fields in the view. 但是,应该可以使这个更聪明,并通过将类型属性与视图中的字段匹配来选择具体类型。

In your Global.asax.cs Application_Start(): 在Global.asax.cs Application_Start()中:

ModelBinders.Binders.DefaultBinder = new CustomModelBinder();

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

My solution for that problem support complex models that can contain other abstract class, multiple inheritance, collections or generic classes. 我对该问题的解决方案支持可以包含其他抽象类,多继承,集合或泛型类的复杂模型。

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

As you see you have to add field (of name Type ) that contains information what concrete class inheriting from abstract class should be created. 如您所见,您必须添加字段(名称为Type ),其中包含应创建从抽象类继承的具体类的信息。 For example classes: class abstract Content , class TextContent , the Content should have Type set to "TextContent". 例如类: 类抽象内容类TextContent ,内容应将Type设置为“TextContent”。 Remember to switch default model binder in global.asax: 请记住在global.asax中切换默认模型绑定器:

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

For more information and sample project check following link . 有关更多信息和示例项目检查以下链接

Change the line: 换行:

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

To this: 对此:

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

This is a naive implementation that checks every assembly for the type. 这是一个天真的实现,它检查每个类型的程序集。 I'm sure there's smarter ways to do it, but this works well enough. 我确信有更聪明的方法可以做到这一点,但这种方法效果很好。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM