简体   繁体   中英

ASP.Net Core MVC model property is nullified if model validation fails

I'm working on an ASP.Net Core MVC app. I have a model Recipe which has two properties that are lists of other objects:

public class Recipe
    {
        public int Id { get; set; }

        [Required(ErrorMessage = "Title is required")]
        [StringLength(45, MinimumLength = 2)]
        [Display(Name = "Title", Prompt = "Title")]
        public string Title { get; set; }

        [Required(ErrorMessage = "Description is required")]
        [StringLength(500)]
        [Display(Name = "Description", Prompt = "Description")]
        public string Description { get; set; }

        [Required(ErrorMessage = "You must include at least one ingredient.")]
        public List<RecipeIngredient> Ingredients {get; set;}

        [Required(ErrorMessage = "You must include at least one instruction step.")]
        public List<Instruction> Directions { get; set; }

        public byte[] Picture { get; set; }
    }

RecipeIngredient.cs:

public class RecipeIngredient
    {
        public int Id { get; set; }

        public int IngredientID { get; set; }

        [Required]
        public string Measurement { get; set; }

        [Required]
        public string Ingredient { get; set; }
    }

Instruction.CS:

public class Instruction
    {
        [Required]
        public int Step { get; set; }

        [Required]
        [StringLength(200, MinimumLength = 2)]
        public string InstructionText { get; set; }
    }

Here is my view for creating a new recipe:

@model TheKitchen.Models.Recipe


@section Scripts {
    <script src="@Url.Content("~/js/CreateRecipe.js")" type="text/javascript"></script>

    <script>
        $(document).ready(function () {
            autocomplete();
        });
    </script>
        <script>
            $('#addNewIngredient').click(function () {
                addIngredient();
            });
        </script>
    <script>
        $('#addNewInstruction').click(function () {
            addInstruction();
        });
    </script>

}

@section Styles{
    <link rel="stylesheet" href="~/css/CreateRecipe.css" />
}

@{
    ViewData["Title"] = "New Recipe";
}

<h3>New Recipe</h3>

<!-- New recipe form -->


@using (Html.BeginForm("Create", "Recipes", FormMethod.Post))
{
    <div class="form-group">
        <input type="text" class="form-control" id="recipeName" asp-for="Title" placeholder="Recipe Name">
        <span asp-validation-for="Title" class="text-danger"></span>
    </div>
    <div class="form-group">
        <textarea class="form-control" id="recipeDescription" asp-for="Description" rows="3" placeholder="Description"></textarea>
        <span asp-validation-for="Description" class="text-danger"></span>
    </div>
    <div class="form-group addIngredients">
        <h4>Ingredients</h4>
        <div id="ingredientSearchbar">
            <input type="search" class="form-control" id="ingredientSearch" placeholder="Enter ingredient name here.">
            <input type="button" class="btn btn-outline-secondary" id="addNewIngredient" value="Add" />
        </div>
        <br />
        <table class="table" id="ingredientTable" name="ingredientTable">
            <tbody>
            </tbody>
        </table>
        <span asp-validation-for="Ingredients" class="text-danger"></span>
    </div>

    <div class="form-group addInstructions">
        <h4>Instructions</h4>
        <div id="instructionTextBar">
            <input type="text" class="form-control" id="instructionText" placeholder="Enter instruction step here, then click Add Step.">
            <input type="button" class="btn btn-outline-secondary" id="addNewInstruction" value="Add" />
        </div>
        <br />

        <!-- model.Instructions is getting returned null if form validation fails -->

        <table class="table" id="instructionTable" name="instructionTable">
            <tbody>
            </tbody>
        </table>
        <span asp-validation-for="Directions" class="text-danger"></span>
    </div>
    <button type="submit" class="btn btn-primary" id="newRecipeButton">Create Recipe</button>
}

And the jQuery driving the addition of RecipeIngredients and Instructions to their respective model lists and html tables for display:

/* Script for autocomplete in ingredient search box. */
function autocomplete() {
    $("#ingredientSearch").autocomplete({
        appendTo: ".addIngredients",
        source: function (request, response) {
            $.ajax({
                url: '/Recipe/IngredientSearch',
                type: 'GET',
                cache: false,
                data: request,
                dataType: 'json',
                success: function (data) {
                    response($.map(data, function (item) {
                        return {
                            label: item.label,
                            value: item.Value
                        }
                    }))
                }
            });
        }
    });
};

