Code Buckets

Buckets of code

.Net

ASP.Net MVC and Binding Complex Objects Magic

I’ve a habit of declaring things magical when I don’t fully understand how they work and haven’t the time to look into them any further. One of the things I’ve often declared magical is model binding with MVC. It just works. Magical.

Recently though I found model binding with child objects a bit on the difficult side of things. The magic was failing to sparkle correctly and needed a little bit of extra developer sprinkles to work. Perhaps it even needs developer understanding.

Demo Application

I’m going to demonstrate the problem and solution using a demo library application. The application queries Google Books API and allows users to edit and add those books to their own virtual bookshelf. The book shelf app can be found here

https://github.com/timbrownls20/BookShelf/

 Workflow

The relevant part of the application is adding of books to the library. The work flow is …

  1. User searches for a book
  2. Application queries GoogleBooks and displays a list of matches
  3. User selects a book
  4. Application displays the selected book.
  5. The user amends the book record as required
  6. The application saves the book

So once the user has typed in a search term they are presented with a list of books which they can select.

book list
Save.cshtml

Once selected a view showing the detail of the book is present to the user. The user is invited to save the book into his or her book shelf.

authors-incorrect-display
Fig 2: Book.cshtml

The problem is that the authors are not saved correctly. It’s pretty obvious that the authors aren’t displayed correctly. The HTML output looks fairly nonsensical.

We are going to have to dig into the application a bit further to fully understand this.

The Model

The model of the application is

public class Book
{
    public int? BookID { get; set; }
 
    [Required]
    public string Title { get; set; }

    [AllowHtml]
    [DataType(DataType.Html)]
    public string Description { get; set; }

    [DataType(DataType.ImageUrl)]
    public string Thumbnail { get; set; }

    public List<Author> Authors { get; set; }
}

public class Author
{
    public int AuthorID { get; set; }

    public string DisplayName { get; set; }
}

So a book can have multiple authors. It is a complex object. It is the complex object and how that is rendered on the Book.cshtml view that causes us the problem. When it is posted back the book object lacks authors. The binding of the complex model has failed.

Book Create Action

When the user selects a book from this list this action is called. It displays the book ready for a save. We pass in an id (from the list selection) then the service goes away and grabs the full details of the book.

public ActionResult Create(string id)
{
   var book = bookService.Get(id);
   return View("Save", book);
}

The book is then displayed on the view Save.cshtml. The user presses submit which posts the book back to the server.

<div class="form-group">
   <!-- the other properties are displayed here -->
   <div class="form-group">
      @Html.LabelFor(model => model.Authors, htmlAttributes: new { @class = "control-label col-md-2" })
      <div class="col-md-10">
         @Html.EditorFor(model => model.Authors, new { htmlAttributes = new { @class = "form-control" } })
      </div>
   </div>
   <!--- rest of the form, submit buttons etc.. -->
</div>

Book Save Action

This action is called when the user saves a book to his or her bookshelf. The posted back book is saved.

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(Book book)
{
   if (ModelState.IsValid)
   {
      db.Books.Add(book);
      db.SaveChanges();
      return RedirectToAction("Index");
   }
   return View("Save", book);
}

This action takes the book object and it is here that we want form data to bind and produce the complex object. We want the model binding to produce the book object as a parameter. We want the magic. The magic fails us.

The Problem

It doesn’t just magically work much to my disappointment. What is displayed is not helpful.

authors-incorrect-display
Fig3. Book.cshtml with incorrect author display

Posting this page doesn’t binding the child object and the authors aren’t there. The HTML is just not there. So how can I easily, and with as much magic as possible, change the view so that the authors are present and the form elements are named correctly? What does the default model binder actually want and how can I provide it?

The Solution

EditorFor Templates

The view needs to know how to display the author object. I could loop through in in the view but a better solution is the EditorFor template.

