27
votes

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?

4

4 Answers

40
votes

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")
5
votes

If you use tag helpers, the partial tag helper accepts a for attribute, which does what you expect.

<partial name="MyPartialView" for="ComplexModel" />

Using the for attribute, rather than the typical model attribute, will cause all of the form fields within the partial to be named with the ComplexModel. prefix.

2
votes

You can try passing the ViewModel to the partial.

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

Edit

You can create a base model and push the complex model in there and pass the based model to the partial.

public class MyViewModel :BaseModel
{
    public string SomeProperty { get; set; }
}

 public class MyViewModel2 :BaseModel
{
    public string SomeProperty2 { get; set; }
}

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

Then your partial will be like below :

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

If this is not an acceptable solution, you may have to think in terms of overriding the model binder. You can read about that here.

0
votes

I came across the same situation and with the help of such informative posts changed my partial code to have prefix on generated in input elements generated by partial view

I have used Html.partial helper giving partialview name and object of ModelType and an instance of ViewDataDictionary object with Html Field Prefix to constructor of Html.partial.

This results in GET request of "xyz url" of "Main view" and rendering partial view inside it with input elements generated with prefix e.g. earlier Name="Title" now becomes Name="MySubType.Title" in respective HTML element and same for rest of the form input elements.

The problem occurred when POST request is made to "xyz url", expecting the Form which is filled in gets saved in to my database. But the MVC Modelbinder didn't bind my POSTed model data with form values filled in and also ModelState is also lost. The model in viewdata was also coming to null.

Finally I tried to update model data in Posted form using TryUppdateModel method which takes model instance and html prefix which was passed earlier to partial view,and can see now model is bound with values and model state is also present.

Please let me know if this approach is fine or bit diversified!