/* Script to add ingredient search bar text to ingredient list if not already present. */
function addIngredient() {
    var listIndex = $('.recipeIngredientRow').length;
    var measurement = '<input type="text" asp-for="Ingredients[' + listIndex + '].Measurement" name="Ingredients[' + listIndex +
        '].Measurement" class="measurementTextBox" id="measurementTextBox' + listIndex + '" placeholder="Enter measurement">';
    var ingredient = '<input type="text" class="noTextborder" asp-for="Ingredients[' + listIndex + '].Ingredient" name="Ingredients[' + listIndex +
        '].Ingredient" value="' + $('#ingredientSearch').val() + '" readonly>';
    var deleteButton = '<input type="button" class="cancel" value="X">';
    var newRow = '<tr class="recipeIngredientRow"><td>' + measurement + '</td><td>' + ingredient +
        '</td><td>' + deleteButton + '</td></tr>';

    var hasDuplicates = false;
    var hasEmptyMeasurements = false;
    $("#ingredientTable td").each(function () {
        var measurementContent = $(this).find('input').val().length;

        var ingredientContent = $(this).find('input').val() == $('#ingredientSearch').val();

        if (ingredientContent) {
            hasDuplicates = true;
            return;
        }
        if (measurementContent == 0) {
            hasEmptyMeasurements = true;
            return;
        }
    });

    if (hasDuplicates)
        alert('This ingredient is already on the ingredient list.');
    else if ($('#ingredientSearch').val().length == 0)
        alert("You can't add a blank ingredient.");
    else if (hasEmptyMeasurements)
        alert("You must provide a measurement before adding a new ingredient.");
    else
        $('#ingredientTable').append(newRow);
    $('#ingredientSearch').val("");
};

/* Script for removing ingredient table row when 'X' button is clicked. */
$('#ingredientTable').on('click', 'tr input.cancel', function () {
    $(this).closest('tr').remove();
});

/* Script for adding instruction step to instruction list */
function addInstruction() {
    var listIndex = $('.recipeInstructionRow').length;
    var stepNumber = '<input type="number" readonly="true" asp-for="Directions" name="Directions[' + listIndex +
        '].Step" class="noTextborder stepNumber" value="' + (listIndex + 1) + '">';
    var instruction = '<textarea asp-for="Directions" name="Directions[' + listIndex +
        '].InstructionText" id="Directions[' + listIndex + ']" class="noTextborder">' + $('#instructionText').val() + '</textarea>';
    var deleteButton = '<input type="button" class="cancel" value="X"';
    var newRow = '<tr class="recipeInstructionRow"><td class="ten">' + stepNumber + ' </td><td class="eighty">' + instruction + '</td><td class="ten">' + deleteButton + '</td></tr>';

    if ($('#instructionText').val().length == 0)
        alert("You can't add a blank instruction.");
    else
        $('#instructionTable').append(newRow);
    $('#instructionText').val("");
}

/* Script for removing instruction table row when 'X' button is clicked */
$('#instructionTable').on('click', 'tr input.cancel', function () {
    $(this).closest('tr').remove();

    $('#instructionTable tr').each(function () {
        var rowIndex = $(this).index() + 1;
        $('td:first-child', this).find('input').val(rowIndex);
    });
});

And finally, the controller method for this:

[Authorize]
        [HttpPost("Recipe/Create")]
        public IActionResult Create(Recipe recipe)
        {
            try
            {
                if (ModelState.IsValid)
                {
                    var userEmail = User.FindFirst(ClaimTypes.Email).Value;
                    var user = userData.GetUser(userEmail);

                    this.recipeData.CreateRecipe(recipe, user.UserId, DateTime.Now);
                    return RedirectToAction("Index");
                }
                else
                    return View("Create", recipe);
            }
            catch (MySqlException mySqlEx)
            {
                throw new Exception(mySqlEx.Message);
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex);
                return StatusCode(500, "Something went wrong. Please try again");
            }
        }

If you submit a valid form with all required fields, the model is passed to the controller and all is well.

However, if you submit an invalid form and you are returned to the View, the Recipe title and description are passed back to the form, but the two lists are made null (eg, this list property itself is null). I can't figure out for the life of me where or why the list properties would be made null in going from the View to the Controller and back to the View.

Any ideas?

Adding the following to the View:

@if (Model != null && Model.Ingredients != null)
  {
    @for (int i = 0; i < Model.Ingredients.Count; i++)
    {
      <tr>
        <td>@Html.EditorFor(model => model.Ingredients[i].Measurement)</td>
        <td>@Html.EditorFor(model => model.Ingredients[i].Ingredient, new { htmlAttributes = new { @class = "noTextborder", @readonly = "true" } })</td>
        <td><input type="button" class="cancel" value="X"></td>
      </tr>
    }
  }

will do the trick.

You must use EditorFor as DisplayFor only displays the contents plainly, so it is on your screen, but is unbound to the model. The html attributes keep things read-only if desired.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM