Search code examples
asp.net-coremodel-binding

Partial View Model Binding


I am building a web application in .Net Core 6.0. I have a main view that is referencing a partial view via the partial view html helper, but the model binding isn't working in that that argument in the controller action result always comes up empty. Snippets of my code are below. I am using third party telerik controls, but I know they are not the issue.

View Code

@model ViewModel
@using (Html.BeginForm("FillList", "Controller", new
            {

            }, FormMethod.Post, null, new { @name = "ListFilterForm", @id = "ListFilterForm" }))
            {
                <partial name="_Filter" model="Model.listTab.FilterViewModel" />
            }

@section Scripts {

    <script type="text/javascript">
        $(function() {
            $('#FilterOfficeDD').change(function() {
                document.ListFilterForm.submit();
            });
        });
    </script>

PartialView Code

@using project.Common
@using project.Models;

@model FilterViewModel

@{
    ViewBag.Title = "Filter";
}

<div class="row">
    <div class="form-group col-md-3">
        <fieldset>
            @Html.LabelFor(m => m.Filters.Active, htmlAttributes: new { @class = "control-label col-md-12" })
            @(Html.Kendo().DropDownListFor(m => m.Filters.Active)
            .HtmlAttributes(new { style = "width:83%", id = "FilterActiveDD", name="FilterActiveDD" })
            .DataTextField("Text")
            .DataValueField("Value")
            .Filter(FilterType.Contains)
            .DataSource(source =>
            {
            source.Read(read =>
            {
            read.Action(Model.ActiveDDActionResult, Model.Controller).Data("AddUser");
            }).ServerFiltering(false);
            })
            .FillMode(FillMode.None)
            )
            @Html.ValidationMessageFor(m => m.Filters.Active, "", new { @class = "text-danger" })
            <input type="hidden" name="activestate" value="@Model.Filters.Active" />
        </fieldset>
    </div>
</div>
<div class="row">
    <div class="col-md-3  align-self-center ">
        <input type="submit" Value="View Records" style=" vertical-align:bottom " class="btn btn-primary" />
    </div>
</div>

ViewModel

namespace project.Models
{
    public class ViewModel
    {
        public ListTab listTab {get;set;} = new ListTab();
    public class ListTab
        {
            public FilterViewModel PartialFilterViewModel { get; set; } = new FilterViewModel ();
        }
    }
}

Partial View ViewModel

namespace project.Models
{
    public class FilterViewModel
    {
        public MasterDataModel Filters { get; set; } = new MasterDataModel();

        public string ActiveDDActionResult { get; set; } = "";

        public string Controller { get; set; } = "";
    }
}

Controller Code

[Route("FillList")]
        public ActionResult FillList(ViewModel model)
        {
        return View("View", ViewModel);
        }

The model argument of the FillList action result is the thing that come back empty from the view. I say empty and not null, but all the properties of the class are set to default values.

I have tried just about everything

  • Using older model binding techniques from .Net Framework

  • Adding a [HttpPost] attribute to the ActionResult

  • Moving the BeginForm to outside of the partial view

  • Added some different attributes to the action result parameter. things like [FromForm]

  • followed this article where you manually set the name of the field as opposed to letting it do it dynamically, but it didn't work. Not sure if it's because it's from an older .net core version

  • Was going to try messing with viewbag. Everything that I see says it's totally fine to pass data from the controller to the view using ViewBag, but not the other way around. I also saw that it was removed entirely from .Net Core, so I'm a little confused.

The frustrating part is that if I replace the html helper calling the partial view in the main view with the code from inside that partial view, the binding works without issue.


Solution

  • Shorten Answer

    You need use for attribute in the partial tag helper:

    <partial name="_Filter" for="@Model.listTab.PartialFilterViewModel" />
    

    Detailed Explanation

    The frustrating part is that if I replace the html helper calling the partial view in the main view with the code from inside that partial view, the binding works without issue.

    No matter you use the partial view or directly use the code in the main view, the model binding always cannot work, because you use the name="FilterActiveDD" in the html attributes. If you remove this name, directly use the partial view code in main view can work.

    Model Binding binds the property by the name attribute, so the name attribute should match the property name. The correct name should be name="listTab.PartialFilterViewModel.Filters.Active".

    If you use model attribute in the partial tag helper(<partial name="_Filter" model="@Model.listTab.PartialFilterViewModel" /> ), it would generate the dropdown with name="Filters.Active" which does not match the ViewModel's property.

    enter image description here

    You need use for attribute in the partial tag helper, then it will generate the correct name:

    <partial name="_Filter" for="@Model.listTab.PartialFilterViewModel" />
    

    Client side generated html code: enter image description here

    Server side receive the value:

    enter image description here