skip to Main Content

I want to create a nested form just like on ruby on rails form_for for my view using .net mvc.

I have a models that has one to many relationship.

// Principal (parent)
public class Blog
{
    public int Id { get; set; }
    public int Name { get; set; }
    public ICollection<Post> Posts { get; set;}
}

// Dependent (child)
public class Post
{
    public int Id { get; set; }
    public int Title { get; set; }
    public int BlogId { get; set; } 
    public Blog Blog { get; set; }
}

and I have this view model:

public class CreateBlogVM
{
    public int Name { get; set; }
    public ICollection<Post> Posts { get; set;}
}

and a GET Request action that initialize the Posts

[HttpGet]
public ActionResult<CreateBlogVM> Create()
{
    var vm = new CreateBlogVM()
    {
        Posts = [new Posts { Title = "" }]

    };
    return View(vm);
}

On my view I wanted to create a nested form for the posts properties just like in ruby on rails

@model CreateBlogVM

<form asp method="post" asp-action="Create">
  <div class="form-group col-4">
     <label asp-for="Name" class="control-label">Name:</label>
     <input asp-for="Name" class="form-control" />
     <span asp-validation-for="Name" class="text-danger"></span>
   </div>

   @foreach (var post in @Model.Posts)
   {
     <div class="form-group col-4">
        <label asp-for="@post.Title" class="control-label">Title:</label>
        <input asp-for="@post.Title" class="form-control" />
        <span asp-validation-for="@post.Title" class="text-danger"></span>
     </div>  
   }
</form>

Validating the POST request I see no posts. Am I missing something? I was trying to look for a documentation about nested forms or nested view models.

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Create(CreateBlogVM model)
{
   System.Console.WriteLine($"{string.Join("-", model.Posts)}"); <--- throws an error because Posts here is null
   if (ModelState.IsValid)
   {
      await _blogService.Create(model);
      return RedirectToAction(nameof(Index));
   }
   return View(model);
}

UPDATE:

OK upon investigating. I just found out that the request is null because data its being sent like this:

Title: ""

and html is being formatted like this.

<input name="Title" class="form-control" />

HTML should be like this. The name property should be like in array format

<input name="Posts[0].Title" class="form-control" />

So I think I have to do it manually since .net mvc doesnt support something like this.

The way I’m thinking to do it and the only way is thru JQuery.

2

Answers


  1. Chosen as BEST ANSWER

    This is how I fixed my problem.

    on my HTML I have this:

    <div class="col-7" id="container">
    <button type="button" class="btn btn-info" id="addPostBtn" data-count="@Model.Posts.Count">Add Post</button>
    
        @for (int i = 0; i < Model.Posts.Count; i++)
        {
          <div class="row">
            <div class="form-group col-4">
              <label asp-for="Posts[@i].Title" class="control-label">Title:</label>
              <input name="Posts[@i].Title" class="form-control" />
              <span asp-validation-for="Posts[@i].Title" class="text-danger"></span>
            </div>
          </div>
        }
    </div>
    

    and on you script section you can do this:

    @section Scripts {
      <script>
        const addPostBtn = $('#addPostBtn');
        const postCount = $('#addPostBtn').data("count");
        const container = $('#container');
    
        addPostBtn.on("click", () => {
          container.append(appendHtml(postCount));
        });
    
    
        function appendHtml(count) {
          return `
            <div class="row">
              <div class="form-group col-4">
                <label asp-for="Posts[${count}].Title" class="control-label">Title:</label>
                <input name="Posts[${count}].Title" class="form-control" />
                <span asp-validation-for="Posts[${count}].Title" class="text-danger"></span>
              </div>
            </div>
          `
        };
      </script>
    }
    

    The issue with this solution is. When the model state gets validated then the old data is not being preserved. So the users have to retype there post again.

    Update:

    ok for the data to be preserved you have to add the value property on your input tag.

    <input name="Posts[@i].Title" class="form-control" value="@(Model.Posts[@i].Title)" />
    

  2. Maybe you miss Include statement in model loading from DbContext?

    For example:

    var model = _context.Blogs
        .Include(x => x.Posts) // <-- that line
        .AsNoTracking()
        .FirstOrDefault(x => x.Id == request.id);
    

    Or, if you really constructing an object in controller without DbContext try to use Dto as View Model with List<Post> property.

    For example:

    public sealed record BlogDto
    {
        public int Id { get; set; }
        public int Name { get; set; }
        public List<PostDto> Posts { get; set;}
    }
    public sealed record PostDto
    {
        public int Id { get; set; }
        public int Title { get; set; }
        public int BlogId { get; set; } 
    }
    

    And configure AutoMapper, docs are here.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search