Search code examples
c#ajaxasp.net-core-mvcmodel-bindingmvc-editor-templates

Ajax call is not performed inside a nested editor templates in MVC


I'm trying to create a web app Admin tool for creating/editing quizzes, that can be fully adjustable (with ASP.NET Core MVC).

What I mean by fully adjustable:

  1. Admin user can add/remove any number of questions per quiz.
  2. Admin user can add/remove any numbers on answers per question.

The problem is that inside the QuizViewModel class, I have a collection of Questions, and each Question has a collection of Answers.

I've already created 2 buttons for adding and removing Questions, by using EditorTemplates and by performing a Ajax call to my controller method, which is adding/removing objects from Questions collection. Everything is working very well with all the bindings.

But there is the problem when I try to crate the same logic for add/remove Answer per each Question. I'm trying to perform the same thing as I did with Questions by assigning each answer's section div it's unique id, which is

string answersContainerId = $"answers-container-{Model.QuestionNumber}";

And do the same for Add/Remove Answer buttons.

Here is the simplified structure of my classes:

public class QuizViewModel
{
    public string QuizName { get; set; }
    public List<QuizQuestion> Questions { get; set; }

    public QuizViewModel()
    {
        this.Questions = new List<QuizQuestion>
        {
            new QuizQuestion() // First must-have question
            {
                QuestionNumber = 1
            }
        };
    }
}
public class QuizQuestion
{   
    public string QuestionName { get; set; }
    public int Duration { get; set; } // Question duration (sec)
    public int QuestionNumber { get; set; }
    public List<QuizAnswer> Answers { get; set; } // List of answers per 1 question

    public QuizQuestion()
    {
        Duration = 30; // default
        Answers = new List<QuizAnswer>() // When creating a new empty question, by default we create 3 answers for it
        {
            new QuizAnswer{ AnswerNumber = 1 },
            new QuizAnswer{ AnswerNumber = 2 },
            new QuizAnswer{ AnswerNumber = 3}
        };
    }
}
public class QuizAnswer
{
    public string AnswerName { get; set; }
    public bool IsCorrect { get; set; }
    public int AnswerNumber { get; set; }
}

Here is Views>Quiz>CreateQuiz.cshtml view (simplified):

@model QuizViewModel

@using (Html.BeginForm("CreateQuiz", "Quiz", FormMethod.Post, new { id = "form-create-new-quiz" }))
{
    @Html.AntiForgeryToken()

    <h3>Quiz Name:</h3>
    <div class="form-group">
        @Html.LabelFor(model => model.QuizName,"Quiz Name:", htmlAttributes: new { @class = "control-label" })
        @Html.TextBoxFor(model => model.QuizName, new { @class = "form-control"})
        @Html.ValidationMessageFor(model => model.QuizName, "", new { @class = "text-danger" })
    </div>
    
    <div>
        <h1>Quiz Questions:</h1>
        <div id="questions-container">
            @Html.EditorFor(model => model.Questions)
        </div>
    </div>
    
    <div>
        <button id="btn-add-new-question" type="button" class="btn btn-success">Add new question</button>
        <button id="btn-remove-quiz-question" type="button" class="btn btn-danger">Remove Question</button>
    
        <input type="submit" value="Submit Quiz" class="btn btn-primary" />
    </div>

}

@section Scripts 
{
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
    <script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>

    <script type="text/javascript">
        $("#btn-add-new-question").on('click', function () {
            $.ajax({
                async: true,
                data: $('#form-create-new-quiz').serialize(),
                type: "POST",
                url: "/Quiz/AddQuizQuestion",
                success: function (partialView) {
                    console.log("partialView: " + partialView);
                    $('#questions-container').html(partialView);
                }
            });
        });
    </script>
    
    <script type="text/javascript">
        $("#btn-remove-quiz-question").on('click', function () {
            $.ajax({
                async: true,
                data: $('#form-create-new-quiz').serialize(),
                type: "POST",
                url: "/Quiz/RemoveQuizQuestion",
                success: function (partialView) {
                    console.log("partialView: " + partialView);
                    $('#questions-container').html(partialView);
                }
            });
        });
    </script>
}

I have an editor template .cshtml file for Question and Answer classes inside EditorTemplates folder:

  1. Views>Shared>EditorTemplates>QuizQuestion.cshtml file:
@model QuizQuestion

@{
    string currentQuestionAddAnswerButtonId = $"btn-add-new-answer-to-question-{Model.QuestionNumber}";
    string currentQuestionRemoveAnswerButtonId = $"btn-remove-answer-from-question-{Model.QuestionNumber}";
    string answersContainerId = $"answers-container-{Model.QuestionNumber}";
}

