Collections in a Dynamic Item
Click here to view the source code
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.
Removing Dynamic Items
Click here to view the updated source code with delete button
Another problem I have run into is attempting to remove items that were dynamically created. 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 the 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) { |