MVC Series Part 1: Dynamically Adding Items Part 3

Collections named with same substring in a Dynamic Item

Click here to download code and sample project

In the second part of the series, I discussed how to include a collection inside a dynamic item. Here is one gotcha that I ran into during development. Let’s say we decide to add a Publisher for every new book and you want the ability to add a few books the publisher has distributed. Here is the PublisherViewModel class I created:

public class PublisherViewModel
	{
		public PublisherViewModel()
		{
			Books = new List<PublisherBookViewModel>();
			for ( int i = 0; i < 3; i++ )
			{
				Books.Add( new PublisherBookViewModel() );
			}
		}
		public string Name { get; set; }

		public IList<PublisherBookViewModel> Books { get; set; }
	}

Here is how the html would look for PublisherViewModel:

<dl class="dl-horizontal">
    <dt>
        @Html.DisplayNameFor( model => @Model.Name )
    </dt>
    <dd>
        @Html.EditorFor( model => @Model.Name )
    </dd>
    <br />

    <dt>
        @Html.DisplayNameFor( model => @Model.Books )
    </dt>
    <dd>
        @for ( int i = 0; i < @Model.Books.Count(); i++ )
        {
            @Html.EditorFor( model => @Model.Books[i] )
        }
    </dd>
</dl>

I went ahead and created a new class for the books collection called PublisherBookViewModel. It simply contains one property for Name. Here is how the Html looks:

@using ( Html.BeginCollectionItem( "Books" ) )
{
    <dl class="dl-horizontal">
        <dt>
            @Html.DisplayNameFor( model => @Model.Name )
        </dt>
        <dd>
            @Html.EditorFor( model => @Model.Name )
        </dd>
    </dl>
}

But when I go to create one new Book and post back, I actually end up receiving 4 items in my ‘NewBooks’ collection. How did this even happen?

Well, the reason this occurred is again thanks to our BeginCollectionItem HtmlHelper class. The extra logic that we added in the last article seems to merely grab the first occurrence of our collection name:

collectionName = htmlFieldPrefix.Substring( 0, htmlFieldPrefix.IndexOf( collectionName ) + collectionName.Length );

If we look at what the HtmlFieldPrefix string looks like coming in we can start to spot the problem:

"NewBooks[18fe8281-9b41-43ff-bcc9-a14ce3e99dc8].Publisher.Books[0]"

So, our parent dynamic item is being bounded to a collection called ‘NewBooks’, but the PublisherViewViewModel has its own collection of items called ‘Books’. Our Substring method needs to be a little bit smarter by replacing ‘IndexOf’ with ‘LastIndexOf’. And with that, we will no longer have issues with sub collection binding.

This is as far as I took with dynamically adding items in MVC, but at some point it would be nice to be able to add dynamic items to a dynamic item (i.e., instead of pre-populating our Characters collection, we let the user add as many as they need). Perhaps another day I will delve into this.

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.