Saturday, 5 May 2012

How to use list box in MVC

Introduction

In many ways, ASP.NET MVC represents a big step forward from Web Forms. Instead of working with an abstraction that tries to impose a non-web model onto a web development framework, ASP.NET MVC embraces the HTTP model and presents developers with a way of working that is much more in-tune with how the web actually works. This is liberating, but for developers only familiar with the Web Forms model, it can be a bit intimidating as well. To work effectively in MVC, we need to have a solid understanding of how HTML forms work, this is something that Web Forms never really required or encouraged (though it was always desirable). In this article, I'll be looking at creating a classic UI construct in MVC: indicating selection by moving items from one list box to another.

Pre-requisites

You'll need at least Visual Studio 2008 (or the equivalent Express edition) with MVC version 2 installed. I'm assuming a reasonable level of familiarity with ASP.NET development and how MVC projects are structured.

Getting started

Getting in the spirit of the season, we are going to build a Christmas gift selector. Of course, if you aren't reading this in December, it's a bit less topical. Our Christmas-themed UI is shown below:
ListBoxDemo.png
Look at all that snow! Doesn't it make you feel festive? Needless to say, I've left the styling as an exercise for the reader. The two list-box scenario is an interesting one; it's fairly commonly used, and is very easy to implement in Web Forms, but it's actually not a very natural fit for HTML forms. To understand why, let's delve a little into how the list box works; once we understand this, implementing our user interface will be much easier.
The (slightly simplified) HTML source for one of the list boxes shown above looks like this:
<select multiple="multiple" name="AvailableSelected" size="6">
    <option value="1">Games Console</option>
    <option value="2">MP3 player</option>
    <option value="3">Smart Phone</option>
    <option value="4">Digital Photo frame</option>
    <option value="5">E-book reader</option>
    <option value="6">DVD Box Set</option>
</select>
The select element represents the list itself; the options represent the items in the list. Each option has a value attribute specified, this determines the value that will be sent back in the form data when an item is selected. Let's say a user selects the Games Console, Digital Photo Frame, and DVD Box Set. The following data gets posted back to the server: AvailableSelected=1&AvailableSelected=4&AvailableSelected=6. 'AvailableSelected' comes from the name given to the select element; 1, 4, and 6 refer to the IDs of the selected products. Notice that if an item is not selected, then no information is sent about it. This presents a problem for our two-list scenario: we are not so much interested in what items are selected but the contents of each list. We will therefore need to find another way of tracking what items the user has selected. As it turns out, this is not terribly difficult, but before we move on, let's spend a moment thinking about how this would work in a Web Forms scenario.

Web Forms ListBox

With a Web Forms ListBox, the items stored in the listbox are available to the server-side code after a form post. For our scenario, this would be very useful. We know that no information is sent in the form data, so how does this work? The ListBox server control automatically saves all of its contents into the Web Form's ViewState hidden field. When the form is posted back, it recreates its contents from this data, then inspects the form data to see if any of its items were selected or not. Without any extra work on the part of the developer, state is automatically maintained across form posts; this is great, right? Well, yes and no. This method works really well as long as you stay within the confines of the Web Forms methodology. Try to step outside this though, and you start hitting problems. For example, try manipulating the list items through JavaScript, and you'll find the changes are not reflected in your server code. Worse, you'll probably find your app will start throwing exceptions unless you adjust the ViewState security settings. What should be a simple modification turns into a huge mess. And if you don't understand the underlying mechanisms, it can be very difficult to fix the problem; I've encountered ASP.NET developers genuinely baffled as to why changes they make in JavaScript aren't visible in the server code.

Using the Code

Now that we understand a bit of theory, let's have a look at some of the key parts of the sample code The Product that represents the gifts the user can choose is shown below:
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public Decimal Price { get; set; }
}
We'll also create a View Model that encapsulates the data going to / from the View. To fill the two list boxes, we'll need two lists of product data: one for the available list, and one for the requested list. We'll need to get back which items in the lists are selected when the user moves items from one box to another. Thinking back to our explanation earlier, the list box data is represented as repeated key / value pairs, with the name of the list box as the key and each selected ID as a value. Fortunately, we don't have to decode this data manually, ASP.NET MVC's model binding facility can do this for us. We will store the data as an array of ints, which will be populated for us from the form data. We'll also need somewhere to save the state between form posts; all we need to remember is what product IDs are in the requested list box. We'll do that by saving a comma delimited list in a string.
The relevant parts of the ViewModel class are shown below:
public class ViewModel
{
    public List<Product> AvailableProducts { get; set; }
    public List<Product> RequestedProducts { get; set; }    

    public int[] AvailableSelected { get; set; }
    public int[] RequestedSelected { get; set; }
    public string SavedRequested { get; set; }
}
The list box part of the View is shown below:
<%using(Html.BeginForm()){ %>
    <div>
        <table>
            <thead>
                <tr>
                    <th>Available</th><th>
                    </th><th>Requested</th>
                </tr>
            </thead>
            <tbody>
                <tr>
                    <td valign="top">
                        <%=Html.ListBoxFor(model => model.AvailableSelected, 
                            new MultiSelectList(Model.AvailableProducts, "Id", 
                            "Name", Model.AvailableSelected), 
                            new { size = "6" })%>
                    </td>
                    <td valign="top">
                        <input type="submit" name="add" 
                          id="add" value=">>" /><br />
                        <input type="submit" name="remove" 
                          id="remove" value="<<" />
                    </td>
                    <td valign="top">
                        <%=Html.ListBoxFor(model => model.RequestedSelected, 
                          new MultiSelectList(Model.RequestedProducts, "Id", 
                          "Name", Model.RequestedSelected))%>
                    </td>
                </tr>
            </tbody>
        </table>
        <br />
        <%=Html.HiddenFor(model=>model.SavedRequested) %>
    </div>
<%} %>
Note the Html.ListBoxFor format; the first parameter sets the property of the model to use (one of the arrays); the second parameter creates a MultiSelectList, which is a collection of objects similar to the ListItem class in Web Forms - each item has a value, text, and selected property. The constructor sets the collection to use as the list, and the properties to use as the value and text fields, as well as an IEnumerable representing the values that should be selected to which we pass one of our arrays. Because it's a MultiSelectList, multiple items can be selected and moved at the same time. A hidden field stores the information that is used to store the state between form posts.
The controller code that receives the form values is shown below:
[HttpPost]
public ActionResult Index(ViewModel model, string add, string remove)
{
    //Need to clear model state or it will interfere with the updated model
    ModelState.Clear();
    RestoreSavedState(model);
    if (!string.IsNullOrEmpty(add))
        AddProducts(model);
    else if (!string.IsNullOrEmpty(remove))
        RemoveProducts(model);
    SaveState(model);
    return View(model);
}
Thanks to the magic of model binding, the method takes a parameter of type ViewModel. ASP.NET MVC will automatically parse the incoming form values and match up what it can. This means the arrays will have values (if any items were selected), as will the SavedRequested field, but the Lists of Products will be null.
The string parameters represent the two buttons. When a submit button is pressed, its name and value (text) are included in the form values. By checking which of the string values isn't empty, we can determine which button was pressed. The RestoreSavedState method gets the saved product IDs from the SavedRequested field and uses this to recreate the list of requested products from the previous roundtrip. The SaveState method takes the current state and stores it in the string for saving in the hidden field. With that, the list box functionality is complete, and items can be moved freely between them. The downloadable sample contains the full application, including some simple validation and a details list for the requested items.

No comments:

Post a Comment