Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ViewModel of Partial View Not Binding to that of main ViewModel after loading via AJAX

ViewModels

//MainViewModel
    public class MainViewModel
    {
        public string UserID { get; set; }
        public string ServiceType { get; set; }
        public List<Service> Services {get; set;}
     }

     public class Service
     {
       public string ServiceID {get; set;}
       public string ServiceName {get; set;
     }

Here is the View

//Index.cshtml
@model ParentViewModel
@{
  <div>
     @Html.DropDownListFor(model => model.UserID, new{onchange="ddSelectionChanged(this)"})
     <div id="services">
        //This is filled with the ajax
     </div>
 </div>
}

Here is the AJAX Call

<script> 
function ddSelectionChanged(element){
$('#services').load('@(Url.Action("GetServices"))?serviceType =' element.value);
};
</script>

Here is the Controller

//Controller
public class UserServicesController : Controller
{
    public ActionResult Index()
    {
      return View();
    }

    public PartialViewResult GetServices(string serviceType)
    {
      //Service servicesViewModel = Fetch Services from DB
      return PartialView("PartialViews/Shared/_Services", servicesViewModel);
    }
}

This works fine and the partial View is rendered as expected but when I post the Index, the property "Services" of MainViewModel is returned "null". The issue is that the 'Services' Property of the model is not rebinding to the MainViewModel because its rendered as a partial view dynamically.

How can I rebind the model either at the time of Partial View Rendering or before POSTing the form?

like image 430
Adil Khalil Avatar asked Mar 18 '23 21:03

Adil Khalil


1 Answers

When you post to an action, the only parts that get filled by the modelbinder are the fields and data that were posted. If you're losing your services, then you need to provide fields in your form to hold on to that data during the post.

For example, in whatever HTML is returned by your AJAX, you'd need to include something like a hidden input for each of the properties on Service:

@Html.HiddenFor(m => m.ServiceID)
@Html.HiddenFor(m => m.ServiceName)

Then, that data will be posted along with everything else when your form is posted and the modelbinder will be able to fill your Services list appropriately. However, you also need to pay attention to the field names that are produced. Particularly if your partial view returned by your action that responds to the AJAX request utilizes a model like List<Service> directly instead of MainViewModel, then the rendered HTML will lose the context of knowing that it's supposed to bind to a property called Services. In your HTML, in other words, your field names will end up as something like [0].ServiceID instead of Services[0].ServiceID. The latter will be necessary to get the modelbinder to handle the posted data properly.

You can compensate for this by passing a custom ViewDataDictionary to your Html.Partial or Html.RenderPartial calls. By defining a new TemplateInfo with HtmlFieldPrefix set to whatever it needs to be, i.e.:

@Html.Partial("_SomePartial", someModel, new ViewDataDictionary
{
    TemplateInfo = new System.Web.Mvc.TemplateInfo { HtmlFieldPrefix = "SomePrefix" }
}

However, it's trickier when returning PartialView directly, because PartialView doesn't accept a parameter for a custom ViewDataDictionary. I haven't tried it myself, but it's possible you can set HtmlFieldPrefix directly in your action:

public PartialViewResult GetServices(string serviceType)
{
  ViewData.TemplateInfo.HtmlFieldPrefix = "Services";
  //Service servicesViewModel = Fetch Services from DB
  return PartialView("PartialViews/Shared/_Services", servicesViewModel);
}

Also worth mentioning, if you haven't worked with binding collections before, is that you need to use a for loop instead of a foreach loop in order to provide the appropriate context for the field names. For example, something like the following:

@foreach (var service in Model)
{
    @Html.HiddenFor(m => service.ServiceID)
}

Would result in a field with the name, service.ServiceID. The modelbinder would not know where this value should go and would basically ignore it. Instead, you would need to do:

@for (var i = 0; i < Model.Count(); i++)
{
    @Html.HiddenFor(m => m[i].ServiceID)
}

That would get you fields with names like [0].ServiceID. Again, the prefix is a problem, so you'll need both components to get this to work correctly.

like image 71
Chris Pratt Avatar answered Apr 07 '23 19:04

Chris Pratt