Tim Scott's Blog

January 2, 2009

Editing a Variable Length List Of Items With MvcContrib.FluentHtml

Filed under: FluentHtml, MS MVC — Tim Scott @ 9:28 pm

Steve Sanderson recently showed  a nifty way to edit variable length lists with MS MVC .  I commented that I liked his approach.  I also mentioned that MvcContrib.FluentHtml might be used to improve a bit of awkward code used to build HTML element names.  To illustrate my point I offered some code examples.   Steve responded by pointing out the shortcomings of  my examples.  He was correct.   I had not really thought it through.  While my examples illustrated the use of FluentHtml, they missed the mark in terms of showing a real solution.

Still I knew that the basic idea of was sound.  This article shows in detail how to use MvcContrib.FluentHtml to edit a variable length list of items.

Get The Code

I will use the same basic application as Steve did — a simple gift request form.

Setup

We start with the Gift entity:

public class Gift
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
}

Render The View

Our view derives from MvcContrib.FluentHtml.ModelViewPage<IList<Gift>> and is rendered via a controller action:

[AcceptGet]
public ViewResult Index()
{
     ViewData.Model = new List<Gift>
     {
         new Gift {Id = 1, Name = "Tar Tinker", Price = 23.44m},
         new Gift {Id = 2, Name = "Flu Flooper", Price = 11.42m}
     };
     return View();
}

The view presents a list of the gifts:

<form action="<%=Html.BuildUrlFromExpression<HomeController>(x => x.Index(null))%>" method="post">
    <div id="addGiftItem">
        <a href="" id="addGift">Add</a>
    </div>
    <fieldset id="giftLineItems">
        <legend>Gifts</legend>
        <%for (var i = 0; i < ViewData.Model.Count; i++) {%>
            <%Html.RenderPartial("GiftLineItem", ViewData.Model, new ViewDataDictionary {{"index", i}});%>
        <%}%>
    </fieldset>
    <%=this.SubmitButton("Save")%>
</form>

GiftLineItem is a user control that derives from MvcContrib.FluentHtml.ViewModelUserControl<IList<Gift>>:

<%var i = (int)ViewData["index"];%>
<div class="giftLineItem">
    <%=this.Hidden(x => x[i].Id)%>
    <%=this.TextBox(x => x[i].Name).Label("Name of gift:")%>
    <%=this.TextBox(x => x[i].Price).Label("Price ($):")%>
    <a href="" class="removeGift">Delete</a>
</div>

You might observe that this user control uses IList<Gift> as its model whereas Steve’s uses Gift.   But the user control represents one gift, right?  Have I sacrificed cohesion? Yes, however a closer look shows that neither Steve’s user control nor mine is cohesive.  That is, neither stands alone outside of the context of a list.  We simply use different techniques to name the elements within the context of an owning list.

Here’s the rendered view (after clicking “Add” twice):

variablelineitemdemo1.gif

Add Behavior

Now we need to add behavior to the “Add” and “Delete” links.  For that we’ll use jQuery:

$(document).ready(function() {

    var nextGiftIndex = <%=ViewData.Model.Count%>;
    setRemoveLinks();

    $('#addGift').click(function() {
        $.get('/Home/AddGift?index=' + nextGiftIndex, 'html', function(lineItemHtml) {
            $('#giftLineItems').append(lineItemHtml);
            setRemoveLinks();
        });
        nextGiftIndex++;
        return false;
    });
});
function setRemoveLinks() {
    $('a.removeGift').click(function() {
        $(this).parent('div').remove();
        return false;
    });
}

Clicking the “Add” link calls the AddGift action asyncronously and passes the index of the item to be added.  The index value is initialized when the page is rendered and incremented on the client side with each addition.  The “Delete” links are wired up to a simple function that removes the parent div.

The AddGift action simply renders the user control using the specified index:

public ViewResult AddGift(int index)
{
    ViewData["index"] = index;
    return View("~/Views/Home/GiftLineItem.ascx");
}

Notice we have not specified a model for the user control, which means it will be null.  As a result, the name and price textboxes will be blank, which is what we want.  And by specifying the desired index, these textboxes will be named such that they will bind correctly when form is saved.

Save Changes

Clicking the “Save” button posts the form to the following action:

[AcceptPost]
public ViewResult Index([Deserialize]IList<Gift> gifts)
{
    //code to save the changes
    ViewData.Model = gifts;
    return View();
}

Notice that the parameter “gifts” is decorated with the Deserialize attribute. This attribute uses MvcContrib’s NameValueDeserializer to obtain our collection of gifts from the form post.  It does not require that collections be indexed purely in sequence. Thus it handles any holes left by deleted line items.

(NOTE: I expected that using [Bind(Prefix = "")] would work the same as [Deserialize], but it does not.  Seems to be a limitation.)

Conclusion

The solution presented here differs from Steve’s in that it uses FluentHtml. There are a few other differences, such as jQuery versus MS Ajax.  My point is not present a solution that is superior, rather just to show how it might be done using  FluentHtml and expressions to avoid some fiddly construction of HTML element names in views.

