MVC Series Part 1: Dynamically Adding Items Part 2

Collections in a Dynamic Item

Click here to download code and sample project

In the first part of the series, I discussed how to add items dynamically. So, why don’t we expand on our idea and let’s say we want to add the ability to include another collection into our original dynamic item. We will expand our BookViewModel editor template to include a collection of 5 story Characters a user can insert:

<dl id="booksContainer">
        @foreach ( var classicBook in @Model.ClassicBooks )
        {
            <dl>
                <dt>
                    @Html.DisplayNameFor( model => classicBook.Title )
                </dt>
                <dd>
                    @Html.DisplayFor( model => classicBook.Title )
                </dd>

                <dt>
                    @Html.DisplayNameFor( model => classicBook.Author )
                </dt>
                <dd>
                    @Html.DisplayFor( model => classicBook.Author )
                </dd>
            </dl>
        }
</dl>

And we included a CharacterViewModel editor template that only includes the ability to insert first and last name. But when you attempt to add a book with a characters we run into the same problem originally where the new Characters are not binding correctly. The reason this exists is because our HtmlHelper is not able to uniquely identify sub items inside a recently dynamically added item. Makes sense?

No? Well, let’s discuss first what the BeginCollectionItem Html helper generates when injected into the Html element. Here is the BookViewModel’s partial view for the title:

<dt>
    @Html.DisplayNameFor( model => @Model.Title )
</dt>
<dd>
    @Html.EditorFor( model => @Model.Title )
</dd>

And here is the output that actually gets injected into the Html element after passing through the Html helper:

<dt>
    Title
</dt>
<dd>
    <input class="text-box single-line"
           id="NewBooks_3ad36db7-7685-4c8e-aadd-a2309f65e858__Title"
           name="NewBooks[3ad36db7-7685-4c8e-aadd-a2309f65e858].Title"
           type="text" value="" />
</dd>

Here you can see how the Html helper is inserting a Guid into the id and name fields so MVC can uniquely identify the property back to the item. But if we use the same comparison for Character, here is the partial view for name:

<dt>
    FirstName
</dt>
<dd>
    <input class="text-box single-line"
           id="Characters_dd611290-f64b-4b1a-9add-bad035f7b94f__FirstName"
           name="Characters[dd611290-f64b-4b1a-9add-bad035f7b94f].FirstName"
           type="text" value="" />
</dd>
<dt>
    LastName
</dt>
<dd>
    <input class="text-box single-line"
           id="Characters_dd611290-f64b-4b1a-9add-bad035f7b94f__LastName"
           name="Characters[dd611290-f64b-4b1a-9add-bad035f7b94f].LastName"
           type="text" value="" />
</dd>

Although the character model is correctly getting a Guid assigned, there is no way for MVC to know that CharacterViewModel belongs to a NewBooks sub item. In order to solve this, we need to delve into our BeginCollectionItem Html helper and make sure it retains the ‘NewBooks’ guid. This is quite straightforward if we do a comparison on the incoming collection name:

var htmlFieldPrefix = html.ViewData.TemplateInfo.HtmlFieldPrefix;
if ( htmlFieldPrefix.Contains( collectionName ) )
{
        collectionName = htmlFieldPrefix.Substring( 0, htmlFieldPrefix.IndexOf( collectionName ) + collectionName.Length );
}

And let’s take a look again at our injected output:

<dt>
     FirstName
</dt>
<dd>
    <input class="text-box single-line"
           id="NewBooks_1a95a483-e5c7-4696-ac91-bd9f0563b83b__Characters_e3e949f5-aae7-44cd-9aa8-419658c25e75__FirstName"
           name="NewBooks[1a95a483-e5c7-4696-ac91-bd9f0563b83b].Characters[e3e949f5-aae7-44cd-9aa8-419658c25e75].FirstName"
           type="text" value="" />
</dd>
<dt>
    LastName
</dt>
<dd>
    <input class="text-box single-line"
           id="NewBooks_1a95a483-e5c7-4696-ac91-bd9f0563b83b__Characters_e3e949f5-aae7-44cd-9aa8-419658c25e75__LastName"
           name="NewBooks[1a95a483-e5c7-4696-ac91-bd9f0563b83b].Characters[e3e949f5-aae7-44cd-9aa8-419658c25e75].LastName"
           type="text" value="" />
</dd>

Now our character items will get posted back onto our dynamic item.

3 thoughts on “MVC Series Part 1: Dynamically Adding Items Part 2

  1. Pingback: MVC Series Part 1: Dynamically Adding Items Part 3 | //InterKnowlogy/ Blogs

  2. I am very capable with web forms but the large differences in MVC have been really throwing me for a loop. So thank you for this series of posts, it was VERY helpful to me.

    Have you run into needing to remove the dynamically created items? For instance, your user added an extra book on accident and now they can’t submit the form since the book’s name field is required. I’m trying to add a ‘Delete Book’ button but I don’t know how I can reference the index for an ajax call. I might be able to find it from traversing the DOM but I figure that is an untenable solution and there is probably a better way I’m ignorant of. If I could dump that index property into an attribute within the partial view that would help further with dynamic behavior.

    • I have actually run into this problem before and the simplest solution I resorted to was adding a uniquely generated id on the incoming model and assigning the highest order html element of that model template the unique id.

      So, if we take my current demo project and add a property to our BookViewModel.cs class called UniqueId:

      public Guid UniqueId { get; set; }

      Then assign it inside our constructor:

      UniqueId = Guid.NewGuid();

      We can now reference this in our Model View template. Looking inside BookViewModel.cshtml, we need to move the ‘dl’ tag outside our ‘BeginCollectionItem’ Html helper and assign its id value to our unique id:

      < dl class="dl-horizontal" id="@Model.UniqueId">

      We need to do this because inside we are going to add a delete button. When the click event occurs, we’re going to call a javascript function and pass into it this parent ‘dl’ html element. The only way we can uniquely identify is by referencing the html element’s id field:

      < input type="button" id="Delete" name="Delete" value="Delete" onclick="javascript: deleteBook(document.getElementById('@Model.UniqueId'))" />

      And so in our Index.cshtml section where we put our scripts we simply include one more function which will remove any html element from the DOM:

      function deleteBook(bookDiv) {
      bookDiv.remove();
      }

      Click here to download the same sample project but with the delete button

      Hope this helps

Leave a Reply

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