Search code examples
c#model-view-controllerrazorrazor-pages

MVC BeginForm returns null


So I've got a partial view which accepts a GeneralViewModel. The GeneralViewModel contains a list of table structures. At the moment it only has one:

GeneralViewModel

public class GeneralViewModel
{
    public List<Customers> Customers {get; set;}
}

Let's just say that Customers has some basic fields eg:

CustomersModel

public class Customers
{
    public int CustomerID {get; set;}
    public string CustomerName {get; set;}
}

I've made the View generic so it can handle any type being passed in (at the moment it's only Customers).

View

@model MVC.Models.GeneralViewModel

@{
    ViewBag.Title = "Index";
    Layout = "~/Views/Shared/_Layout.cshtml";
}

@section Styles {
    <link href="@Url.Content("~/Content/Home.css")" rel="stylesheet" type="text/css" />
}

@{
//Get the properties of GeneralViewModel
var generalProps = @Model.GetType().GetProperties();}

<div class="container">

    <h2>Index</h2>
    @using (Html.BeginForm("Export", "Home", FormMethod.Post, new { app = @Model }))
    {
        <input type="submit" value="Export" />
        <button>Import</button>
        <button>Approve Changes</button>
        <table>
            @{
                foreach(var generalProp in @generalProps)
                {
                    //Get the List model within the GeneralViewModel and check if it's got items within there
                    var item = (System.Collections.IList)generalProp.GetValue(@Model);
                    if (item.Count > 0)
                    {
                        //Get the properties for the list in this case CustomerID, CustomerName
                        var props = item[0].GetType().GetProperties();
                        <tr>
                            @foreach (var prop in props)
                            {
                                <th>
                                    //Show either the displayname or field as the header
@((System.ComponentModel.DisplayNameAttribute)Attribute.GetCustomAttribute(prop, typeof(System.ComponentModel.DisplayNameAttribute)) != null ?
                                ((System.ComponentModel.DisplayNameAttribute)Attribute.GetCustomAttribute(prop, typeof(System.ComponentModel.DisplayNameAttribute))).DisplayName : prop.Name)
                                </th>
                            }
                        </tr>
                        
                        //Loop through the List<Customers>
                        for (int i = 0; i < item.Count; i++)
                        {
                            <tr>
                                //Loop through the properties of Customers and post it
                                @for(int j = 0; j < props.Count(); j++)
                                {
                                    <td><input type="text" value="@props[j].GetValue(item[i], null)" readonly/></td>
                                }
                            </tr>
                        }
                    }
                }
            }
        </table>
    }
</div>

The code displays the table correctly but it's just the Export button which doesn't seem to work as intended.

Controller

[HttpPost]
public ActionResult Export(GeneralViewModel table)
{
    if(table.Customers != null)
    {
        Console.WriteLine("not null");
    }
}

I used to have foreach loops instead and have changed them to for loops instead as I had found a few places where they stated it must be for loops. Other then that I've seen that the forms have the field hardcoded within the View. For example //Loop through the List<Customers> section would be replaced with:

//Loop through the List<Customers>
for (int i = 0; i < item.Count; i++)
{
    <tr>
        <td><input type="text" value="item[i].CustomerID" readonly/></td>
        <td><input type="text" value="item[i].CustomerName" readonly/></td>
    </tr>
}

However this doesn't achieve what I want with GeneralViewModel containing more List models in the future. I know that I can create separate Views for the other models which will be added in the future but it would feel as if I'm just duplicating code.

In the controller what I get when I run it and inspect the table variable is that GeneralViewModel.Customers is null


Solution

  • Managed to get it working it was due to input element not having a name field. Also the name field has to be properly assigned according to the structure of the ViewModel.

    So I had switched:

    <td><input type="text" value="@props[j].GetValue(item[i], null)" readonly/></td>

    with:

    <td><input type="text" value="@props[j].GetValue(item[i], null)" 
    name="@(generalProp.Name)[@(i)].@props[j].Name" readonly/></td>