Model binding with nested child models and PartialViews in ASP.NET MVC
Asked Answered
V

1

9

I have the following types and classes:

namespace MVC.Models

public class Page 
{
   public EditableContent Content {get; set; }
}

public class EditableContent
{
    public TemplateSection SidebarLeft {get; set; }
    public TemplateSection SidebarRight {get; set; }
}

I want to edit the Page instance in my Edit.aspx View. Because EditableContent is also attached to other models, I have a PartialView called ContentEditor.ascx that is strongly typed and takes an instance of EditableContent and renders it.

The rendering part all works fine, but when I post - everything inside my ContentEditor is not binded - which means that Page.Content is null.

On the PartialView, I use strongly typed Html Helpers to do this:

<%= Html.HiddenFor(m => m.TemplateId) %>

But because the input elements on the form that are rendered by ContentEditor.ascx does not get the Content prefix to its id attribute - the values are not binded to Page.

I tried using loosely typed helpers to overcome this:

<%= Html.Hidden("Content.TemplateId", Model.TemplateId) %>

And when I'm dealing with a property that is a List<T> of something it gets very ugly. I then have to render collection indexes manually.

Should I put both Page and EditableContent as parameters to the controller action?:

public ActionResult Edit(Page page, EditableContent content) { ... }

What am I missing?

Volgograd answered 17/3, 2010 at 13:24 Comment(0)
C
19

I would suggest you to use the EditorFor helper

Model:

public class EditableContent
{
    public string SidebarLeft { get; set; }
    public string SidebarRight { get; set; }
}

public class Page
{
    public EditableContent Content { get; set; }
}

Views/Home/Index.aspx:

<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<ToDD.Models.Page>" %>

<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
    Home Page
</asp:Content>

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
<% using (Html.BeginForm()) { %>
    <%-- 
    This is the important part: It will look for 
    Views/Shared/EditorTemplates/EditableContent.ascx
    and render it. You could also specify a prefix
    --%>
    <%= Html.EditorFor(page => page.Content, "Content") %>
    <input type="submit" value="create" />
<% } %>
</asp:Content>

Views/Shared/EditorTemplates/EditableContent.ascx:

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

<%= Html.TextBoxFor(m => m.SidebarLeft) %>
<br/>
<%= Html.TextBoxFor(m => m.SidebarRight) %>

And finally Controller/HomeController:

public class HomeController : Controller
{
    public ActionResult Edit()
    {
        var page = new Page
        {
            Content = new EditableContent
            {
                SidebarLeft = "left",
                SidebarRight = "right"
            }
        };
        return View(page);
    }

    [HttpPost]
    public ActionResult Edit(Page page)
    {
        return View(page);
    }
}
Coquelicot answered 17/3, 2010 at 14:8 Comment(3)
It works, dude! Thanks. I assume that the EditorFor takes care of all the scaffolding to tie everything together.Volgograd
I have done this on a project of mine, but can you explain why Html.RenderPartial("_blah" , Model.Blah) doesn't work Darin? As that's what we previously had.Guitarfish
@GONeale, it doesn't work because you pass Model.Blah, so inside the partial you are in the context of Blah, not in the context of Model => all helpers that you use in this partial generate wrong input names. Editor templates are different => they take into account the parent context and generate helpers that are used inside them will generate proper input names. Just look at your markup and you will immediately see the difference. For the default model binder to work you must respect the conventions.Coquelicot

© 2022 - 2024 — McMap. All rights reserved.