You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ASP.NET Core动态类型模型绑定:多类型内容编辑器开发技术问询

Alright, let's walk through how to build this dynamic content editor with ASP.NET Core's model binding. I've worked on similar projects where we needed support for multiple custom widget types, so here's a practical, step-by-step approach that should fit your needs:

Core Setup: Base Model & Marker Interface

First, let's solidify your base model structure—adding a WidgetType property is critical for telling our binder which concrete model to use:

// Your existing base abstract model
public abstract class WidgetModel
{
    // This will let us identify which widget type we're dealing with
    public string WidgetType { get; set; } 
}

// Marker interface for consistency (helps with model binder targeting)
public interface IWidgetModel { }

// Your example concrete widget model
public class ImageWithCopyWidgetModel : WidgetModel, IWidgetModel 
{ 
    public string ImageUrl { get; set; } 
    public string ImageAltText { get; set; } 
    public string HeaderText { get; set; } 
    public string BodyContent { get; set; } 
}
Step 1: Build a Custom Model Binder

ASP.NET Core's default binding needs to know the exact type upfront, so we'll create a custom binder that resolves the correct model based on the WidgetType value from the request:

public class WidgetModelBinder : IModelBinder
{
    private readonly IModelBinderFactory _modelBinderFactory;
    private readonly ITypeActivatorCache _typeActivatorCache;

    public WidgetModelBinder(IModelBinderFactory modelBinderFactory, ITypeActivatorCache typeActivatorCache)
    {
        _modelBinderFactory = modelBinderFactory;
        _typeActivatorCache = typeActivatorCache;
    }

    public async Task BindModelAsync(ModelBindingContext bindingContext)
    {
        // First, pull the WidgetType from the request data
        var widgetTypeValue = bindingContext.ValueProvider.GetValue("WidgetType").FirstValue;
        if (string.IsNullOrEmpty(widgetTypeValue))
        {
            bindingContext.ModelState.TryAddModelError(bindingContext.ModelName, "Widget type is required");
            return;
        }

        // Map the widget type string to its concrete class
        var widgetType = ResolveWidgetType(widgetTypeValue);
        if (widgetType == null)
        {
            bindingContext.ModelState.TryAddModelError(bindingContext.ModelName, $"Unknown widget type: {widgetTypeValue}");
            return;
        }

        // Create a new binding context for the specific widget type
        var newBindingContext = DefaultModelBindingContext.CreateBindingContext(
            bindingContext.ActionContext,
            bindingContext.ValueProvider,
            bindingContext.MetadataProvider.GetMetadataForType(widgetType),
            null,
            bindingContext.ModelName);

        // Get the default binder for this concrete type and run it
        var modelBinder = _modelBinderFactory.CreateBinder(newBindingContext);
        await modelBinder.BindModelAsync(newBindingContext);

        // Pass the bound model back to the original context
        if (newBindingContext.Result.IsModelSet)
        {
            bindingContext.Result = newBindingContext.Result;
        }
        else
        {
            bindingContext.ModelState.TryAddModelError(bindingContext.ModelName, "Failed to bind widget model");
        }
    }

    // Helper to map widget type names to their classes (replace with a registry later if needed)
    private Type ResolveWidgetType(string widgetTypeName)
    {
        var typeMap = new Dictionary<string, Type>
        {
            { "ImageWithCopy", typeof(ImageWithCopyWidgetModel) }
            // Add new widget types here as you create them
        };

        typeMap.TryGetValue(widgetTypeName, out var type);
        return type;
    }
}
Step 2: Register the Binder with ASP.NET Core

We need to tell the framework to use our binder for any IWidgetModel types. Create a provider and register it in your startup code:

// Provider class to hook our binder into the pipeline
public class WidgetModelBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context.Metadata.ModelType.IsAssignableTo(typeof(IWidgetModel)))
        {
            var modelBinderFactory = context.Services.GetRequiredService<IModelBinderFactory>();
            var typeActivatorCache = context.Services.GetRequiredService<ITypeActivatorCache>();
            return new WidgetModelBinder(modelBinderFactory, typeActivatorCache);
        }

        return null;
    }
}

// Register in Program.cs (or Startup.cs for older versions)
builder.Services.AddControllersWithViews(options =>
{
    // Insert at the top to ensure it takes priority over default binders
    options.ModelBinderProviders.Insert(0, new WidgetModelBinderProvider());
});
Step 3: Work with Your Edit ViewModel

