locked
[MVC] Model validation fails with datalist RRS feed

  • Question

  • User-1370514677 posted

    Hi everyone,

    I'm having an issue when I submit a HttpPost request on my Publish View through Model Validation.

    Indeed, I get an Error Message for the ForeignKey Category on my Article Model.

    I read the tutorial but couldn't find a solution : Model validation in ASP.NET Core MVC | Microsoft Docs

    Here is the code :

    Article.cs

    using System.ComponentModel.DataAnnotations;
    
    namespace DevAstuces.Models
    {
        public class Article
        {
            [Key]
            public int Id { get; set; }
            public virtual User Author { get; set; }
    
            [Required(ErrorMessage = "Merci de spécifier une catégorie")] // This Error message is triggered
            public virtual Category Category { get; set; }
    
            [Required(ErrorMessage = "Merci de spécifier un titre")]
            [Display(Name="Title", Prompt="Titre")]
            public string Title { get; set; }
    
            [Required(ErrorMessage = "Merci de spécifier une description")]
            [Display(Name="Description", Prompt="Description")]
            public string Description { get; set; }
    
            [Required(ErrorMessage = "Merci de saisir un article")]
            [Display(Name="Content", Prompt="Saisissez votre article")]
            public string Content { get; set; }
    
            public enum ArticleStatus
            {
                Pending,
                Rejected,
                Validated
            }
    
            public ArticleStatus Status { get; set; }
        }
    }

    Category.cs

    using System.ComponentModel.DataAnnotations;
    
    namespace DevAstuces.Models
    {
        public class Category
        {
            [Key]
            public int Id { get; set; }
            public byte[] Image { get; set; }
            
            [Required]
            public string Name { get; set; }
        }
    }

    ArticleViewModel.cs

    using System.Collections.Generic;
    using DevAstuces.Models;
    
    namespace DevAstuces.ViewModels
    {
        public class ArticleViewModel
        {
            public List<Category> Category { get; set; }
            public Article Article { get; set; }
        }
    }

    ArticleController.cs

            [HttpGet]
            public IActionResult Publish()
            {
                var ArticleViewModel = new ArticleViewModel();
                ArticleViewModel.Category = _articlecontext.Category.ToList();
                
                return View(ArticleViewModel);
            }
    
            [HttpPost]
            public IActionResult Publish(ArticleViewModel ArticleViewModel)
            {
                ArticleViewModel.Category = _articlecontext.Category.ToList();
    
                if(!ModelState.IsValid)
                    return View(ArticleViewModel);
    
                return RedirectToAction("Index","Home");
            }

    Publish.cshtml

    @model DevAstuces.ViewModels.ArticleViewModel
    
    @{
        Layout = "_Layout";
        ViewData["Title"] = "Publier";
    }
    
    @section page_styles{
        <link href="~/css/Article/publish.css" type="text/css" rel="stylesheet">
    }
    
    @section page_content{
        <h1>Publier</h1>
    
        <form id="editor_pane" method="POST">
            <input type="list" list="Category" placeholder="Catégorie" name="Category">
            <datalist id="Category">
            @foreach(var category in Model.Category)
            {
                <option value="@category.Name"></option>
            }
            </datalist>
            <span asp-validation-for="@Model.Article.Category" class="error_span"></span>
    
            <input asp-for="@Model.Article.Title">
            <span asp-validation-for="@Model.Article.Title" class="error_span"></span>
            
            <input asp-for="@Model.Article.Description">
            <span asp-validation-for="@Model.Article.Description" class="error_span"></span>
    
            <div id="editor_controls">
                <button type="button" onclick="previewArticle()">Prévisualiser</button>
                <input value="Valider" type="submit">
            </div>
        </form>
    
        <div id="preview_pane">
    
        </div>
    }
    
    @section page_scripts{
        <script src="~/js/Article/editor.js"></script>
    }

    The Error may be linked to the fact that the ArticleViewModel uses a List of Category and that it is not directly linked to the Article which is to be created ?

    Error in Article.cs :

            [Required(ErrorMessage = "Merci de spécifier une catégorie")] // This Error message is triggered
            public virtual Category Category { get; set; }

    Best Regards

    Saturday, February 20, 2021 4:19 PM

