views:

805

answers:

3

I have a view model with a collection of other objects in it.

public ParentViewModel
{
    public int Id { get; set; }
    public string Name { get; set; }
    public List<ChildViewModel> Child { get; set; } 
}

public ChildViewModel
{
    public int Id { get; set; }
    public string FirstName { get; set; }
}

In one of my views I pass in a ParentViewModel as the model, and then use

<%: Html.EditorFor(x => x) %>

Which display a form for the Id and Name properties.

When the user clicks a button I call an action via Ajax to load in a partial view which takes a collection of Child:

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<IEnumerable<Child>>" %>
<%: Html.EditorFor(x => x) %>

which then uses the custom template Child to display a form for each Child passed in.

The problem I'm having is that the form created by the Child custom template does not use the naming conventions used by the DefaultModelBinder.

ie the field name is (when loaded by Ajax):

[0].FirstName

instead of:

Child[0].FirstName

So the Edit action in my controller:

[HttpPost]
public virtual ActionResult Edit(int id, FormCollection formValues)
{
    ParentViewModel parent = new ParentViewModel();
    UpdateModel(parent);

    return View(parent);
}

to recreate a ParentViewModel from the submitted form does not work.

I'm wondering what the best way to accomplish loading in Custom Templates via Ajax and then being able to use UpdateModel is.

+2  A: 

Hmm... i personally would recommend to use a JSON result, instead of a HTML result, that you fiddle in the page...

makes the system cleaner. and your postback working ;-)

cRichter
I'm not sure how a JSON result would help? I need the response that gets loaded into the page to be a Form not just data. And the Form needs to have the correct naming convention so that the model binding works when posting back to the server.
Chris
you could get the result per AJAX, and than replace the data in the form with the results...
cRichter
+4  A: 

Couple of things to start with is that you need to remember the default ModelBinder is recursive and it will try and work out what it needs to do ... so quite clever. The other thing to remember is you don't need to use the html helpers, actual html works fine as well :-)

So, first with the Model, nothing different here ..

public class ParentViewModel
{
    public int Id { get; set; }
    public string Name { get; set; }
    public List<ChildViewModel> Child { get; set; }
}

public class ChildViewModel
{
    public int Id { get; set; }
    public string FirstName { get; set; }
}

Parent partial view - this takes an instance of the ParentViewModel

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<ParentViewModel>" %>

<h2>Parent</h2>
<%: Html.TextBox("parent.Name", Model.Name) %>
<%: Html.Hidden("parent.Id", Model.Id) %>

<% foreach (ChildViewModel childViewModel in Model.Child)
{
    Html.RenderPartial("Child", childViewModel);         
}
%>

Child partial view - this takes a single instance of the ChildViewModel

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<ChildViewModel>" %>

<h3>Child</h3>
<%: Html.Hidden("parent.Child.index", Model.Id) %>
<%: Html.Hidden(string.Format("parent.Child[{0}].Id", Model.Id), Model.Id)%>
<%: Html.TextBox(string.Format("parent.Child[{0}].FirstName", Model.Id), Model.FirstName) %>

Something to note at this point is that the index value is what is used for working out the unique record in the list. This does not need to be incremental value.

So, how do you call this? Well in the Index action which is going to display the data it needs to be passed in. I have setup some demo data and returned it in the ViewData dictionary to the index view.

So controller action ...

public ActionResult Index()
{
    ViewData["Message"] = "Welcome to ASP.NET MVC!";

    ViewData["Parent"] = GetData();

    return View();
}

private ParentViewModel GetData()
{
    var result = new ParentViewModel
                     {
                         Id = 1,
                         Name = "Parent name",
                         Child = new List<ChildViewModel>
                                     {
                                         new ChildViewModel {Id = 2, FirstName = "first child"},
                                         new ChildViewModel {Id = 3, FirstName = "second child"}
                                     }
                     };
    return result;
}

In the real world you would call a data service etc.

And finally the contents of the Index view:

<form action="<%: Url.Action("Edit") %>" method="post">
    <% if (ViewData["Parent"] != null)  { %>

        <%
            Html.RenderPartial("Parent", ViewData["Parent"]); %>

    <% } %>
    <input type="submit" />
</form>

Saving

So now we have the data displayed how do we get it back into an action? Well this is something which the default model binder will do for you on simple data types in relatively complex formations. So you can setup the basic format of the action which you want to post to as:

[HttpPost]
public ActionResult Edit(ParentViewModel parent)
{

}

This will give you the updated details with the original ids (from the hidden fields) so you can update/edit as required.

New children through Ajax

You mentioned in your question loading in custom templates via ajax, do you mean how to give the user an option of adding in another child without postback?

If so, you do something like this ...

Add action - Need an action which will return a new ChildViewModel

[HttpPost]
public ActionResult Add()
    {
        var result = new ChildViewModel();
        result.Id = 4;
        result.FirstName = "** to update **";
        return View("Child", result);
    }

I've given it an id for easy of demo purposes.

You then need a way of calling the code, so the only view you need to update is the main Index view. This will include the javascript to get the action result, the link to call the code and a target HTML tag for the html to be appended to. Also don't forget to add your reference to jQuery in the master page or at the top of the view.

Index view - updated!

<script type="text/javascript">

   function add() {

        $.ajax(
            {
                type: "POST",
                url: "<%: Url.Action("Add", "Home") %>",
                success: function(result) {
                    $('#newchild').after(result);
                },
                error: function(req, status, error) {

                }
        });
    }

</script>

<form action="<%: Url.Action("Edit") %>" method="post">
    <% if (ViewData["Parent"] != null)  { %>

        <%
            Html.RenderPartial("Parent", ViewData["Parent"]); %>

    <% } %>
    <div id="newchild"></div>

    <br /><br />
    <input type="submit" /> <a href="#" onclick="JavaScript:return add();">add child</a>
</form>

This will call the add action, and append the response when it returns to the newChild div above the submit button.

I hope the long post is useful.

Enjoy :-)

WestDiscGolf
@WestDiscGolf your long post is very useful and much appreciated. Looks like it's exactly what I needed.
Chris
@Chris - Awesome, glad its useful :-)
WestDiscGolf
+1  A: 

I found another way to accomplish this which works in my particular situation.

Instead of loading in a partial via via Ajax that is strongly typed to a child collection like:

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<IEnumerable<Child>>" %>

I created a strongly typed view to the parent type and then called EditorFor on the list like so:

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<Parent>" %>
<%: Html.EditorFor(x => x.ChildList) %>

This then calls a Custom Display Template and the result is that all the HTML elements get named correctly and the Default Model binder can put everything back together.

Chris