Tim Scott's Blog

January 13, 2009

Editing a Variable Length List Of Items With MvcContrib.FluentHtml – Take 2

Filed under: FluentHtml, MS MVC — 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 by a post on the same topic by Steve Sanderson.  Steve commented on my post pointing out some limitations.  To address these limitations required some enhancements to MvcContrib.FluentHtml and some changes to my example.  So here we go with take 2.

MvcContrib.FluentHtml Enhancements

The limitations were mostly related to handling validation.  To support validation using Model State we have added to MvcContrib.FluentHtml:

  • Html Prefix Support
  • A Validation Helper
  • List Indexing Support

Html Prefix Support

HTML elements generated for strongly typed views have no prefix by default.  So elements for ModelViewPage<Person> might look something like this:

<input name="FirstName" id="FirstName" value="Jim" type="text"/>
<input name="LastName" id="LastName" value="Smith" type="text"/>

The problem is, neither MvcContrib’s NameValueSerializer nor MS MVC’s default model binder fully handle this scenario.   While NameValueDeserializer does the binding just fine, it does not place bind errors into ModelState.  The default model binder does not handle quite handle the binding — it does not deserialize enumerable properties when no prefix is used.

To bind everything correctly and get any errors into ModelState we need a prefix.  One way to handle this is to wrap our primary model in a view model. This is a valid pattern. It’s even a standard for some applications. However, we don’t want to have to do this just to get a prefix. So we provide a way to specify a prefix for a strongly typed view.

Either in the code-behind:

public class Index : ModelViewPage<IList<Gift>>
{
    public Index() : base("person") { }
}

Or in the view:

<%this.HtmlNamePrefix = "person"%>

Thus our HTML elements will be prefixed:

<input name="person.FirstName" id="person_FirstName" value="Jim" type="text"/>
<input name="person.LastName" id="person_LastName" value="Smith" type="text"/>

Then in our action method, we can use the default binder without any attribute.  Any bind errors will be captured in ModelState.

[AcceptPost]
public ViewResult Index(Person person)

New Validation Helper

We have added validation support to FluentHtml for strongly typed views. The following works basically the same as HtmlHelper’s ValidationMessage.

<%this.ValidationMessage(x => x.Price, "Price must be numeric.")%>

We have also added a behavior to ModelViewPage<T>, ModelViewMasterPage<T> and ModelViewUserControl<T> which basically mimics validation used by HtmlHelper.  That is, it adds a CSS class “input-validation-error” to any HTML element with an error in ModelState.  If you wish, you can remove this behavior or change the CSS class name in the derived class.

List Indexing Support

MS MVC uses a special technique to deserialize enumerable properties.  With this technique you set an arbitrary value in a specially named hidden element to signify that a group of elements belongs to a particular instance of the enumerable property. This has an advantage over using positional indexes in that it preserves the identity of an instance across posts, which is necessary for ModelState based validation to work properly.

Therefore, we have added support for this to MvcContrib.FluentHtml.  Let’s assume ModelViewPage<IList<Person>> with a prefix of “persons”.  Then this markup:

<%var id = Model[i].Id;%>
<%=this.Index(x => x).Value(id)%>
<%=this.TextBox(x => x[id].FirstName).Value(Model[i].FirstName)%>

Will generate HTML like this:

<input id="persons_Index_123" name="persons.Index" value="123" type="hidden"/>
<input id="persons[123]_FirstName" name="persons[123].FirstName" value="Jim" type="text"/>

Changes To Our Example

So based on these changes to FluentHtml we made a few changes to the “Gift Request Form” demo from the previous post. In the main view:

<%for (var i = 0; i < ViewData.Model.Count; i++) {%>
    <%ViewData["index"] = i;
    Html.RenderPartial("GiftLineItem");%>
<%}%>

And in the user control:

<%var i = (int)ViewData["index"];
  var gift = ViewModel == null ? null : ViewModel[i];
  var id = gift == null ? -1 * new Random().Next() : gift.Id;
  var name = gift == null ? null : gift.Name;
  var price = gift == null ? (decimal?)null : gift.Price;%>
<div class="giftLineItem">
    <%=this.Index(x => x, x => x[i].Id)%>
    <%=this.Hidden(x => x[id].Id).Value(id)%>
    <%=this.TextBox(x => x[id].Name).Value(name).Label("Name of gift:")%>
    <%=this.TextBox(x => x[id].Price).Value(price).Label("Price ($):")%>
    <%=this.ValidationMessage(x => x[id].Price, "Must be a number")%>
    <a href="" class="removeGift">Delete</a>
</div>

Viola, validation works:

showvalidationerror1

Note that we stole Steve’s technique of using random negative Ids for unsaved instances, which we will handle in the save method on the server.

GET THE REVISED CODE
GET THE REVISED CODE, UPDATED FOR MVC 2, SIMPLIFIED, AND USING GUID IDs (2/15/10)

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.

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

Follow

Get every new post delivered to your Inbox.