locked
How to post and add a new value to a collection RRS feed

  • Question

  • User-1390269482 posted

    Hi All,

    I am building a web site of recipes. Each recipe can have tags and comments. My current code does not post the tags or comments back to the page model after editing and existing recipe. Can someone point me in the right direction?

    Thanks

    The Data Model:

    public class Recipe
        {
            [System.ComponentModel.DataAnnotations.Required, StringLength(80)]
            public string Name { get; set; } = "";
            public Uri Url { get; set; } = new Uri("http://LocalHost:44306/Recipes");
            public int ID { get; set; }
            [Required]
            public CuisineType CuisineType { get; set; } = CuisineType.None;
            public ConcurrentBag<string> Tags { get; set; } = new ConcurrentBag<string>();
            public float Rating { get; set; } = 0.0F;
            public ConcurrentBag<string> Comments { get; set; } = new ConcurrentBag<string>();
        }

    The PageModel

     public class RecipeEditModel : PageModel
        {
            private readonly IRecipeData recipeData;
            private readonly IHtmlHelper htmlHelper;
    
            [BindProperty]
            public Recipe Recipe { get; set; }
            
            // This will be used in conjunction with the IHTMLHelper to provide a collection of cuisine types
            public IEnumerable<SelectListItem> CuisineType { get; private set; }
    
            public ConcurrentBag<string> Tags { get; set; }
    
            public RecipeEditModel(IRecipeData recipeData, IHtmlHelper htmlHelper)
            {
                this.recipeData = recipeData;
                this.htmlHelper = htmlHelper;
            }
    
            public IActionResult OnGet(int recipeId)
            {
                Recipe = recipeData.GetRecipeById(recipeId);
                if (Recipe == null)
                    return RedirectToPage("./NotFound");
    
                CuisineType = htmlHelper.GetEnumSelectList<CuisineType>();
    
                // by default ASP.Net renders the model (Recipe)
                return Page();
            }
    
            public IActionResult OnPost()
            {
                if (ModelState.IsValid)
                {
                    Recipe = recipeData.Update(Recipe);
                    recipeData.Commit();
                }
    
                CuisineType = htmlHelper.GetEnumSelectList<CuisineType>();
                return Page();
            }
        }

    CSHTML

    @page "{recipeId:int}"
    @model OdeToFood.Pages.Recipes.RecipeEditModel
    @{
        ViewData["Title"] = "Edit Recipe";
    }
    
    <h2>Editing @Model.Recipe.Name</h2>
    
    <!--
        Create a form to edit data    
    -->
    <form method="post">
        <!--Include all recipe data necessary in order to update recipe-->
        <!--Recipe ID should be hidden so it can't be edited-->
        <!--asp.for sets the parameter "recipeId" in the model and sets its value-->
        <input type="hidden" asp-for="Recipe.ID" />
    
        <!--Set up the editable form elements-->
        <div class="form-group">
            <label asp-for="Recipe.Name">Recipe Name</label>
            <input asp-for="Recipe.Name" class="form-control" />
            <span class="text-danger" asp-validation-for="Recipe.Name"></span>
        </div>
        <!--
            Cuisine Type leverages the IHtmlHelper in the page model and the asp-items here to create the select list of cuisine type
            Do not want a text box here that would allow the user to enter random cuisine types
            Note: The asp-for taghelper makes sure the selected item is the same as the recipe cuisine type
        -->
        <div class="form-group">
            <label asp-for="Recipe.CuisineType">Cuisine Type</label>
            <select asp-for="Recipe.CuisineType" asp-items="Model.CuisineType" class="form-control"></select>
            <span class="text-danger" asp-validation-for="Recipe.CuisineType" ></span>
        </div>
        <div class="form-group">
            <label asp-for="Recipe.Rating">Rating</label>
            <input asp-for="Recipe.Rating" class="form-control" />
        </div>
        <div class="form-group">
            <label asp-for="Recipe.Tags">Tags</label>
            <table asp-for="Model.Recipe.Tags">
                <tr>
                    @foreach (var tag in Model.Recipe.Tags)
                    {
                        <td>
                            <input class="btn btn-sm" asp-for="@tag" />
                        </td>
                    }
                </tr>
                <tr>
                    <td>
                        <input class="form-control" asp-for="Recipe.Tags" placeholder="Add Tags" />
                    </td>
                </tr>
            </table>
        </div>
        <div class="form-group">
            <label asp-for="Recipe.Comments">Comments</label>
            <table width="100%">
                @foreach (var c in Model.Recipe.Comments)
                {
                    <tr  >
                        <td>
                            <input class="form-control" value="@c" />
                        </td>
                    </tr>
                }
                <tr>
                    <td >
                        <input class="form-control" placeholder="Add Comment" />
                    </td>
                </tr>
            </table>
        </div>
    
    
    
        <button type="submit" class="btn btn-primary">Save</button>
    </form>
    

    Monday, December 14, 2020 9:45 PM

