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

ASP.NET MVC Core 2:主表单内嵌动态表单实现求助(初学者)

Building Nested Dynamic Forms in ASP.NET Core MVC for Your Test App

Hey there! I totally get where you're coming from—building dynamic forms can feel tricky when you're starting out, but this is a super common pattern once you know the steps. Let's walk through how to implement a main Test form that lets you add/remove Questions and their associated Answers dynamically.

First, Confirm Your Model Relationships

First, let's make sure your entity models are set up correctly for nested collections. Assuming you already have something like this (if not, adjust to match your existing setup):

public class Test
{
    public int Id { get; set; }
    public string Title { get; set; }
    public ICollection<Question> Questions { get; set; } = new List<Question>();
}

public class Question
{
    public int Id { get; set; }
    public string Text { get; set; }
    public int TestId { get; set; }
    public Test Test { get; set; }
    public ICollection<Answer> Answers { get; set; } = new List<Answer>();
}

public class Answer
{
    public int Id { get; set; }
    public string Text { get; set; }
    public bool IsCorrect { get; set; }
    public int QuestionId { get; set; }
    public Question Question { get; set; }
}

Use ViewModels for Better Form Binding

Instead of using your entity models directly in views, create ViewModels to simplify form handling. These will help ASP.NET Core's model binder correctly map the dynamic fields:

public class TestCreateViewModel
{
    public string Title { get; set; }
    public List<QuestionViewModel> Questions { get; set; } = new List<QuestionViewModel>();
}

public class QuestionViewModel
{
    public string Text { get; set; }
    public List<AnswerViewModel> Answers { get; set; } = new List<AnswerViewModel>();
}

public class AnswerViewModel
{
    public string Text { get; set; }
    public bool IsCorrect { get; set; }
}

Controller Setup

GET Action (Load the Empty Form)

Initialize the view model with at least one question and answer so the user has a starting point:

public IActionResult Create()
{
    var viewModel = new TestCreateViewModel();
    // Add an initial question with an initial answer to kick things off
    viewModel.Questions.Add(new QuestionViewModel
    {
        Answers = new List<AnswerViewModel> { new AnswerViewModel() }
    });
    return View(viewModel);
}

POST Action (Handle Form Submission)

Map the ViewModel back to your entity models and save to the database:

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(TestCreateViewModel viewModel)
{
    if (ModelState.IsValid)
    {
        // Map ViewModel data to your database entities
        var test = new Test
        {
            Title = viewModel.Title,
            Questions = viewModel.Questions.Select(q => new Question
            {
                Text = q.Text,
                Answers = q.Answers.Select(a => new Answer
                {
                    Text = a.Text,
                    IsCorrect = a.IsCorrect
                }).ToList()
            }).ToList()
        };

        _context.Tests.Add(test);
        await _context.SaveChangesAsync();
        return RedirectToAction(nameof(Index));
    }

    // If validation fails, return the view with the submitted data to fix errors
    return View(viewModel);
}

Build the View with Dynamic Fields

Main View (Create.cshtml)

Use partial views for questions and answers, and add buttons to dynamically add new fields. We'll use JavaScript to clone these partials and update the form field names for proper model binding:

@model TestCreateViewModel

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

    <h4>Questions</h4>
    <div id="questions-container">
        @foreach (var (question, index) in Model.Questions.Select((q, i) => (q, i)))
        {
            await Html.RenderPartialAsync("_QuestionPartial", question, new ViewDataDictionary(ViewData) { {"QuestionIndex", index} });
        }
    </div>
    <button type="button" id="add-question" class="btn btn-secondary mt-2">Add Question</button>

    <div class="form-group mt-4">
        <input type="submit" value="Create Test" class="btn btn-primary" />
    </div>
</form>