Let's assume your EditContentViewModel holds a collection of widgets. The binder will automatically resolve each widget to its concrete type:

public class EditContentViewModel
{
    public string PageTitle { get; set; }
    public List<IWidgetModel> Widgets { get; set; } = new List<IWidgetModel>();
}

// In your controller
[HttpPost]
public IActionResult SaveContent(EditContentViewModel viewModel)
{
    // viewModel.Widgets will contain correctly bound instances like ImageWithCopyWidgetModel
    foreach (var widget in viewModel.Widgets)
    {
        if (widget is ImageWithCopyWidgetModel imageWidget)
        {
            // Access imageWidget.ImageUrl, etc.
        }
    }

    // Save to database, redirect, etc.
    return RedirectToAction("Index");
}
Step 4: Render Dynamic Editor Views

On the frontend, use partial views to render the correct form fields for each widget type. In your main edit view:

@model EditContentViewModel

<form asp-action="SaveContent" method="post">
    <div class="form-group">
        <label asp-for="PageTitle"></label>
        <input asp-for="PageTitle" class="form-control" />
    </div>

    <div id="widgets-container">
        @foreach (var widget in Model.Widgets)
        {
            <div class="widget-card">
                <input type="hidden" asp-for="@widget.WidgetType" />
                @await Html.PartialAsync($"Widgets/Editor_{widget.WidgetType}", widget)
            </div>
        }
    </div>

    <button type="button" id="add-image-widget" class="btn btn-secondary">Add Image with Copy</button>
    <button type="submit" class="btn btn-primary">Save Content</button>
</form>

Then create a partial view for your ImageWithCopy widget (Views/Shared/Widgets/Editor_ImageWithCopy.cshtml):

@model ImageWithCopyWidgetModel

<div class="form-group">
    <label asp-for="ImageUrl"></label>
    <input asp-for="ImageUrl" class="form-control" />
    <span asp-validation-for="ImageUrl" class="text-danger"></span>
</div>

<div class="form-group">
    <label asp-for="ImageAltText"></label>
    <input asp-for="ImageAltText" class="form-control" />
</div>

<div class="form-group">
    <label asp-for="HeaderText"></label>
    <input asp-for="HeaderText" class="form-control" />
</div>

<div class="form-group">
    <label asp-for="BodyContent"></label>
    <textarea asp-for="BodyContent" class="form-control"></textarea>
</div>
Bonus: Add Widgets Dynamically

To let users add new widgets on the fly, add a JavaScript handler that fetches the partial view for the desired widget type:

document.getElementById('add-image-widget').addEventListener('click', async () => {
    const response = await fetch('/Widgets/GetEditorPartial?widgetType=ImageWithCopy');
    const html = await response.text();
    document.getElementById('widgets-container').insertAdjacentHTML('beforeend', html);
});

And the corresponding controller action:

public class WidgetsController : Controller
{
    private readonly ITypeActivatorCache _typeActivatorCache;

    public WidgetsController(ITypeActivatorCache typeActivatorCache)
    {
        _typeActivatorCache = typeActivatorCache;
    }

    [HttpGet]
    public IActionResult GetEditorPartial(string widgetType)
    {
        var modelType = ResolveWidgetType(widgetType); // Reuse the ResolveWidgetType method from the binder
        var model = (IWidgetModel)_typeActivatorCache.CreateInstance<object>(HttpContext.RequestServices, modelType);
        model.WidgetType = widgetType;
        
        return PartialView($"Widgets/Editor_{widgetType}", model);
    }

    private Type ResolveWidgetType(string widgetTypeName)
    {
        var typeMap = new Dictionary<string, Type>
        {
            { "ImageWithCopy", typeof(ImageWithCopyWidgetModel) }
        };

        typeMap.TryGetValue(widgetTypeName, out var type);
        return type;
    }
}
Key Notes for Production
  • Validation: Add data annotations to your widget models (like [Required])—the custom binder will automatically pick up validation errors.
  • Type Registry: Replace the hardcoded typeMap with a dynamic registry (use reflection to scan assemblies for IWidgetModel types on startup, or register them via DI).
  • Persistence: For storing widgets in a database, use EF Core's polymorphic mapping (TPH or TPT) to handle different widget types in a single table or separate tables.

内容的提问来源于stack exchange,提问作者Matt Millican

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.25 08:11:22