Search code examples
c#.netasp.net-mvcasp.net-corerazor

Form in foreach in Razor view: Validation is shown to all the elements


Consider the following Razor code

foreach(Person p in Model.Persons){
    <form>

        @Html.TextBoxFor(m => p.Name)
        @Html.ValidationMessageFor(m => p.Name)

        @Html.TextBoxFor(m => p.Surname)
        @Html.ValidationMessageFor(m => p.Surname)

        <button type="submit">Save</button>
        
    </form>
}

In the processed .html each Person will have an identical name attribute with each other. This leads to a wrong behaviour when the validation does not succeed: each Person will have an error message even if I was editing only one of them.

What would be the best way to resolve this issue?

My Post handler is as follows

[HttpPost]
public IActionResult SavePerson(Person person){
    // ...
}

and it'd be great to leave it as it is.


Solution

  • The reason why the validation error will show for all input textbox is Html.TextBoxFor and Html.ValidationMessageFor helpers generate HTML elements with the same name attributes for each Person in the list. This results in validation messages being shown for all items when only one item is edited and validation fails.

    To solve this issue, I suggest you could consider create a partial views to split each Person form and handle the submit individually.

    More details, you could refer to below example:

    1.Create a partial views inside the shared view folder:

    @model Person
    
    <form asp-action="SavePerson" method="post">
        @Html.HiddenFor(m => m.Id)
    
        <label for="Name">Name</label>
        @Html.TextBoxFor(m => m.Name)
        @Html.ValidationMessageFor(m => m.Name)
    
        <label for="Surname">Surname</label>
        @Html.TextBoxFor(m => m.Surname)
        @Html.ValidationMessageFor(m => m.Surname)
    
        <button type="submit">Save</button>
    </form>
    

    2.Use it inside the view:

    @foreach (var person in Model.Persons)
    {
        @await Html.PartialAsync("_PersonForm", person)
    }
    

    Result:

    enter image description here

    Post method:

    enter image description here


    My whole test sample:

    Person model:

    public class Person
    {
        public int Id { get; set; }
    
        [Required]
        public string Name { get; set; }
        [Required]
    
        public string Surname { get; set; }
    
    }
    

    Home controller:

        public IActionResult Index()
        {
            List<Person> people = new List<Person>() { new Person { Id=1, Name="test1", Surname="test1" }, new Person { Id = 2, Name = "test2", Surname = "test2" } };   
    
            return View(people);
        }
        [HttpPost]
        public IActionResult SavePerson(Person person)
        {
            if (ModelState.IsValid)
            {
                // Save the person data to the database or perform necessary operations
               
                return RedirectToAction("Index");
            }
    
           
            return View(person);   
        }
    

    View:

    @{
        ViewData["Title"] = "Home Page";
    }
    
    @model List<Person>
    
    
    <div class="text-center">
        <h1 class="display-4">Welcome</h1>
        <p>Learn about <a href="https://learn.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
    </div>
    
    <><hr/>
    
        @foreach (var person in Model)
        {
            @await Html.PartialAsync("_PersonForm", person)
        }
    
    
    
    
        @section scripts {
        <partial name="_ValidationScriptsPartial" />
        }  
    

    Test result:

    It works at myside, each submit button works for each form.

    enter image description here