27

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?

Blair Holmes
  • 1,521
  • 2
  • 22
  • 35

4 Answers4

43

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")
David Carek
  • 1,103
  • 1
  • 12
  • 26
  • this looks very promising. I've left the office for the day but will give it a shot tomorrow when I get back and mark as the answer if it works for me. thanks. – Blair Holmes Apr 22 '15 at 22:41
  • boo...I can't upvote it because it says I have to have 15 reputation. +1 for you though as soon as I get the 15 rep :) – Blair Holmes Apr 23 '15 at 13:50
  • @BlairHolmes, Added a html helper method to make this a little easier –  Apr 23 '15 at 22:54
  • In my opinion This helper should be built in the Mvc framework. – Cristian E. Mar 08 '16 at 11:23
  • @Christian, What are you talking about? - it is MVC (or do you mean it should be part of the source code?) –  Mar 08 '16 at 11:24
  • Part of source code mate. Its too vital even for basic create pages. – Cristian E. Mar 08 '16 at 11:37
  • 1
    @Christian, In a way it already is, because the correct approach is to use `EditorFor()` with a custom `EditorTemplate` for nested models (and the `EditorFor()` method passes the `HtmlFieldPrefix` as per the code above). Partials were not really designed to be used in this scenario which is why I assume the MVC team did not include it) –  Mar 08 '16 at 12:21
  • 1
    The use of EditorFor is "too conventional" (strict naming convetions, folder structure) and finally is altering the output html. The second solution is to pass the parent model which also is a wrong approach because it becomes not reusable anymore. – Cristian E. Mar 08 '16 at 12:31
  • Not sure what you mean by _altering the output html_ - using an `EditorTemplate` correctly will always generate the correct html. And a lot of the MVC framework is based around conventions. –  Mar 08 '16 at 12:33
  • Well when conventions become constraints then its not a convention anymore. Also if there is a convention in place it doesn't mean that it is a good one. Eg: Simply take the default folder structure. It comes with a nearly "folder per extension type convention". – Cristian E. Mar 08 '16 at 12:38
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/105692/discussion-between-christian-and-stephen-muecke). – Cristian E. Mar 08 '16 at 12:51
  • Oh thank god, I was looking for this for hours! Thank you! – Gašper Sladič Sep 27 '18 at 18:40
10

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.

Todd
  • 12,995
  • 3
  • 30
  • 25
2

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.

Yogiraj
  • 1,952
  • 17
  • 29
  • I can't do that because it needs to be generic. There will be several parent ViewModels that have their own type that will need to be passed in...See my first edit in the OP. – Blair Holmes Apr 22 '15 at 21:53
0

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!