Answers

  • User1686398519 posted

    Hi mcmk,   

    1. @foreach (var tag in Model.Recipe.Tags)
      • You should use a for loop to generate the correct name for model binding.
    2. The default model binder supports most common .NET Core data types.
      1. You can modify ConcurrentBag<string> to List<string>.
      2. Default model binder limitations

    Below is the modified code, please refer to it.

    Model

    public List<string> Tags { get; set; }
    public List<string> Comments { get; set; }

    Page

        <div class="form-group">
            <label asp-for="Recipe.Tags">Tags</label>
            <table>
                <tr>
                    @for (int i = 0; i < Model.Recipe.Tags.Count; i++)
                    {
                        <td>
                            <input class="btn btn-sm" asp-for="@Model.Recipe.Tags[i]" />
                        </td>
                    }
                </tr>
                <tr>
                    <td>
                        <input class="form-control" placeholder="Add Tags" name="Recipe.Tags[@Model.Recipe.Tags.Count]" />
                    </td>
                </tr>
            </table>
        </div>
        <div class="form-group">
            <label asp-for="Recipe.Comments">Comments</label>
            <table width="100%">
                @for (int i = 0; i < Model.Recipe.Comments.Count; i++)
                {
                    <tr>
                        <td>
                            <input class="btn btn-sm" asp-for="@Model.Recipe.Comments[i]" />
                        </td>
                    </tr>
                }
                <tr>
                    <td>
                        <input class="form-control" placeholder="Add Comment" name="Recipe.Comments[@Model.Recipe.Comments.Count]" />
                    </td>
                </tr>
            </table>
        </div>

    Here is the result. 

    Best Regards,

    YihuiSun

    • Marked as answer by Anonymous Thursday, October 7, 2021 12:00 AM
    Tuesday, December 15, 2020 7:49 AM

All replies

  • User475983607 posted

    Model binding to a collection requires the input names to contain an index.

    https://www.learnrazorpages.com/razor-pages/model-binding#binding-simple-collections

    Monday, December 14, 2020 10:01 PM
  • User1686398519 posted

    Hi mcmk,   

    1. @foreach (var tag in Model.Recipe.Tags)
      • You should use a for loop to generate the correct name for model binding.
    2. The default model binder supports most common .NET Core data types.
      1. You can modify ConcurrentBag<string> to List<string>.
      2. Default model binder limitations

    Below is the modified code, please refer to it.

    Model

    public List<string> Tags { get; set; }
    public List<string> Comments { get; set; }

    Page

        <div class="form-group">
            <label asp-for="Recipe.Tags">Tags</label>
            <table>
                <tr>
                    @for (int i = 0; i < Model.Recipe.Tags.Count; i++)
                    {
                        <td>
                            <input class="btn btn-sm" asp-for="@Model.Recipe.Tags[i]" />
                        </td>
                    }
                </tr>
                <tr>
                    <td>
                        <input class="form-control" placeholder="Add Tags" name="Recipe.Tags[@Model.Recipe.Tags.Count]" />
                    </td>
                </tr>
            </table>
        </div>
        <div class="form-group">
            <label asp-for="Recipe.Comments">Comments</label>
            <table width="100%">
                @for (int i = 0; i < Model.Recipe.Comments.Count; i++)
                {
                    <tr>
                        <td>
                            <input class="btn btn-sm" asp-for="@Model.Recipe.Comments[i]" />
                        </td>
                    </tr>
                }
                <tr>
                    <td>
                        <input class="form-control" placeholder="Add Comment" name="Recipe.Comments[@Model.Recipe.Comments.Count]" />
                    </td>
                </tr>
            </table>
        </div>

    Here is the result. 

    Best Regards,

    YihuiSun

    • Marked as answer by Anonymous Thursday, October 7, 2021 12:00 AM
    Tuesday, December 15, 2020 7:49 AM
  • User-1390269482 posted

    YihuiSun,

    Thanks very much for this. It works like a champ. 

    Thursday, December 17, 2020 3:58 PM