Search code examples
c#htmlasp.net-mvcrazormodel-binding

HTML.Textarea Values in MVC Razor View


It is hard for me to clearly state the problem I am having. I am trying to understand how to retain values in form fields created in a loop after validation fails. I have a more complicated real world form that has a bunch of elements created in the loop and validation. I have reduced it to a simple example included below.

When validation fails I would like the textareas named "Comment" that have been created in the loop to retain the values that are shown in the Pre-Submit image below.

When I debug the form submission, the values from each of the fields are successfully connected to the IList variable named Comment found in the Model. This is what I want so I can loop through and locate them based on index.

After submitting, each textarea produced by the loop shows the comma separated representation of the IList variable Comment in the Model. It appears that the field in the view and in the model are connecting because they share a name. They connect properly on the way in but not on the way out. I would like the view to only show the value associated with the Comment[i] instead of the entire list so that the values remain constant between form submissions.

Screenshots and Sample Code Below
First Load:
First load of form without changes

Pre-Submit Form Changes:
Form with changes to the first input before submitting

Form as seen after first submit:
Form as seen after the first submission

Form as seen after second submit:
enter image description here

Model Code

using System.Collections.Generic;
namespace UI.Models.Forms
{
    public class TempListModel : ContentModel
    {
        public TempListModel()
        {
            Comment = new List<string>();
        }
        public IList<string> Comment { get; set; }  //Comments for each URL in the list
    }
}


View Code

@model UI.Models.Forms.TempListModel  
@using (Html.BeginForm("temptest", "Test", new { id = 1 }, FormMethod.Post, new { id = "listForm", name = "listForm" }))
{
    <ul>
        @for (int i = 0; i < Model.Comment.Count(); i++)
        {
            <li>
                <div class="llformlabel">
                    Notes:
                    <div>@Model.Comment[i]</div>
                    @Html.TextArea("Comment", Model.Comment[i], 4, 63, new { @id = "Comment_" + i, @title = "Comment" })</div>
            </li>
        }
    </ul>
    <input type="submit" value="Save Changes" />
}


Controller Code

using System.Collections.Generic;
using System.Web.Mvc;
using UI.Models.Forms;
namespace UI.Controllers
{
    public class TestController : Controller
    {
        [AcceptVerbs(HttpVerbs.Post)]
        public ActionResult TempTest(TempListModel model)
        {
            //This function executes after the user submits the form.
            //If server side validation fails then the user should be shown the form as it was when they submitted.
            //model.Comment = GetComments();  //In my real world example this comes from a database.
            if (true) //!ModelState.IsValid) //In my real world code this is a validation step that may fail
            {
                return View(model);
            }
        }
        [AcceptVerbs(HttpVerbs.Get)]
        public ActionResult TempTest(int? id)
        {
            //In the real world example there is a lot going on in this function.
            //It is used to load data from databases and set up the model to be displayed.
            var model = new TempListModel(); 
            model.Comment = GetComments();
            return View("TempTest", "TempLayout", model);
        }
        private static IList<string> GetComments()
        {
            //Simple sample function used for demo purposes.
            IList<string> comments = new List<string>();
            comments.Add("Comment 1");
            comments.Add("Comment 2");
            comments.Add("Comment 3");
            return comments;
        }
    }
}

Solution

  • If you fail validation just return the model.

        [AcceptVerbs(HttpVerbs.Post)]
        public ActionResult TempTest(TempListModel model)
        {
            if (ModelState.IsValid)
            {
                return RedirectToAction("TempTest");
            }
            return View(model);
        }
    

    Edit Try this in your view instead

    @for (int i = 0; i < Model.Comment.Count(); i++)
    {
        <li>
            @Html.TextAreaFor(m => m.Comment[i], 4, 63, new { @title = "Comment" })
        </li>
    }
    

    And let the helper name the elements for you. You end up with name attributes like Comment[i].