Answers

  • User1686398519 posted

    Hi valenciano8, 

    1. Even though the User does select a category, it acts like no categories were chosen.
      1. You can see the code you originally provided:
        1. When you add name="Category", the Category in ArticleViewModel is bound after the form is submitted.
        2. But you did not assign a value to Category in Article, so an error (Required) will be triggered.
      2. <input list="Category" placeholder="Catégorie" name="Category">
      3.  public class Article
         {
           public virtual Category Category { get; set; }  
         }
         public class ArticleViewModel
         {
           public List<Category> Category { get; set; }
         }
    2. Even if you modify it to asp-for="@Model.Article.Category" now, the Category in your Article is of the Category type, but when you use the datalist, you get a string type. After submitting the form, even if the name is the same, the string type will not be successfully bound to the Category type.
      • <input list="Category" asp-for="@Model.Article.Category">
    3. Therefore, I suggest you modify the Category in Article to string type.

    Model

    public class Article
    {
      [Required(ErrorMessage = "Merci de spécifier une catégorie")] // This Error message is triggered
      public string CategoryName { get; set; }
    }

    View

    <input list="Category" placeholder="Catégorie" asp-for="@Model.Article.CategoryName">
    <datalist id="Category">
       @foreach (var category in Model.Category)
       {
          <option value="@category.Name"></option>
        }
    </datalist>
    <span asp-validation-for="@Model.Article.CategoryName" class="error_span"></span>

    Best Regards,

    YihuiSun

    • Marked as answer by Anonymous Thursday, October 7, 2021 12:00 AM
    Monday, February 22, 2021 7:15 AM

