ModelBinder用于模型身份的接口

时间:2010-12-26 03:13:12

标签: c# asp.net-mvc

我有一个IIdentifiable界面:

public interface IIdentifiable
{
    int Id { get; set; }
}

一个简单的课程Foo

public class Foo : IIdentifiable
{
    public int Id { get; set; }
    public string Name { get; set; }
}

当我有一个页面需要添加一组Foo并指定一个作为默认值时,我会有一个这样的视图模型:

public class BarViewModel
{
    public IList<Foo> Foos { get; set; }
    public Foo DefaultFoo { get; set; }
}

所以实际上我只想 传递Ids而不是隐藏输入中的实际完整对象(这只是讨厌且不需要)。所有关注的事情(至少在数据库中)都在Bar和它的Foo.Ids和默认的Foo.Id之间。

我希望能够轻松添加一个能够接受所有IIdentifiable的模型绑定器,然后设置Id,如果只为值提供者设置了int。我遇到的问题是我不能做类似下面的事情并让它设置Id(因为模型绑定器不看衍生类型链......呃):

ModelBinders.Binders[typeof(IIdentifiable)] = new IdentifiableModelBinder();

所以我决定扩展DefaultModelProvider以允许此功能,如果类型是IIdentifiable并且值提供程序中找到的值只是一个字符串/ int,那么创建模型并设置Id属性为匹配值:

public class DefaultWithIdentifiableModelBinder : DefaultModelBinder
{

    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {

        var modelType = bindingContext.ModelType;
        bool isList = false;

        // Determine the real type of the array or another generic type.
        if (modelType.IsArray)
        {
            modelType = modelType.GetElementType();
            isList = true;
        }
        else if (modelType.IsGenericType)
        {
            var genericType = modelType.GetGenericTypeDefinition();

            if (genericType == typeof(IEnumerable<>) || genericType == typeof(IList<>) || genericType == typeof(ICollection<>))
            {
                modelType = modelType.GetGenericArguments()[0];
                isList = true;
            }
        }

        // The real model type isn't identifiable so use the default binder.
        if (!typeof(IIdentifiable).IsAssignableFrom(modelType))
        {
            return base.BindModel(controllerContext, bindingContext);
        }

        // Get the value provider for the model name.
        var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);

        // Get the string values from the value provider.
        var stringValues = valueProviderResult != null ? (IEnumerable<string>)valueProviderResult.RawValue : Enumerable.Empty<string>();

        int tempFirstId = -1;

        // If the first element is an integer, we assume that only the Ids are supplied
        // and therefore we parse the list.
        // Otherwise, use the default binder.
        if (stringValues.Any() && int.TryParse(stringValues.First(), out tempFirstId))
        {

            var listType = typeof(List<>).MakeGenericType(new Type[] { modelType });

            var items = (IList)base.CreateModel(controllerContext, bindingContext, listType);

            // Create each identifiable object and set the Id.
            foreach (var id in stringValues)
            {
                var item = (IIdentifiable)Activator.CreateInstance(modelType);
                item.Id = int.Parse(id);
                items.Add(item);
            }

            if (items.Count == 0)
            {
                return null;
            }

            // Determine the correct result to return.
            if (bindingContext.ModelType.IsArray)
            {
                var array = Array.CreateInstance(modelType, items.Count);
                items.CopyTo(array, 0);
                return array;
            }
            else if (isList)
            {
                return items;
            }
            else
            {
                return items[0];
            }
        }
        else
        {
            return base.BindModel(controllerContext, bindingContext);
        }

    }

}

我只是不确定这对我正在尝试做的事情是否真的有必要。如果有人可以对这种类型的模型绑定留下反馈/建议/改进,那将非常感激。

编辑:这是一个简单的例子:

订单包含许多项目,因此,与ORM和数据库一起使用时,只需要确实,而不是加载项目的整个对象图表。因此,项目类加载了Id,然后该项目可以添加到订单的项目列表中:

public class Order
{
    List<Item> Items { get; set; }
}

var order = new Order();
order.Items.Add(new Item() { Id=2 });
order.Items.Add(new Item() { Id=5 });

这是因为完成订单的回发不会发送整个 Item,它只是发送ID。

这是我的核心要求。当发生回发时,我需要从回发中构建Id中的Items。无论这是一个视图模型还是实际的域模型,我仍然需要一种使用Id集轻松地从int转换到域模型的方法。有意义吗?

3 个答案:

答案 0 :(得分:2)

我遇到了与以下解决方案类似的问题。

首先,创建您的界面。在下面的示例中,其名称为 IInterface

然后,如下所示创建您的 CustomBinderProvider

public class IInterfaceBinderProvider : IModelBinderProvider
{
       public IModelBinder GetBinder(ModelBinderProviderContext context)
       {
           if (context.Metadata.ModelType.GetInterface(nameof(IInterface)) != null)
           {
               return new BinderTypeModelBinder(typeof(IInterface));
           }

           return null;
       }
}

现在,您可以按以下方式实现您的活页夹;

public Task BindModelAsync(ModelBindingContext bindingContext)
{
           // Do something

           var model = (IInterface)Activator.CreateInstance(bindingContext.ModelType);

           // Do something

           bindingContext.Model = model;
           bindingContext.Result = ModelBindingResult.Success(bindingContext.Model);
           return Task.FromResult(model);
} 

最后,您必须按如下所示将提供程序插入 Startup 类;

services.AddMvcCore(x =>
{
     x.ModelBinderProviders.Insert(0, new IInterfaceBinderProvider());
})

答案 1 :(得分:1)

这似乎是一种体面的方式,但不是很容易扩展。如果您不太可能在不同的界面上出现同样的情况,那么这是一个很好的解决方案。

另一种方法是在启动时使用反射来查找实现IIdentifiable的所有类型,并为所有类型分配自定义模型绑定器。

答案 2 :(得分:1)

好吧,假设你需要Foo及其数据。因此,您为其保存Name(可能与其他属性一起),并将持久性中的所有数据作为Foo实例检索,这正是您的域实体。它包含与Foo

相关的所有数据

另一方面,你有一个View,你唯一想要操作的是Id,所以你创建了包含Id作为属性的ViewModel(可能还有更多Ids,如ParentFooId例如)这是你的ViewModel 。它仅包含特定于View的数据 - 它类似于View和Controller之间的接口。

这样一切都是用DefaultModelBinder完成的。例如,如果您有:

  • 您的RouteData词典中的id(类型int)参数或通过表单发布;
  • BarViewModel实例作为控制器操作的参数;
  • {li> Id BarViewModel 的属性

然后根据请求,barViewModel.Id属性的值将是您的RouteData(或您的表单)中的值,因为DefaultModelBinder能够做到这一点。而且您只为非常不寻常的情况创建自定义IModelBinder

我只是没有理由让事情过于复杂。

有道理吗?