Update - Variable Length Lists

Getting Started


I took a bit of time tonight to update my variable length lists solution and got rid of most of the ugliness.  I'll try and keep this short and sweet.  Here is the code.

Steve Sanderson said;

"Have you considered using Html.ValidationMessage() to display details of any binding errors that occurred? To make this work, you might need to stop using random GUID keys and ensure each entity keeps using the same key on subsequent requests.

Your CollectionScope concept is very neat. What about changing it to something like "ControlPrefixScope" so that it simply builds up a stack of HTML ID prefixes to keep things unique? That way, the programmer would remain in control of those prefixes and could ensure they are tied to the identity of entities being rendered."

These are the goals that I have worked towards the last couple hours.  And now we have this,

   10     <%foreach (Person p in Model){%>

   11         <%using(Html.ControlPrefixScope(p.Id)){ %>

   12             <%Html.RenderPartial("~/Views/List/PersonControl.ascx", p, ViewData); %>

   13         <%} %>

   14     <%}%>

this,

   10     <%foreach (Person p in Model){%>

   11         <%using(Html.ControlPrefixScope()){ %>

   12             <%Html.RenderPartial("~/Views/List/PersonControl.ascx", p, ViewData); %>

   13         <%} %>

   14     <%}%>

and this:

    9 <% Model.HtmlEach(p => Html.RenderPartial("~/Views/List/PersonControl.ascx", p, ViewData));%>


If you use the Html.ControlPrefixScope() with an empty constructor it will automatically look for an Id property on your model and use that as the index.  You may also use the Html.ControlPrefixScope(object obj) to assign any index you'd like.  This allows us to use ASP.NET MVC's ModelState to report errors throughout the entire list.

Cleaning Things Up

I couldn't stand the way I was inputing the hidden index field so I refactored it, even though it likely won't be needed in the next drop of ASP.NET MVC.  I am now rendering it directly into the response stream from on .Dispose() of the ControlPrefixScope.  This cleans up a whole lot of ugly code and makes creating Html Extension methods that use this technique a breeze.
This:

   21         public static string TextBoxFor<T>(this VariableList.HtmlHelper<T> helper, Expression<Func<T, string>> action, object htmlAttributes) where T : class

   22         {

   23             var body = action.Body as MemberExpression;

   24             if(body != null)

   25             {

   26                 var function = action.Compile();

   27                 T model = (T)helper.ViewData.Model;

   28                 object value = function(model);

   29 

   30                 string name = ObjectGraph.GetObjectGraph(body, model);

   31 

   32                 return helper.TextBox(name, value, htmlAttributes);

   33             }

   34 

   35             return string.Empty;

   36         }


is all you need.  The only really special line in there is line 30.


Finishing Up


There weren't a whole lot of changes that went into this update but they sure made a world of difference from a maintence and functionality point of view.  If you want to extend on this yourself with other conventions for identifying collections you can easily modify the .GetObjectGraph method.  Thanks again to Steve for making a couple of great suggestions.

My Take On Variable Length Lists in ASP.NET MVC

Catching Up


If you haven't already and want an understanding of the underlying issues at hand I recommend reading Phil Haack's and Steve Sanderson's posts.  To sum up, doing variable length lists in ASP.NET MVC is currently a fairly ugly process for some rather good reasons, and I wanted to see if I could take a stab at simplifying it.

Getting Started

I had previously written my own custom TextBoxFor<T> extensions.  In them, I traversed the expression tree to generate an input name that would look something like this with a ViewPage<Person>:

    <%= Html.TextBoxFor(p=>p.FirstName) %>

and would output:

    <input type="text" value="Justin" name="person.FirstName" />


The benefits of this method is that you can traverse an arbitrary length object graph and have it output html that will automatically work with ASP.NET MVC's default model binding.  For example (if a poor one):

    <%= Html.TextBoxFor(p=>p.Address.State.Country) %>

which would produce:

    <input type="text" value="United States" name="person.Address.State.Country" />


and we could then just get the entire Person back by simply defining a method on our controller like so:

            public ActionResult SubmitPerson(Person person)

            {

                //do stuff

            }


Inspiration

Now I was trying to think of a way to clean up the implementations that Phill and Steve used and was struck with one of those ah ha moments.  Having previously been looking at an excellent, up-and-coming patterns framework for dataccess, validation, and business rules by Ritesh Rao called NCommon.  I realized I could use the same pattern that he uses to implement his UnitOfWorkScope.  The end result of it all is this:

