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

Posted

技术标签:

【中文标题】ASP.NET MVC 3:具有继承/多态性的 DefaultModelBinder【英文标题】:ASP.NET MVC 3: DefaultModelBinder with inheritance/polymorphism 【发布时间】:2011-07-24 12:33:41 【问题描述】:

首先,很抱歉这篇大文章(我已经尝试先做一些研究)以及针对同一问题的技术组合(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 */ 

这些类都是由实体框架生成的,所以我不会修改它们,因为我需要更新模型(我知道我可以扩展它们)。另外,还会有更多的订单,但都是来自Order

我有一个通用视图 (Create.aspx) 来创建一个订单,这个视图为每个继承的订单(在本例中为 OrderBottlingOrderFinishing)调用一个强类型的部分视图。我为 GET 请求定义了一个 Create() 方法,并为 OrderControllerclass 上的 POST 请求定义了其他方法。第二个是这样的:

public class OrderController : Controller

    /* rest of the implementation ommited */

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

现在的问题是:当我收到带有表单数据的 POST 请求时,MVC 的默认绑定器会尝试实例化一个 Order 对象,这没关系,因为方法的类型就是这样。但是因为Order是抽象的,所以不能实例化,这是应该做的。

问题:如何发现视图发送的具体Order 类型?

我已经在 Stack Overflow 上进行了搜索,并在 Google 上搜索了很多相关信息(我正在处理这个问题大约 3 天!)并找到了一些解决类似问题的方法,但我找不到任何东西就像我真正的问题一样。解决这个问题的两种选择:

覆盖 ASP.NET MVC DefaultModelBinder 并使用直接注入来发现 Order 的类型; 为每个订单创建一个方法(不美观,维护起来会有问题)。

我没有尝试过第二种选择,因为我认为这不是解决问题的正确方法。对于第一个选项,我尝试了 Ninject 来解析订单的类型并实例化它。我的 Ninject 模块如下所示:

private class OrdersService : NinjectModule

    public override void Load()
    
        Bind<Order>().To<OrderBottling>();
        Bind<Order>().To<OrderFinishing>();
    

我曾尝试通过 Ninject 的 Get&lt;&gt;() 方法获取其中一种类型,但它告诉我解析该类型的方法不止一种。所以,我知道该模块没有很好地实现。我也尝试为这两种类型实现这样的:Bind&lt;Order&gt;().To&lt;OrderBottling&gt;().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 和 Jason 的回答,并对延误表示歉意!我尝试了这两种方法,都奏效了!我将 Martin 的答案标记为正确,因为它更灵活并且可以满足我项目的一些需求。具体来说,每个请求的 ID 都存储在数据库中,如果我只在一个地方(数据库或班级)更改 ID,将它们放在班级上可能会破坏软件。 Martin 的方法在这一点上非常灵活。

@Martin:在我的代码中我更改了行

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

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

因为我的课程在另一个项目上(因此,在不同的程序集上)。我之所以分享这个,是因为它似乎比只获取无法解析外部程序集类型的执行程序集更灵活。在我的情况下,所有订单类都在同一个程序集中。这不是更好也不是一个神奇的公式,但我认为分享这个很有趣;)

【问题讨论】:

完整的堆栈跟踪总是更容易诊断问题。 @jmpcm - 只是为了确保,如果您从 Order 中删除 abstract 修饰符,它是否有效? @Sergi:不,它也不起作用。之前生成模型的一次,我没有把 Order 作为抽象,结果是一样的,但是有不同的错误(不记得是什么了)。 @Sergi,没有 abstract 修饰符,默认模型绑定器将实例化一个 Order 对象并填充 Order 对象的属性;它不会绑定子类型的属性 @Martin - 子类型的属性无论如何都会丢失,因为该操作将Order 作为参数,不是吗? ModelBinder 将尝试根据它可以匹配的属性来理解 Order 对象。 【参考方案1】:

我之前尝试过做类似的事情,但我得出的结论是没有内置的东西可以处理这个问题。

我选择的选项是创建自己的模型绑定器(虽然继承自默认,所以它没有太多代码)。它查找一个名为 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")

【讨论】:

嗨,马丁!谢谢你的主意!您所描述的是我在DefaultModelBinder 所做的研究中看到的选项之一。我没有尝试过这种方式,但如果它是最好的,我会试一试!如果您能在这里放一些代码,我将非常感激! :) 有没有人能够在更复杂的场景中使用它?我已经成功地利用它来改变“var concreteType = Assembly.GetExecutingAssembly().GetType(concreteTypeValue.AttemptedValue);”来搜索所有加载的程序集,但我需要能够有一个父子视图模型,其中孩子是一个视图模型,其属性被更改为抽象类型......似乎不太喜欢这个......有什么提示吗? 查看我下面的附加答案,了解检查所有已加载程序集的版本。 您的解决方案不支持多重继承,既不支持集合也不支持泛型类。检查my solution,它适用于所有类型的模型。【参考方案2】:

您可以创建一个自定义 ModelBinder,该模型绑定器在您的操作接受特定类型时运行,并且它可以创建您想要返回的任何类型的对象。 CreateModel() 方法采用 ControllerContext 和 ModelBindingContext ,让您可以访问通过路由、url 查询字符串和 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
    

通过将其添加到 Global.asax.cs 中的 Application_Start() 中,将其设置为在您的操作中有 Order 参数时使用:

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

如您所见,您必须添加字段(名称为 Type),其中包含应创建从抽象类继承的具体类的信息。例如类:class abstract Contentclass TextContent,Content 的 Type 应该设置为“TextContent”。 记得在 global.asax 中切换默认模型绑定器:

protected void Application_Start()

    ModelBinders.Binders.DefaultBinder = new EnhancedModelBinder();
    [...]

更多信息和示例项目请查看link。

【讨论】:

【参考方案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;
                
            

这是一个简单的实现,它检查每个程序集的类型。我确信有更聪明的方法可以做到这一点,但这已经足够好了。

【讨论】:

嗨@Corey!感谢您的提示,但正如您在我对问题的编辑中看到的那样,我已经这样做了。这是一种更简洁的方式来编写您提出的内容。具有此代码并且我提出此问题的软件已经完美运行了将近一年:) 感谢您的关注!

以上是关于ASP.NET MVC 3:具有继承/多态性的 DefaultModelBinder的主要内容,如果未能解决你的问题,请参考以下文章

C#面试题

在具有 3 层架构的 ASP.NET MVC 应用程序中验证业务规则的更好方法是啥?

从 MVC 迁移到 ASP.NET Core 3.1 中的端点路由时,具有角色的 AuthorizeAttribute 不起作用

ASP.NET MVC 架构:ViewModel 通过组合、继承还是复制?

用于接口或继承类的 ASP.NET MVC3 编辑器模板

Asp.Net MVC过滤器