About these ads

8 Comments »

  1. Hi Tim! That’s really cool – nice one. A couple of questions about how you might take this even further:

    * You’re using MVCContrib’s NameValueDeserializer. Could you change it to use ASP.NET MVC’s native model binder to deserialize the incoming collection, so that you can automatically report on binding validation errors using ModelState and Html.ValidationMessage()? Otherwise, how do you handle the case when somebody enters “blah” for a price? I’m wondering whether you might find that positional indices don’t really work because, as I pointed out previously, the positional indices can get out of sync with the entities they previously referred to (unless MVCContrib has a different way of dealing with this).

    * What about editing a hierarchy? If each gift could have a collection of sub-gifts (and so on, recursively), would the strategy of passing into partials collections plus indices still work?

    Comment by Steve — January 5, 2009 @ 10:56 am

  2. @Steve

    I have not yet been using the default model binder because IMO it’s not quite ready for prime time. One thing that it does not do is bind collections on the model for a strongly typed view. According to this article (http://weblogs.asp.net/scottgu/archive/2008/10/16/asp-net-mvc-beta-released.aspx) to bind to a strongly typed view model you can specify an empty prefix like [Bind(Prefix = "")] and it will correctly deserialize the model from the form. However it does not work with collections. That is, if I have ViewPage<IList<Gifts>>, it will not bind inputs named like “[0].Id” to the model. Castle Binder has the same limitation, however you can work around it by employing a wrapper view model (e.g., GiftsViewModel) with propery Gifts, and it will recognize items named like “Gifts[0].Id”. However the default binder does not handle enumerable properties at all.

    Hopefully these these limitations will be addressed in the next release. In the meantime, I think it will be a good idea to enhance NameValueDeserializer to work with ModelState. I will plan to look into that.

    Regarding positional indexes getting out of sync, as I mention in the article, this is not an issue using NameValueDeserializer. It is not bothered by non-contiguous indexes. You can see this by trying out the code.

    Regarding hierarchy editing, it should work fine. That is, “this.TextBox(x => x.Gifts[i].SubGifts[j])” should result in an element named like “Gifts[2].SubGifts[4]“. Based on this I should expect a binder to correctly instantiate and hydrate my model object.

    Comment by Tim Scott — January 5, 2009 @ 3:50 pm

  3. Hi there

    > However the default binder does not handle enumerable properties at all.
    It certainly does! That’s exactly how my version of the gift list editor works. Check out my source code to see, or download the MVC Framework source code and read DefaultModelBinder.cs.

    > Regarding positional indexes getting out of sync, as I mention in the article, this is
    > not an issue using NameValueDeserializer. It is not bothered by non-contiguous indexes.
    > You can see this by trying out the code.
    Sorry for not being clear about this. Honestly, I understand what you are saying – the potential problem I keep trying to point out is something different. The potential problem occurs when you’re using the MVC Framework’s convention surrounding model binding and storing validation or binding errors in ModelState. I appreciate that your code does not follow that convention, which is why you do not see the problem. This is why I asked how you would deal with parsing errors, such as if somebody entered “blah” for a price. The default model binding conventions automatically give a way to display feedback messages to the user, but this won’t work properly with positional indexes. (It’s nothing to do with whether the indexes are contiguous.)

    Comment by Steve — January 6, 2009 @ 8:37 am

  4. @Steve

    In your example enumerables are deserialized properly because you use a prefix. Try it without a prefix (as when your view is strongly typed to a model with a collection property).

    I now see what you mean about the indexing. I’ll have a look at how I might handle that.

    Comment by Tim Scott — January 6, 2009 @ 2:27 pm

  5. > Try it without a prefix
    Aha, I see – thanks for clarifying. Yes, it doesn’t support that.

    Comment by Steve — January 6, 2009 @ 4:51 pm

  6. I just posted my take on doing variable list binding here:

    http://justsimplecode.com/archive/2009/01/07/my-take-on-variable-length-lists-in-asp.net-mvc.aspx

    please let me know what you think, thanks.

    Comment by justin — January 7, 2009 @ 6:18 am

  7. [...] – Take 2 Filed under: MS MVC, Uncategorized — Tim Scott @ 11:48 pm In a previous post, I showed how to edit a variable length list using MvcContrib.FluentHtml.  The post was inspired [...]

    Pingback by Editing a Variable Length List Of Items With MvcContrib.FluentHtml - Take 2 « Lunablog — January 13, 2009 @ 11:48 pm

  8. @Steve

    I think the following addresses your comments. http://lunaverse.wordpress.com/2009/01/13/editing-a-variable-length-list-of-items-with-mvccontribfluenthtml-take-2/

    Comment by Tim Scott — January 16, 2009 @ 2:09 pm


RSS feed for comments on this post. TrackBack URI

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

The Shocking Blue Green Theme. Blog at WordPress.com.

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: