getting the values from a nested complex object that is passed to a partial view

Blair Holmes picture Blair Holmes · Apr 22, 2015 · Viewed 8.8k times · Source

I have a ViewModel that has a complex object as one of its members. The complex object has 4 properties (all strings). I'm trying to create a re-usable partial view where I can pass in the complex object and have it generate the html with html helpers for its properties. That's all working great. However, when I submit the form, the model binder isn't mapping the values back to the ViewModel's member so I don't get anything back on the server side. How can I read the values a user types into the html helpers for the complex object.

ViewModel

public class MyViewModel
{
     public string SomeProperty { get; set; }
     public MyComplexModel ComplexModel { get; set; }
}

MyComplexModel

public class MyComplexModel
{
     public int id { get; set; }
     public string Name { get; set; }
     public string Address { get; set; }
     ....
}

Controller

public class MyController : Controller
{
     public ActionResult Index()
     {
          MyViewModel model = new MyViewModel();
          model.ComplexModel = new MyComplexModel();
          model.ComplexModel.id = 15;
          return View(model);
     }

     [HttpPost]
     public ActionResult Index(MyViewModel model)
     {
          // model here never has my nested model populated in the partial view
          return View(model);
     }
}

View

@using(Html.BeginForm("Index", "MyController", FormMethod.Post))
{
     ....
     @Html.Partial("MyPartialView", Model.ComplexModel)
}

Partial View

@model my.path.to.namespace.MyComplexModel
@Html.TextBoxFor(m => m.Name)
...

how can I bind this data on form submission so that the parent model contains the data entered on the web form from the partial view?

thanks

EDIT: I've figured out that I need to prepend "ComplexModel." to all of my control's names in the partial view (textboxes) so that it maps to the nested object, but I can't pass the ViewModel type to the partial view to get that extra layer because it needs to be generic to accept several ViewModel types. I could just rewrite the name attribute with javascript, but that seems overly ghetto to me. How else can I do this?

EDIT 2: I can statically set the name attribute with new { Name="ComplexModel.Name" } so I think I'm in business unless someone has a better method?

Answer

user3559349 picture user3559349 · Apr 23, 2015

You can pass the prefix to the partial using

@Html.Partial("MyPartialView", Model.ComplexModel, 
    new ViewDataDictionary { TemplateInfo = new TemplateInfo { HtmlFieldPrefix = "ComplexModel" }})

which will perpend the prefix to you controls name attribute so that <input name="Name" ../> will become <input name="ComplexModel.Name" ../> and correctly bind to typeof MyViewModel on post back

Edit

To make it a little easier, you can encapsulate this in a html helper

public static MvcHtmlString PartialFor<TModel, TProperty>(this HtmlHelper<TModel> helper, Expression<Func<TModel, TProperty>> expression, string partialViewName)
{
  string name = ExpressionHelper.GetExpressionText(expression);
  object model = ModelMetadata.FromLambdaExpression(expression, helper.ViewData).Model;
  var viewData = new ViewDataDictionary(helper.ViewData)
  {
    TemplateInfo = new System.Web.Mvc.TemplateInfo 
    { 
      HtmlFieldPrefix = string.IsNullOrEmpty(helper.ViewData.TemplateInfo.HtmlFieldPrefix) ? 
        name : $"{helper.ViewData.TemplateInfo.HtmlFieldPrefix}.{name}"
    }
  };
  return helper.Partial(partialViewName, model, viewData);
}

and use it as

@Html.PartialFor(m => m.ComplexModel, "MyPartialView")