MSDN Community

ASP.NET MVC: How to correctly pass multiple complex objects to a controller action?

Hi everyone,

I'm working on an ASP.NET MVC application and I'm facing a challenge when trying to pass multiple complex objects from a view to a controller action. I'm using view models to structure the data, but I'm having trouble with model binding on the server side.

Here's a simplified version of what I'm trying to achieve:

ViewModel in the View:
I have a main ViewModel that contains two other complex object ViewModels:

public class MainViewModel
{
    public UserProfileViewModel UserProfile { get; set; }
    public OrderDetailsViewModel OrderDetails { get; set; }
}

public class UserProfileViewModel
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }
}

public class OrderDetailsViewModel
{
    public int OrderId { get; set; }
    public List<string> Items { get; set; }
}
                

In my view, I'm using EditorFor and HiddenFor to render these properties. When the form is submitted, the controller action receives a null `MainViewModel` or some properties are missing.

Controller Action:

public ActionResult ProcessOrder(MainViewModel model)
{
    // model.UserProfile and model.OrderDetails are often null here.
    // ...
    return View("Success");
}
                

Could someone shed some light on the correct way to structure the form elements or the controller action to ensure proper model binding for nested complex objects? Any examples or best practices would be greatly appreciated!

Thanks in advance.
Reply Quote Upvote (5) Report
Hi John,

This is a common issue with model binding complex objects in ASP.NET MVC. The key is to ensure that your form input names precisely match the property names in your ViewModel, including the nested structure.

When you use `EditorFor` and `HiddenFor` with complex types, MVC attempts to bind them by prefixing the input names with the parent property name. Ensure your HTML is structured correctly for this to happen. Often, this means using `for` attributes in labels that correspond to the generated input `id` attributes.

A common pattern is to use `EditorForModel` or `EditorFor` for your nested ViewModels. Make sure you're not accidentally breaking the naming convention.

Let's look at how you might structure your view:

@model YourProject.ViewModels.MainViewModel

@using (Html.BeginForm("ProcessOrder", "YourControllerName", FormMethod.Post))
{
    // User Profile Section
    

User Profile

@Html.EditorFor(m => m.UserProfile, "UserProfileEditor") // Using a template is a good practice // Order Details Section

Order Details

@Html.EditorFor(m => m.OrderDetails, "OrderDetailsEditor") // Using a template }
And create corresponding Editor Templates (under `Views/Shared/EditorTemplates` or `Views/YourControllerName/EditorTemplates`):

Views/Shared/EditorTemplates/UserProfileEditor.cshtml

@model YourProject.ViewModels.UserProfileViewModel
User Profile
@Html.LabelFor(m => m.FirstName) @Html.EditorFor(m => m.FirstName) @Html.ValidationMessageFor(m => m.FirstName)
@Html.LabelFor(m => m.LastName) @Html.EditorFor(m => m.LastName) @Html.ValidationMessageFor(m => m.LastName)
@Html.LabelFor(m => m.Email) @Html.EditorFor(m => m.Email) @Html.ValidationMessageFor(m => m.Email)
Views/Shared/EditorTemplates/OrderDetailsEditor.cshtml

@model YourProject.ViewModels.OrderDetailsViewModel
Order Details
@Html.LabelFor(m => m.OrderId) @Html.EditorFor(m => m.OrderId) @Html.ValidationMessageFor(m => m.OrderId)
@Html.LabelFor(m => m.Items) @if (Model.Items != null) { for (int i = 0; i < Model.Items.Count; i++) {
@Html.EditorFor(m => m.Items[i], new { id = "item_" + i }) @Html.ValidationMessageFor(m => m.Items[i])
} }
By using editor templates, you ensure that the naming convention `UserProfile.FirstName`, `UserProfile.LastName`, `OrderDetails.OrderId`, etc., is maintained correctly.

Also, make sure your `MainViewModel` is instantiated before passing it to the view if you're pre-populating it. For example:

public ActionResult CreateOrder()
{
    var model = new MainViewModel
    {
        UserProfile = new UserProfileViewModel(),
        OrderDetails = new OrderDetailsViewModel { Items = new List() }
    };
    return View(model);
}
                
Let me know if this helps!
Reply Quote Upvote (3) Report
Sarah,

Thank you so much for the detailed explanation and the excellent example using editor templates! I was indeed missing the correct naming convention and the editor template approach is brilliant.

I've implemented your suggestions, and model binding is now working perfectly for both `UserProfile` and `OrderDetails` objects. The `MainViewModel` is correctly populated in my controller action.

One quick follow-up: For the `OrderDetailsViewModel.Items` which is a `List`, your example shows a loop. If the list can grow dynamically on the client-side (e.g., via JavaScript), how would you handle binding that? Does the same naming convention still apply?
Reply Quote Upvote (1) Report
Great to hear it worked, John!

Regarding dynamically adding items to a list client-side, yes, the same naming convention is crucial. When you add new input fields using JavaScript, you need to ensure their `name` attributes follow the pattern. For a list like `Items`, the generated names typically look like `Items[0]`, `Items[1]`, `Items[2]`, and so on.

If you're dynamically adding items, your JavaScript would need to keep track of the index. For example, if you have a button to "Add Item", the JavaScript handler for that button would increment a counter and append a new input field with the correct `name` attribute:

let itemIndex = @(Model.OrderDetails != null && Model.OrderDetails.Items != null ? Model.OrderDetails.Items.Count : 0); // Start with current count

function addItem() {
    const container = document.getElementById('order-items-container'); // Assuming you have a div with this ID
    const newItemDiv = document.createElement('div');
    newItemDiv.innerHTML = `
        
         // For validation messages
    `;
    container.appendChild(newItemDiv);
    itemIndex++;
}
                
And in your `OrderDetailsEditor.cshtml` template, you'd have a container for these dynamic items:

@model YourProject.ViewModels.OrderDetailsViewModel
Order Details
@Html.LabelFor(m => m.OrderId) @Html.EditorFor(m => m.OrderId) @Html.ValidationMessageFor(m => m.OrderId)
@Html.LabelFor(m => m.Items)
@if (Model.Items != null) { for (int i = 0; i < Model.Items.Count; i++) {
@Html.EditorFor(m => m.Items[i], new { id = "item_" + i }) @Html.ValidationMessageFor(m => m.Items[i])
} }
The crucial part is `name="OrderDetails.Items[${itemIndex}]"`. MVC's model binder understands this array/list notation and will correctly populate the `List Items` property in `OrderDetailsViewModel`.

Remember to also handle the initial `itemIndex` calculation if your list can be pre-populated.

This approach works reliably for lists and arrays when data is submitted from the client. Accepted Solution
Reply Quote Upvote (7) Report

Post a Reply