ASP.NET MVC - Model binding to a list OR How to bring your “datagrid” into full edit mode?

MVC is awesome. I love the nice and clean HTML output, and the nice separation of concerns!!
Not to mention how easy we can test the controllers, with Moq and MvcContrib…

“How can we bring a list of items into full edit mode like in the image below?“
         I had this request from a client a couple of months ago…
image 
Disclaimer: I am not saying this is a good idea (from a UX perspective), I am just showing how you can do it…

image
Figure: A list of items in Full edit mode

 

In this nice blog post about Model binding to a List, Kristof Mattei explains how to do this.

 
The important part here is the naming of the variables: “globalMessages”

How can we implement and extend this?

 

We have 2 forms that post to different actions: “Save” and “Add”

I removed some things and kept only the relevant pieces

<%=Html.ValidationMessage("GlobalErrorMessage") %>
<%using (Html.BeginForm("Save", "GlobalMessages", FormMethod.Post)) {%> <table> <% int i = 0; foreach (var item in Model) { %> <tr> <td> <%: Html.ActionLink("Delete", "Delete", new { id=item.GlobalMessageId })%> </td> <td> <%: Html.Hidden("globalMessages[" + i +"].GlobalMessageId",item.GlobalMessageId ) %> <input type="text" name="globalMessages[<%=i %>].MessageOrder" value="<%: item.MessageOrder %>" maxlength="3" size="3" /> <%=Html.ValidationMessage("globalMessages[" + i + "].MessageOrder")%> </td> <td> <input type="text" name="globalMessages[<%=i %>].Message" value="<%: item.Message %>" maxlength="100" size="100" /> <%=Html.ValidationMessage("globalMessages[" + i+ "].Message")%> </td> </tr> <% i++; } %> </table>
<input id="Submit1" type="submit" value="Save" runat="server" /> <%} %>

 

<%=Html.ValidationMessage("GlobalErrorMessage") %> <%using (Html.BeginForm("Add", "GlobalMessages", FormMethod.Post)) {%> <fieldset> <legend>Add new message</legend> <div class="display-label">Order</div> <div class="display-field"> <%=Html.TextBox("MessageOrder", ViewData["NextMaxValue"], new { maxlength="3", size="3"})%> <%=Html.ValidationMessage("MessageOrder")%> </div> <div class="display-label"> Message</div> <div class="display-field"> <%=Html.TextBox("Message", "", new { maxlength = "100", size = "100" })%> <%=Html.ValidationMessage("Message")%> </div> <input type="submit" value="Add" runat="server" id="add" /> </fieldset> <%} %>

The important one is the “Save” form (the top one), the other one is out of the box MVC functionality (if you can call it like that)

 

The parameter of my Save methods is

   [HttpPost]
   public ActionResult Save(List<GlobalMessage> globalMessages)
        

 

The rendered HTML source code is

<tr>
    <td>
        <a href="/MyWeb/GlobalMessages/Delete/42">Delete</a>

    </td>
    <td>
        <input id="globalMessages_0__GlobalMessageId" name="globalMessages[0].GlobalMessageId" type="hidden" value="42" />
        <input type="text" name="globalMessages[0].MessageOrder" value="0"
            maxlength="3" size="3" />
        
    </td>
    <td>
        <input type="text" name="globalMessages[0].Message" value="XXX"
            maxlength="100" size="100" />
        
    </td>
</tr>

The name of my input fields is globalMessages[X] and the MVC ModelBinder is automagically wrapping those in a list.
Sweeeeet!!

If that doesn’t work because of a validation error (not a number, text too long, etc…), the ModelBinder sets the ModelState.IsValid to false, and will display a nice error message.

So, make sure to test the ModelState before you submit, like below

 

        [HttpPost]
        public ActionResult Save(List<GlobalMessage> globalMessages)
        {
            
            if (ModelState.IsValid)
            {
                _dbHelper.SaveUpdateGlobalMessages(globalMessages);
                return RedirectToAction("Index");
            }
            else
            {
                // Don't redirect to Index, otherwise the error messages get lost
                return View("Index", GetGlobalMessages());
            }
        }
Figure: Don’t forget to check the ModelState

 

Next steps

1. Annotate your business objects with Data Annotations
Data Annotations are the future on the .NET platform. We MUST use them. And they work in all technologies: Silverlight, RIA services, MVC, WPF, …

2. Enable Client Validation
http://hadihariri.com/blogengine/post/2009/10/06/Client-Side-Validation-in-MVC-20.aspx

3. Make sure to have paging in place, if you have more than 50 items in the list!!

4. Have fun!
image


PS: If you have problems with passing/binding DateTime’s, because you want to display them in Australian or Italian format, check this blog post about ModelBinder and Localization

 

References:

1
Tips for model binding in ASP.NET MVC and how to write your own custom model binder (MUST READ!)
http://odetocode.com/blogs/scott/archive/2009/04/27/6-tips-for-asp-net-mvc-model-binding.aspx
Check tip #5: Binding and Validation are 2 different things

2
More on ModelBinders
http://www.singingeels.com/Articles/Model_Binders_in_ASPNET_MVC.aspx

3
ScottH has a nice ModelBinder for getting the current user as IPrincipal
http://www.hanselman.com/blog/IPrincipalUserModelBinderInASPNETMVCForEasierTesting.aspx

Latest Posts

Popular Posts