17

首先,很抱歉这篇大文章(我已经尝试过先做一些研究)以及针对同一问题的技术组合(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) 来创建订单,并且该视图为每个继承的订单(在本例中OrderBottlingOrderFinishingCreate()为 GET 请求定义了一个方法,为OrderController类上的 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 MVCDefaultModelBinder并使用 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: Cannot create an abstract class。所以,我认为活页夹不是或无法解析为正确的类型。

非常感谢提前!

编辑:首先,感谢 Martin 和 Jason 的回答,并对延误表示歉意!我尝试了这两种方法,都奏效了!我将 Martin 的答案标记为正确,因为它更灵活并且可以满足我项目的一些需求。具体来说,每个请求的 ID 都存储在数据库中,如果我只在一个地方(数据库或班级)更改 ID,将它们放在班级上可能会破坏软件。在这一点上,马丁的方法非常灵活。

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

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

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

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

4

5 回答 5

18

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

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

您可以创建一个自定义 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());
于 2011-03-29T01:11:44.633 回答
5

您还可以构建适用于所有抽象模型的通用 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);
    }
}
于 2011-09-22T15:48:03.977 回答
2

我对该问题的解决方案支持可以包含其他抽象类、多重继承、集合或泛型类的复杂模型。

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),其中包含应创建从抽象类继承的具体类的信息。例如类:抽象内容类,文本内容,内容应将类型设置为“文本内容”。请记住在 global.asax 中切换默认模型绑定器:

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

有关更多信息和示例项目,请查看以下链接

于 2012-05-11T10:35:35.160 回答
0

换行:

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

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

于 2012-02-21T03:21:14.427 回答