28

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?

5 Answers 5

45

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")
Sign up to request clarification or add additional context in comments.

12 Comments

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.
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 :)
@BlairHolmes, Added a html helper method to make this a little easier
@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)
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.
|
11

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.

Comments

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.

1 Comment

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.
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!

Comments

0

I bumped onto this issue and solution but I was using dotnet core. So I've used David Carek's solution and converted it to it's dotnet core variant:

public static class HtmlHelperExtensions
{
    public static Task<IHtmlContent> PartialForAsync<TModel, TProperty>(
        this IHtmlHelper<TModel> htmlHelper,
        Expression<Func<TModel, TProperty>> expression,
        string partialViewName
    )
    {
        if (htmlHelper == null)
        {
            throw new ArgumentNullException(nameof(htmlHelper));
        }

        if (expression == null)
        {
            throw new ArgumentNullException(nameof(expression));
        }

        if (partialViewName == null)
        {
            throw new ArgumentNullException(nameof(partialViewName));
        }

        string name = htmlHelper.GetExpressionText(expression);
        object model = htmlHelper.GetModelExpression(expression).Model;
        var viewData = new ViewDataDictionary(htmlHelper.ViewData)
        {
            TemplateInfo =
            {
                HtmlFieldPrefix = string.IsNullOrEmpty(htmlHelper.ViewData.TemplateInfo.HtmlFieldPrefix)
                    ? name
                    : $"{htmlHelper.ViewData.TemplateInfo.HtmlFieldPrefix}{(name.StartsWith('[') ? "" : ".")}{name}"
            }
        };

        return htmlHelper.PartialAsync(partialViewName, model, viewData);
    }

    public static string GetExpressionText<TModel, TResult>(
        this IHtmlHelper<TModel> htmlHelper,
        Expression<Func<TModel, TResult>> expression)
    {
        var expressionProvider = htmlHelper.ViewContext.HttpContext.RequestServices
            .GetService(typeof(ModelExpressionProvider)) as ModelExpressionProvider;

        if (expressionProvider == null)
        {
            throw new InvalidOperationException("Unable to retrieve ModelExpressionProvider from DI");
        }

        return expressionProvider.GetExpressionText(expression);
    }

    public static ModelExpression GetModelExpression<TModel, TResult>(
        this IHtmlHelper<TModel> htmlHelper,
        Expression<Func<TModel, TResult>> expression)
    {
        var expressionProvider = htmlHelper.ViewContext.HttpContext.RequestServices
            .GetService(typeof(ModelExpressionProvider)) as ModelExpressionProvider;

        if (expressionProvider == null)
        {
            throw new InvalidOperationException("Unable to retrieve ModelExpressionProvider from DI");
        }

        return expressionProvider.CreateModelExpression(htmlHelper.ViewData, expression);
    }
}

Usage also adapts a bit:

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

I've also added a small adaptation where there's no added . in case your model expression starts with a [ to be able to cope with lists (e.g. list[1]).

If anyone knows a better way of reaching the same result in dotnet core, feel free to react.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.