views:

96

answers:

2

My view model has the property:

public IEnumerable<string> Names { get; set; }

My view displays that information using a simple markup that renders to:

<div id="names">
    <ul>
        <li>Name1<img src="/delete.png"/></li>
        <li>Name2<img src="/delete.png"/></li>
        <li>Name3<img src="/delete.png"/></li>
    </ul>
</div>

The image on each name allows you to delete the current name using some jQuery

$('#names li img').click(function () {
    $(this).closest('li').remove();
});

I was hoping there would be some way to have my changes to the list from jQuery be reflected on my model. Could this behavior be accomplished through an MVC Editor Template?

I know I can create a hidden input with a delimited string and parse it out, but I want my view and my controller to have an easy time processing this list.

I thought if I added a name or an id to the ul element the model binder would be able to read the li elements and construct a list but that did not work.

A: 

if you can rig up your jQuery to compile a query string, such that it corresponds to an IEnumerable<string>, you can get your Model Binder to bind the query string parameters to the list.

get each <li> with jQuery and append it to the query string so it's like:

?names[0]=val1&names[1]=val2&names[2]=val3 // ...not 100% sure on this...

suppose your action method is as follows:

public ActionResult MyActionMethod(IEnumerable<string> names)
{
    // names should correspond to the items you pass in as part of the query string
}

You then have a representation of the list that exists on the page available in your action method. You can then update the Model to correspond to that list.

edit: but just to Extend this a bit further, suppose you wanted to pass in a list of YourType?

public ActionResult MyActionMethod(IEnumerable<YourType> yourtypes)
{

}

if your query string had values for the Ids of each YourType instance, you could include that in the query string also, as follows

?yourTypeId[0]=1&yourTypeId[1]=2&yourTypeId[2]=3

and your Model Binder will create instances of YourType with the ID values you pass in and add them to the list for you.

Of course, it's a little bit less than ideal building query strings up with jQuery. The more ideal way would be to serialize the list to JSON and pass a JSON object to the action method.

DaveDev
+4  A: 

Try adding a hidden input for the list item, using the name of the property (in your case "Names"), and the item's index in square brackets. This definitely works:

<div id="names">
    <ul>
    <% for (int i = 0; i < Model.Names.Count; i++ ) { string name = Model.Names.ElementAt(i); %>
        <li>
            <%: Html.Hidden("Names[" + i + "]", name) %><%: name %><img src="/delete.png"/>
        </li>
    <% } %>
    </ul>
</div>

The model binder needs some sort of context to decide how to bind complex types. By providing a hidden element with the correct name, it has enough context to decide that the item belongs in the Names property at index i. If you don't include the index, it uses a finite set of items (meaning you can't add or remove items from the list, and calling .Add() or .Remove() will throw an exception).

While I agree that the jQuery method will work, if no other part of your page is using JSON then why do a one off? The name on the hidden element is doing exactly the same thing, providing a name/value pair with its index. Only this way, the model binder does the work for you (for a small price of less-than-gorgeous view syntax). On the plus side, no javascript necessary to get this model binding properly.

If you're using IEnumerable<string> or ICollection<string>, you have to use .ElementAt(i) like the above sample. If you're using an IList<string> or string[], you can use an indexer instead.

EDIT: Solving the deletion problem

Create another view model for your strings, so you'll have a IEnumerable<NameViewModel> Names { get; set; } property on your actual view model (in place of IEnumerable<string>). NameViewModel will look like this:

public class NameViewModel
{
    public string Name { get; set; }
    public bool IsDeleted { get; set; }
}

Then, add a hidden element for IsDeleted to the <li>. Remember that the model binder requires the proper name for context. After adding a property to the indexed name, the hidden inputs will look like this:

<%: Html.Hidden("Names[" + i + "].Name", name.Name) %>
<%: Html.Hidden("Names[" + i + "].IsDeleted", name.IsDeleted) %>

In your jQuery delete code, find the hidden element and set the value to True, and call .hide() on the list item. When you post the form to the server, your action method can figure out what should be deleted like this:

var names = model.Names.Where(name => name.IsDeleted).Select(t => t.Name);
John Nelson
That's close to what I am looking for. Deleting the middle name of a set of three would leave the hidden inputs in a state where there is a missing number like: Names[0], Names[2]. When the model binds this it returns a count of 1.
Steve Hook
@Steve Hook edited answer to include solution for Deleting the middle name.
John Nelson