With ViewPage<IList<Person>>

        <%foreach (Person p in Model){%>

            <%using(Html.CollectionScope()){ %>

                <%Html.RenderPartial("~/Views/List/PersonControl.ascx", p); %>

            <%} %>

        <%}%>


and inside the person user control: 

         <%= Html.HiddenFor(p=>p.Id) %>

         <%= Html.TextBoxFor(p=>p.FirstName) %>

         <%= Html.TextBoxFor(p=>p.LastName) %>

         <%foreach (Address address in Model.Addresses){%>

             <%using(Html.CollectionScope()){ %>

                <% Html.RenderPartial("~/Views/List/AddressControl.ascx", address); %>

             <%} %>

         <%} %>


and with a little extension method foo we can get to this:

     <% Model.HtmlEach(p => Html.RenderPartial("~/Views/List/PersonControl.ascx", p));%>


And this small amount of display logic allows us to automatically get our list of Persons back like so:

            public ActionResult SubmitPeople(IList<Person> people)

            {

                //do stuff

            }


The Code

If you want to just jump right in and get the code you can access it here.  Otherwise read on.

Edit: the above download is an updated copy of the code as described here.

The CollectionScope class that I use here:

        <% using(Html.CollectionScope()){ %>

   

        <%} %>

 
is stored on a Stack<CollectionScope> in HttpContext.Current.Items.  This allows us to keep track of all of our CollectionScopes and generate our object graph with or without indexers, based upon whether we are within a CollectionScope or not.

The implementation of our Html extension Method TextBoxFor<T> is where we access the CollectionScope stack and generate our input names.

   28         public static string TextBoxFor<T>(this VariableList.HtmlHelper<T> helper, Expression<Func<T, string>> action, object htmlAttributes) where T : class

   29         {

   30             var body = action.Body as MemberExpression;

   31             if(body != null)

   32             {

   33                 string name = body.GetObjectGraph();

   34                 var function = action.Compile();

   35                 T model = (T)helper.ViewData.Model;

   36                 string value = function(model);

   37 

   38                 if (CollectionScope.Current != null &&   !CollectionScope.Current.HasSetIndexer)

   39                 {

   40                     CollectionScope.Current.HasSetIndexer = true;

   41 

   42                     string indexProp = name.Substring(0, name.LastIndexOf('['));

   43                     return helper.Hidden(indexProp + ".Index",

   44                         CollectionScope.Current.Indexer.ToString())

   45                         + helper.TextBox(name, value, htmlAttributes);

   46                 }

   47 

   48                 return helper.TextBox(name, value, htmlAttributes);

   49             }

   50 

   51             return string.Empty;

   52         }



The most important line (33) is currently hidden in an extension method .GetObjectGraph().
This extension method basically recurses through the Expression tree and generates the name attribute that we then insert into our input tag.  Here is an example input tag from the demo project.

   <input type="text" value="City0:0" name="people[eb528441-c0b7-46fc-bd41-85dca72a86d6].addresses[2a269c54-1396-4804-a83c-dbde65b19a84].City" id="people[eb528441-c0b7-46fc-bd41-85dca72a86d6].addresses[2a269c54-1396-4804-a83c-dbde65b19a84].City"/>


As you can see I'm using a Guid to ensure uniqueness for each index.  I'm not a huge fan of this and will probably rework it to keep page-size down and readability up but for now it works.

Things I Don't Like

As I stated above I'm using a Guid to ensure uniqueness.  This makes the page a little ugly to read and could hurt performance.  Another issue currently is this block of code in each For<T> extension:

   38                 if (CollectionScope.Current != null && !CollectionScope.Current.HasSetIndexer)

   39                 {

   40                     CollectionScope.Current.HasSetIndexer = true;

   41 

   42                     string indexProp = name.Substring(0, name.LastIndexOf('['));

   43                     return helper.Hidden(indexProp + ".Index",

   44                         CollectionScope.Current.Indexer.ToString())

   45                         + helper.TextBox(name, value, htmlAttributes);

   46                 }


This block is used to insert an input tag to define the indexer for each unique item in a collection.  There are a couple of ways I could rework this to make it a lot less code for each Html extension method but levib mentions in this thread that the requirement for the indexer definition is likely to go away in the next drop of ASP.NET MVC, so I'm not going to bother at the moment.

The Sappy Ending

Since this is my first post I'd like to say a big "Thank You!" to all the bloggers out there that helped me on my way.  If you have any questions, comments, or constructive criticism please feel free to leave them in the comments.

Edit:  An updated post on this topic can be found here.
«July»
SunMonTueWedThuFriSat
2829301234
567891011
12131415161718
19202122232425
2627282930311
2345678