5

in my create view I want to give the user the possibility to create a list of objects (of the same type). Therefore I created a table in the view including each inputfield in each row. The number of rows respective "creatable" objects is a fixed number.

Lets say there is a class Book including two properties title and author and the user should be able two create 10 or less books.

How can I do that?

I don't know how to pass a list of objects (that are binded) to the controller. I tried:

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(ICollection<Book> bookList)
    {
        if (ModelState.IsValid)
        {
            foreach(var item in bookList)
                db.Books.Add(item);
            db.SaveChanges();
            return RedirectToAction("Index");
        }
        return View(articlediscounts);
    }

And in the view it is:

<fieldset>
    <legend>Book</legend>

    <table id="tableBooks" class="display" cellspacing="0" width="100%">
            <thead>
                <tr>
                    <th>Title</th>
                    <th>Author</th>
                </tr>
            </thead>
            <tbody>
                @for (int i = 0; i < 10 ;i++ )
                {
                    <tr>
                        <td>
                            <div class="editor-field">
                                @Html.EditorFor(model => model.Title)
                                @Html.ValidationMessageFor(model => model.Title)
                            </div>
                        </td>
                        <td>
                            <div class="editor-field">
                                @Html.EditorFor(model => model.Author)
                                @Html.ValidationMessageFor(model => model.Author)
                            </div>
                        </td>
                    </tr>
                }
            </tbody>
        </table>

    <p>
        <input type="submit" value="Create" />
    </p>
</fieldset>

As booklist is null, it doesn't work and I don't know how to put all created objects in this list.

If you have any suggestions I would be very thankful.

Underfaker
  • 97
  • 2
  • 12

3 Answers3

2

Scott Hanselman has some details on passing arrays to MVC control binding: http://www.hanselman.com/blog/ASPNETWireFormatForModelBindingToArraysListsCollectionsDictionaries.aspx

Which is essentially: ensure your controls have the correct names: using an index for lists

Change your for loop to something like:

@for (int i = 0; i < 10 ; i++)
{
    <tr>
        <td>
            <div class="editor-field">
                <input type="text" name="book[" + i + "].Title" />
            </div>
        </td>
        <td>
            <div class="editor-field">
                <input type="text" name="book[" + i + "].Author" />
            </div>
        </td>
    </tr>
}

this will then bind to your post action automatically.

[HttpPost]
public ActionResult Create(IList<Book> bookList)

You can then show/hide these as required or use js/jquery to add them dynamically

Edit: As correctly observed by Stephen Muecke, the above answer only regards the binding from the form+fields to the HttpPost, which appears to be the emphasis of the question.

The post action in the original post is not compatible with the view. There's quite a bit of missing code in the OP that may or may not be relevant, but worth observing that if your view is for a single model, then your fail code on ModelState.IsValid needs to return a single model or your view needs to be for an IList (or similar), otherwise you won't get server-side validation (but you can still get client-side validation if you manually add it to the <input>s)

freedomn-m
  • 27,664
  • 8
  • 35
  • 57
0

You'd need to send a JSON object that has the list of books in it. So the first thing is to create a Model class like this:

public class SavedBooks{
    public List<Book> Books { get; set; }
}

Then the Book class would have to have these 2 props:

public class Book {
   public string Title { get; set; }
   public string Author { get; set; }
}

Next, change your controller to use this model:

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(SavedBooks model)

Then create a javascript method (using jQuery) to create a JSON object that matches the structure of the controllers SavedBooks class:

var json = { Books: [ { Title: $('#title_1').val(), Author: $('#Author_1').val() } ,
                      { as many items as you want }
                    ]
           };
$.ajax(
{
    url: "/Controller/Create",
    type: "POST",
    dataType: "json",
    data: json
});
T McKeown
  • 12,971
  • 1
  • 25
  • 32
0

The fact you use @Html.EditorFor(model => model.Title) suggests that you have declared the model in the view as

@model yourAssembly.Book

Which allows to to post back only one Book so the POST method would need to be

public ActionResult Create(Book model)

Note that you current implementation create inputs that look like

<input id="Title" name="Title" ... />

The name attributes do not have indexers (they would need to be name="[0].Title", name="[1].Title" etc.) so cannot bind to a collection, and its also invalid html because of the duplicate id attributes.

If you want to create exactly 10 books, then you need initialize a collection in the GET method and pass the collection to the view

public ActionResult Create()
{
  List<Book> model = new List<Book>();
  for(int i = 0; i < 10;i++)
  {
    model.Add(new Book());
  }
  return View(model);
}

and in the view

@model yourAssembly.Book
@using (Html.BeginForm())
{
  for(int i = 0; i < Model.Count; i++)
  {
    @Html.TextBoxFor(m => m[i].Title)
    @Html.ValidationMessageFor(m => m[i].Title)
    .... // ditto for other properties of Book
  }
  <input type="submit" .. />
}

which will now bind to your collection when you POST to

public ActionResult Create(List<Book> bookList)

Note the collection should be List<Book> in case you need to return the view.

However this may force the user to create all 10 books, otherwise validation may fail (as suggested by your use of @Html.ValidationMessageFor()). A better approach is to dynamically add new Book items in the view using either the BeginCollectionItem helper method (refer example) or a client template as per this answer.

Community
  • 1
  • 1
  • "Which allows to to post back only one Book" is not correct. The viewmodel of the GET does not need to match the model of the POST. The post action parameters are bound based on the fields in the form, not the `@model` of the view. – freedomn-m Jun 01 '15 at 08:28
  • @freedomn-m, Look at the code in OP's view - they are creating an input with `name="title"` and one input with `name="Author"` which will post back to one `Book` object! –  Jun 01 '15 at 08:37
  • "@model yourAssembly.Book.. which allows you to post back only one Book" - your statement about only posting one book refers to the `@model` - not the input names. The rest of the post with `m[i].Title` etc is good, just that part. – freedomn-m Jun 01 '15 at 08:43
  • Only if your were to not use model binding, ignore validation, have it fail if you return the view etc. etc, (as per your answer) but I suspect that not what OP wants –  Jun 01 '15 at 08:47