在 ASP.NET Core MVC 中运行时动态绑定模型

Ale*_*lex 5 c# asp.net asp.net-mvc asp.net-core

我正在使用 ASP.NET Core MVC 3.1 开发 Web 应用程序。我正在实现一个系统,人们可以在其中请求事物,然后进入通用的“请求”流程。因为我不想创建数十个 80% 相似的控制器和视图,所以我正在考虑动态绑定模型并使用部分视图来处理不同的事物。

为此,我想重写模型绑定器行为,以便它可以在运行时从正确的类型进行绑定。我找到了一些关于如何使用“经典”ASP.NET MVC 执行此操作的指南[1],但这似乎不适用于 ASP.NET Core MVC,因为这一切都已重新设计。

我发现这ComplexModelTypeBinder可能是我所需要的,但是继承和重写并没有让我走得更远,因为 BindingContext 上的很多属性现在都是只读的。

如何在 ASP.NET Core MVC 中实现相同的目标?

[1] ASP.NET MVC - 如何在运行时动态绑定模型

Joh*_*n H 8

我可以为您提供一个开始的起点。

本着与您链接的文章相同的精神,让我们定义一些与宠物相关的类型:

public interface IPet
{
    string Name { get; }
}

public class Cat : IPet
{
    public string Name => "Cat";
    public bool HasTail { get; set; }
}

public class Dog : IPet
{
    public string Name => "Dog";
    public bool HasTail { get; set; }
}

public class Fish : IPet
{
    public string Name => "Fish";
    public bool HasFins { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

在视图中,定义我们可以使用的以下形式:

<form asp-action="BindPet" method="post">
    <input type="hidden" name="PetType" value="Fish" />
    <input type="hidden" name="pet.HasTail" value="true" />
    <input type="hidden" name="pet.HasFins" value="true" />
    <input type="submit" />
</form>
Run Code Online (Sandbox Code Playgroud)

最后,一个简单的控制器操作,它将一个IPet实例作为参数:

public IActionResult BindPet(IPet pet)
{
    return RedirectToAction("Index");
}
Run Code Online (Sandbox Code Playgroud)

现在,创建一个像这样的多态绑定器有 3 个部分:

  1. 创建模型绑定器,实现IModelBinder
  2. 创建一个实现 的类型IModelBinderProvider,它将用于创建我们的实例IModelBinder
  3. 注册我们的IModelBinderProvider类型以便可以使用

我们的活页夹的实现可能如下所示(我添加了注释,因为它做了相当多的事情):

public class PetModelBinder : IModelBinder
{
    private readonly IDictionary<Type, (ModelMetadata, IModelBinder)> _binders;

    public PetModelBinder(IDictionary<Type, (ModelMetadata, IModelBinder)> binders) 
    {
        _binders = binders;
    }

    public async Task BindModelAsync(ModelBindingContext bindingContext)
    {
        // Read our expected type from a form,
        // and convert to its .NET type.
        var petType = bindingContext.ActionContext.HttpContext.Request.Form["PetType"];
        var actualType = TypeFrom(petType);

        // No point continuing if a valid type isn't found.
        if (actualType == null)
        {
            bindingContext.Result = ModelBindingResult.Failed();
            return;
        }

        // This will become clearer once we see how _binders
        // is populated in our IModelBinderProvider.
        var (modelMetadata, modelBinder) = _binders[actualType];

        // Create a new binding context, as we have provided
        // type information the framework didn't know was available.
        var newBindingContext = DefaultModelBindingContext.CreateBindingContext(
            bindingContext.ActionContext,
            bindingContext.ValueProvider,
            modelMetadata,
            bindingInfo: null,
            bindingContext.ModelName);

        // This tries to bind the actual model using our
        // context, setting its Result property to the bound model.
        await modelBinder.BindModelAsync(newBindingContext);
        bindingContext.Result = newBindingContext.Result;

        // Sets up model validation.
        if (newBindingContext.Result.IsModelSet)
        {
            bindingContext.ValidationState[newBindingContext.Result] = new ValidationStateEntry
            {
                Metadata = modelMetadata,
            };
        }
    }

    private static Type? TypeFrom(string name)
    {
        return name switch
        {
            "Cat" => typeof(Cat),
            "Dog" => typeof(Dog),
            "Fish" => typeof(Fish),
            _ => null
        };
    }
}
Run Code Online (Sandbox Code Playgroud)

接下来我们来实现一下IModelBinderProvider

public class PetModelBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context.Metadata.ModelType != typeof(IPet))
        {
            return null;
        }

        var pets = new[] { typeof(Cat), typeof(Dog), typeof(Fish) };

        var binders = new Dictionary<Type, (ModelMetadata, IModelBinder)>();
        foreach (var type in pets)
        {
            var modelMetadata = context.MetadataProvider.GetMetadataForType(type);
            binders[type] = (modelMetadata, context.CreateBinder(modelMetadata));
        }

        return new PetModelBinder(binders);
    }
}
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,这比活页夹本身要简单得多,而且只不过是一个华丽的工厂。它查询每个具体类型的元数据,还创建一个可以处理每个类型的绑定器,并将它们传递给我们的绑定器。

最后,在 中Startup,我们需要注册IModelBinderProvider以供使用:

services.AddControllersWithViews(options =>
{
    options.ModelBinderProviders.Insert(0, new PetModelBinderProvider());
});
Run Code Online (Sandbox Code Playgroud)

指示0模型绑定器具有的优先级。这确保我们的活页夹将首先被检查。如果我们不这样做,另一个绑定器将尝试绑定该类型,但会失败。

现在已经完成了,启动调试器,在我们创建的操作方法中放置一个断点,然后尝试提交表单。检查 的实例IPet,您应该看到HasFins为 设定的属性Fish。将元素编辑PetTypeDog,重复上述操作,您应该会看到HasTail已设置。

狗模型绑定 鱼模型绑定