All replies

  • User-1370514677 posted

    EDIT :

    To be more precise, the issue is here :

            <input list="Category" placeholder="Catégorie" name="Category">
            <datalist id="Category">
            @foreach(var category in Model.Category)
            {
                <option value="@category.Name"></option>
            }
            </datalist>

    I'd like to achieve something like :

            <input asp-for="@Model.Article.Category">
            <datalist asp-for="@Model.Article.Category" asp-items="@Model.Category">
            </datalist>

    This is why it's not working, how can I link an input with a datalist with ASP.NET Core ?

    Sunday, February 21, 2021 10:45 AM
  • User475983607 posted

    You have not clarified what "Not working" means.  There's no datalist tag helper in Core if that's what you are asking.

    Working example

    using Microsoft.AspNetCore.Mvc;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading.Tasks;
    
    namespace MvcDemo.Controllers
    {
        public class FormController : Controller
        {
            public class FormViewModel
            {
                public string Category { get; set; }
                public string[] Categories { get; set; }
            }
    
            [HttpGet]
            public IActionResult Index()
            {
                FormViewModel vm = new FormViewModel()
                {
                    Categories = PopulateOptions()
                };
                return View(vm);
            }
            [HttpPost]
            public IActionResult Index(FormViewModel model)
            {
                FormViewModel vm = new FormViewModel()
                {
                    Category = model.Category,
                    Categories = PopulateOptions()
                };
    
                return View(vm);
            }
    
            private string[] PopulateOptions()
            {
                return new string[] { "Option 1", "Option 2", "Option 3" };
            }
        }
    }
    
    @model MvcDemo.Controllers.FormController.FormViewModel
    @{
        ViewData["Title"] = "Index";
    }
    
    <h1>Index</h1>
    
    <form method="post">
        <input list="Categories" asp-for="Category">
        <datalist id="Categories">
            @foreach (var category in Model.Categories)
            {
                <option value="@category"></option>
            }
        </datalist>
        <input id="Submit1" type="submit" value="submit" />
    </form>
    
    

    Sunday, February 21, 2021 1:26 PM
  • User-1370514677 posted

    Hi @mgebhard,

    mgebhard

    You have not clarified what "Not working" means.

    I mean, when the User fills those fields :

    • Category (List<Category> => user's choice is linked to Article.Category)
    • Title (Article.Title)
    • Description (Article.Description)

    The only field that causes an "error" during ModelState Validation is Category. Even though the User does select a category, it acts like no categories were chosen.

            [Required(ErrorMessage = "Merci de spécifier une catégorie")] // This error message is triggered
            public virtual Category Category { get; set; }

    According to your code exemple it should normally go through ModelValidation without any issue but no :

            <input list="Category" asp-for="@Model.Article.Category">
            <datalist id="Category">
            @foreach(var category in Model.Category)
            {
                <option value="@category.Name"></option>
            }
            </datalist>

    Sunday, February 21, 2021 3:42 PM
  • User475983607 posted

    The only field that causes an "error" during ModelState Validation is Category. Even though the User does select a category, it acts like no categories were chosen.

    Category is complex type but the view defines category as a string.  You are not following model binding naming conventions or standard MVC programming patterns.   

    The code example in my last post specifically targets model binding and what a ViewModel should look like.  The getting started tutorials in this site also illustrate these standard patterns and practices. 

    https://docs.microsoft.com/en-us/aspnet/core/tutorials/first-mvc-app/start-mvc?view=aspnetcore-5.0&tabs=visual-studio

    https://docs.microsoft.com/en-us/aspnet/core/data/ef-mvc/?view=aspnetcore-5.0

    You'll need to learn the basics before moving forward.

    Sunday, February 21, 2021 5:37 PM
  • User-1370514677 posted

    Hi @mgebhard,

    Thanks again for the time you take to help me, I'm really grateful.

    Yet, I'm sorry but I don't understand what you mean by saying that I'm not following the model binding naming conventions.

    I already read the tutorials you shared (several times) but according to what you say and what is in my Controller I don't see where I did a mistake ?

    ArticleController.cs

            [HttpGet]
            public IActionResult Publish()
            {
                var ArticleViewModel = new ArticleViewModel();
                ArticleViewModel.Category = _articlecontext.Category.ToList();
                
                return View(ArticleViewModel);
            }
    
            [HttpPost]
            public IActionResult Publish(ArticleViewModel ArticleViewModel)
            {
                ArticleViewModel.Category = _articlecontext.Category.ToList();
    
                if(!ModelState.IsValid)
                    return View(ArticleViewModel);
    
                return RedirectToAction("Index","Home");
            }

    Indeed I "populate" the Category list with a basic access to the Article DB and a LINQ query ToList()

    Secondly, I use the following ViewModel which matches the official MVMC tutorial :

    using System.Collections.Generic;
    using DevAstuces.Models;
    
    namespace DevAstuces.ViewModels
    {
        public class ArticleViewModel
        {
            public List<Category> Category { get; set; }
            public Article Article { get; set; }
        }
    }

    Sunday, February 21, 2021 8:18 PM
  • User1686398519 posted

    Hi valenciano8, 

    1. Even though the User does select a category, it acts like no categories were chosen.
      1. You can see the code you originally provided:
        1. When you add name="Category", the Category in ArticleViewModel is bound after the form is submitted.
        2. But you did not assign a value to Category in Article, so an error (Required) will be triggered.
      2. <input list="Category" placeholder="Catégorie" name="Category">
      3.  public class Article
         {
           public virtual Category Category { get; set; }  
         }
         public class ArticleViewModel
         {
           public List<Category> Category { get; set; }
         }
    2. Even if you modify it to asp-for="@Model.Article.Category" now, the Category in your Article is of the Category type, but when you use the datalist, you get a string type. After submitting the form, even if the name is the same, the string type will not be successfully bound to the Category type.
      • <input list="Category" asp-for="@Model.Article.Category">
    3. Therefore, I suggest you modify the Category in Article to string type.

    Model

    public class Article
    {
      [Required(ErrorMessage = "Merci de spécifier une catégorie")] // This Error message is triggered
      public string CategoryName { get; set; }
    }

    View

    <input list="Category" placeholder="Catégorie" asp-for="@Model.Article.CategoryName">
    <datalist id="Category">
       @foreach (var category in Model.Category)
       {
          <option value="@category.Name"></option>
        }
    </datalist>
    <span asp-validation-for="@Model.Article.CategoryName" class="error_span"></span>

    Best Regards,

    YihuiSun

    • Marked as answer by Anonymous Thursday, October 7, 2021 12:00 AM
    Monday, February 22, 2021 7:15 AM
  • User-1370514677 posted

    Hi @YiHuiSun,

    Thanks a lot for your very detailed answer !

    It works now, you were right !

    Monday, February 22, 2021 8:52 AM