<!-- Hidden template for new questions (we'll clone this with JS) -->
<div id="question-template" style="display:none;">
    @await Html.RenderPartialAsync("_QuestionPartial", new QuestionViewModel(), new ViewDataDictionary(ViewData) { {"QuestionIndex", 0} })
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
    <script>
        let questionIndex = @Model.Questions.Count;

        // Add new question to the form
        document.getElementById('add-question').addEventListener('click', function() {
            const template = document.getElementById('question-template').innerHTML;
            // Replace the placeholder index with the current question count
            const newQuestionHtml = template.replace(/\[0\]/g, `[${questionIndex}]`);
            document.getElementById('questions-container').insertAdjacentHTML('beforeend', newQuestionHtml);
            
            // Attach click event to the new question's "Add Answer" button
            const newAnswerBtn = document.querySelector(`#question-${questionIndex} .add-answer`);
            newAnswerBtn.addEventListener('click', handleAddAnswer);
            
            questionIndex++;
        });

        // Add new answer to a specific question
        function handleAddAnswer(e) {
            const questionId = e.target.dataset.questionId;
            const answerContainer = document.getElementById(`answers-${questionId}`);
            const template = document.getElementById(`answer-template-${questionId}`).innerHTML;
            
            const answerCount = answerContainer.children.length;
            const newAnswerHtml = template.replace(/\[0\]/g, `[${answerCount}]`);
            answerContainer.insertAdjacentHTML('beforeend', newAnswerHtml);
        }

        // Attach event listeners to initial answer buttons
        document.querySelectorAll('.add-answer').forEach(btn => {
            btn.addEventListener('click', handleAddAnswer);
        });
    </script>
}

Question Partial (_QuestionPartial.cshtml)

This partial renders a single question and its answers, plus a button to add more answers:

@model QuestionViewModel

@{
    var questionIndex = ViewData["QuestionIndex"];
}

<div class="card mb-3" id="question-@questionIndex">
    <div class="card-body">
        <div class="form-group">
            <label>Question Text</label>
            <input asp-for="Text" name="Questions[@questionIndex].Text" class="form-control" />
        </div>

        <h6>Answers</h6>
        <div id="answers-@questionIndex">
            @foreach (var (answer, index) in Model.Answers.Select((a, i) => (a, i)))
            {
                await Html.RenderPartialAsync("_AnswerPartial", answer, new ViewDataDictionary(ViewData) { {"AnswerIndex", index}, {"QuestionIndex", questionIndex} });
            }
        </div>
        <button type="button" class="btn btn-sm btn-outline-secondary mt-2 add-answer" data-question-id="@questionIndex">Add Answer</button>
    </div>
</div>

<!-- Hidden answer template for this question -->
<div id="answer-template-@questionIndex" style="display:none;">
    @await Html.RenderPartialAsync("_AnswerPartial", new AnswerViewModel(), new ViewDataDictionary(ViewData) { {"AnswerIndex", 0}, {"QuestionIndex", questionIndex} })
</div>

Answer Partial (_AnswerPartial.cshtml)

This partial renders a single answer field with a checkbox for "Is Correct":

@model AnswerViewModel

@{
    var questionIndex = ViewData["QuestionIndex"];
    var answerIndex = ViewData["AnswerIndex"];
}

<div class="input-group mb-2">
    <input asp-for="Text" name="Questions[@questionIndex].Answers[@answerIndex].Text" class="form-control" placeholder="Answer text" />
    <div class="input-group-append">
        <div class="input-group-text">
            <input asp-for="IsCorrect" name="Questions[@questionIndex].Answers[@answerIndex].IsCorrect" type="checkbox" />
            <label asp-for="IsCorrect" class="ml-1">Correct</label>
        </div>
    </div>
</div>

Key Tips for Success

  • Model Binding: The form field names follow the pattern Questions[N].Answers[M].Property—this is crucial for ASP.NET Core to map the dynamic fields to your ViewModel correctly.
  • Validation: Don't forget to include the validation scripts partial (_ValidationScriptsPartial) to get client-side validation working for dynamic fields.
  • Initial Data: Always load at least one question/answer pair so users don't start with an empty form (it makes the UI more intuitive).

You've got this! This pattern works for most nested dynamic form scenarios in ASP.NET Core MVC. If you hit specific issues (like validation not triggering on new fields, or model binding errors), just double-check the field names or adjust the ViewModel mapping.

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.20 07:01:33