In View/Shared/EditorTemplates create a file called Author.cshtml. By convention MVC will find this and use it to display the Author property. We are telling it what to write out when the author property is edited i.e. what HTML to write out when the following call is made in the view

@Html.EditorFor(model => model.Authors, new { htmlAttributes = new { @class = "form-control" } })

The Author.cshtml is pretty simple

<div class="form-group">
    <div class="col-md-10">
        @Html.HiddenFor(model => model.AuthorID)
        @Html.EditorFor(model => model.DisplayName, new { htmlAttributes = new { @class = "form-control" } })
    </div>
</div>

The Corrected View

Now this is in place the view looks a lot better and it works.

 

authors-correct-display
Fig 4. Book.cshtml with corrected author functionality

When save is pressed the full object is passed to the Save Action and the object included authors can be persisted

What is interesting is the rendered output. Viewing the page source (stripping out the validation data elements, styling etc..) we see this

<input id="Authors_0__AuthorID" name="Authors[0].AuthorID" type="hidden" value="0">

<input id="Authors_0__DisplayName" name="Authors[0].DisplayName" type="text" value="Adam Freeman">

<input id="Authors_1__AuthorID" name="Authors[1].AuthorID" type="hidden" value="0">

<input id="Authors_1__DisplayName" name="Authors[1].DisplayName" type="text" value="Matthew MacDonald">

<input id="Authors_2__AuthorID" name="Authors[2].AuthorID" type="hidden" value="0">

<input id="Authors_2__DisplayName" name="Authors[2].DisplayName" type="text" value="Mario Szpuszta">

So the convention that the default model binding requires is revealed. I wouldn’t have just guessed it but the EditorFor Template does it for us.

Although both the id and name look like reasonable candidates for model binding, it is the name attribute that is used. The format for child objects is

name=”PropertyName[index].ChildPropertyName”

so more complex objects are perfectly possible

name=”PropertyName[index].ChildPropertyName[index].GrandchildPropertyName”

and so on until you gone down a complex rabbit hole of 7 nested objects from which you might never return.

Adding New Authors (child objects)

One of the things we might want to do is enable a user to add new authors. Perhaps the retrieved book doesn’t have all the authors and the user is a stickler for detail. Knowing the required format of the child object html is going to help us with that. In its very simplest form it could a bit of jQuery based on this idea

var index = getLastIndex() + 1;
var newAuthor = $(“#authorTextBox”).val();

$( ".container" ).after("<input id="Authors_" + index + "__AuthorID" name="Authors[" + index + "].AuthorID" type="hidden" value=0>" );

$( ".container" ).after ("<input id=’Authors_" + index + "__DisplayName' name='Authors[" + index + "].DisplayName' type=text value='"+ newAuthor + "'>" );

But we would need to bit cleverer, use templating, work out the next index etc… But this gives us the starting point to be able to provide that functionality. An exercise for another day perhaps.

Note
I’ve now implemented a solution using Handlebars.js to add authors into the complex object. See this post for details.

DisplayFor Templates

As an aside we could/should also do a template for displaying a read version of the model i.e. when the following call is made in a view.

@Html.DisplayFor(model => model.Author)

It’s not needed to get this to work but it seems like a generally good and wholesome thing to provide this as well. The display template can be very simple..

@model Author

@Html.DisplayFor(model => model.DisplayName, new { htmlAttributes = new { @class = "form-control" } })

 Useful Links

More detail about EditorFor and DisplayFor templates
http://www.codeguru.com/csharp/.net/net_asp/mvc/using-display-templates-and-editor-templates-in-asp.net-mvc.htm

Some more insight into how complex data binding works
http://blog.codeinside.eu/2012/09/17/modelbinding-with-complex-objects-in-asp-net-mvc/

How to manipulate the DOM through JQuery which we would need to dig into a bit to implement adding an author
http://api.jquery.com/category/manipulation/

Google Books API reference in the blog post
https://developers.google.com/books/

 

LEAVE A RESPONSE

Your email address will not be published. Required fields are marked *