<div class="form-group">
    <div>
        @Html.LabelFor(question => question.QuestionName,"Question Name:", htmlAttributes: new { @class = "control-label" })
        @Html.TextBoxFor(question => question.QuestionName, new { @class = "form-control"})
        @Html.ValidationMessageFor(question => question.QuestionName, "", new { @class = "text-danger" })
    </div>

    <div id="@{@answersContainerId}">
        @Html.EditorFor(question => question.Answers)
    </div>

    <button id="@{@currentQuestionAddAnswerButtonId}" type="button" class="btn btn-success">Add New Answer</button>
    <button id="@{@currentQuestionRemoveAnswerButtonId}" type="button" class="btn btn-danger">Remove Answer</button>

</div>

@section Scripts
{
    <script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">

    <script type="text/javascript">
        $("#@{@currentQuestionAddAnswerButtonId}").on('click', function () {
            $.ajax({
                async: true,
                data: $('#form-create-new-quiz').serialize(),
                type: "POST",
                url: "/Quiz/AddNewAnswerToQuestion",
                success: function (partialView) {
                    console.log("partialView: " + partialView);
                    $('#@{@answersContainerId}').html(partialView);
                }
            });
        });
    </script>

    <script type="text/javascript">
        $("#@{@currentQuestionRemoveAnswerButtonId}").on('click', function () {
            $.ajax({
                async: true,
                data: $('#form-create-new-quiz').serialize(),
                type: "POST",
                url: "/Quiz/RemoveAnswerFromQuestion",
                success: function (partialView) {
                    console.log("partialView: " + partialView);
                    $('#@{@answersContainerId}').html(partialView);
                }
            });
        });
    </script>
}
  1. Views>Shared>EditorTemplates>QuizAnswer.cshtml file:
@model QuizAnswer

<div>
    <h2><strong>Answer @Html.DisplayFor(answer => answer.AnswerNumber)</strong></h2>
    @Html.HiddenFor(answer => answer.AnswerNumber)

    <div class="form-group">
        <div>
            @Html.LabelFor(answer => answer.AnswerName,"Answer:", htmlAttributes: new { @class = "control-label" })
            @Html.TextBoxFor(answer => answer.AnswerName, new { @class = "form-control"})
            @Html.ValidationMessageFor(answer => answer.AnswerName, "", new { @class = "text-danger" })
        </div>

        <div class="form-check">
            @Html.CheckBoxFor(answer => answer.IsCorrect, new { @class = "form-check-input"})
            @Html.LabelFor(answer => answer.IsCorrect, "Is Correct", new { @class = "form-check-label"})
        </div>
    </div>
</div>

Respectively, I do have 2 partial views for the editor templates:

  1. Views>Quiz>QuizQuestions.cshtml:
@model QuizViewModel
@Html.EditorFor(model => model.Questions)
  1. Views>Quiz>QuizAnswers.cshtml:
@model QuizQuestion
@Html.EditorFor(model => model.Answers)

And finally, here is my QuizController class:

public class QuizController : Controller
{
    public IActionResult CreateQuiz()
    {
        return View(new QuizViewModel());
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> AddQuizQuestion([Bind("Questions")] QuizViewModel quizViewModel)
    {
        QuizQuestion newQuizQuestion = new QuizQuestion
        {
            QuestionNumber = quizViewModel.Questions.Count + 1
        };

        quizViewModel.Questions.Add(newQuizQuestion);
        return PartialView("QuizQuestions", quizViewModel);
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> RemoveQuizQuestion([Bind("Questions")] QuizViewModel quizViewModel)
    {
        if (quizViewModel.Questions.Count is not 1)
        {
            quizViewModel.Questions.RemoveAt(quizViewModel.Questions.Count - 1);
        }

        return PartialView("QuizQuestions", quizViewModel);
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> AddNewAnswerToQuestion([Bind("Questions")] QuizViewModel quizViewModel)
    {
        // Define to which question to add answer
        var quizQuestion = quizViewModel.Questions.FirstOrDefault();
        return PartialView("QuizAnswers", quizQuestion);
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> RemoveAnswerFromQuestion([Bind("Questions")] QuizViewModel quizViewModel)
    {
        // Define from which question to remove answer
        var quizQuestion = quizViewModel.Questions.FirstOrDefault();
        return PartialView("QuizAnswers", quizQuestion);
    }
}

So in the end, when I press the unique Add/Remove Answer buttons, my Controller methods AddNewAnswerToQuestion() and RemoveAnswerFromQuestion() are not called.

However, the same logic works for Add/Remove Question.

So it looks like the problem is in Nested classes I think.

Really hope that somebody can help, I'm stuck with this. If you need some more info to provide just let me know.

And this is my first ever question on Stack Overflow, so I do apologize if something is not well explained.


Solution

  • We can see that we don't have the <script> for the Views>Shared>EditorTemplates>QuizQuestion.cshtml, which means we don't have onclick event handler for the Add New Answer button.

    enter image description here

    This is because in the partial view, we use @section Scripts{ } to surround the scripts. Let's remove @section Scripts{ } in QuizQuestion.cshtml and see the result.

    enter image description here

    I stopped the troubleshooting after hitting the controller method for the add answer button...