asp.net-corerazor-pagesmodel-binding

Razor Pages Binding a List<t> to HTML Table


I am using ASP.NET Core 8.0 with Razor Pages (not MVC). Webpage has a table where the prices of services offered can be modified as shown here:

enter image description here

I have created a model class ServProdModel and a bound property collection called Servs:

[BindProperty]
public List<ServProdModel>? Servs { get; set; }

The values get bound properly, as can be seen. But I am facing issues in the POST handler.

I have bound it into the HTML Table as follows (using explicit index) like this:

@foreach (var itm in Model.Servs)
{
    <tr>
        <td class="visually-hidden">
            <input type="hidden" name='Servs.Index' value='@itm.ServID' />
            <input type="hidden" name='Servs[@itm.ServID].ServID' value='@itm.ServID' />
        </td>
        <td>
            <input type="text" name='Servs[@itm.ServID].Price' value='@itm.Price' />
        </td>
        ...
    </tr>
}

I get the collection in the Request.Form as shown below:

enter image description here

I also see the Servs.Count = 5, which is correct since there are 5 services.

HOWEVER, all the items in the Servs List are null. And I get an error:

System.NullReferenceException: Object reference not set to an instance of an object.

What is missing? Please help!

I also tried using for loop but the same thing happens.

[EDIT]

Image shows Collection with null objects:

enter image description here

Following image shows Request.Form collection after using Sequential Index instead of Explicit Index:

enter image description here

[UPDATE]: As promised, attaching a minimal code to reproduce the defect.

1. TestPg.cshtml

@page
@model NewCOS.Pages.Admins.TestPgModel
@{
}

@if (Model.Servs != null)
{
    <form method="post">
        <div class="row g-2 mb-1">
            <div class="col-md-8">
                <button type="submit" asp-page-handler="AddServs" class="btn btn-sm btn-success">Add Selected Services To Cart</button>
            </div>
            <div class="col-md-4">
            </div>
        </div>
        <div class="table-responsive scrollbar" style="max-height:300px">
            <table id="tblItems" class="table table-bordered table-striped fs-10">
                <thead class="clsDummy">
                    <tr>
                        <th class="visually-hidden"></th>
                        <th class="bg-200 text-center">Select</th>
                        <th class="bg-200 text-center">Services</th>
                        <th class="bg-200 text-center">Quantity</th>
                        <th class="bg-200 text-center">Price</th>
                        <th class="bg-200 text-center cos-pre">Promotion</th>
                        <th class="bg-200 text-center">Discount</th>
                        <th class="bg-200 text-center">Discount<br />Amount</th>
                    </tr>
                </thead>
                <tbody class="clsDummyBody">
                    @{
                        //int i = 0;
                        @foreach (var itm in Model.Servs)
                        {
                            <tr>
                                <td class="visually-hidden">
                                    <input type="hidden" name='Servs.Index' value='@itm.ServID' />
                                    <input type="hidden" name='Servs[@itm.ServID].ServID' value='@itm.ServID' />
                                </td>
                                <td class="text-center">
                                    <input type="checkbox" id='@("S" + itm.ProdID)' class="form-check-input" onchange="changeQty(this);" />
                                </td>
                                <td><span>@itm.Service</span></td>
                                <td>
                                    <div class="input-group input-group-sm flex-nowrap" data-quantity="data-quantity">
                                        <button class="btn btn-sm btn-outline-secondary border-300 px-2 shadow-none" data-type="minus">-</button>
                                        <input type="number" id='@("QS" + itm.ProdID)' name='Servs[@itm.ServID].Qty' value='@itm.Qty' min="0" aria-label="Quantity" style="width: 50px" class="form-control text-center px-2 input-spin-none" />
                                        <button class="btn btn-sm btn-outline-secondary border-300 px-2 shadow-none" data-type="plus">+</button>
                                    </div>
                                </td>
                                <td>
                                    <div class="d-flex flex-row">
                                        <div class="d-inline-flex pt-1">$&nbsp;</div>
                                        <div class="d-inline-flex">
                                            <input type="text" name='Servs[@itm.ServID].Price' value='@itm.Price' class="form-control form-control-sm" placeholder="Enter Price" maxlength="10" />
                                        </div>
                                    </div>
                                </td>
                                <td>
                                    <select class="form-select form-select-sm" name='Servs[@itm.ServID].PromoID' asp-for="@itm.PromoID">
                                        <option value="0">-- Select --</option>
                                        <option value="1">Senior Discount</option>
                                        <option value="2">Complementary</option>
                                        <option value="3">Student Discount</option>
                                    </select>
                                </td>
                                <td>
                                    <input class="form-control form-control-sm text-center px-2 input-spin-none" type="number" min="0" max="1" step="0.01" value="0" aria-label="Discount between 0 and 1" style="width: 50px" />
                                </td>
                                <td>$&nbsp;<span>0.00</span></td>
                            </tr>
                            //i++;
                        }
                    }
                </tbody>
            </table>
        </div>
    </form>
}

2. TestPg.cshtml.cs

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace NewCOS.Pages.Admins
{
    public class TestPgModel : PageModel
    {
        [BindProperty]
        public List<ServModel>? Servs { get; set; }

        public void OnGet()
        {
            ServModel ServProd = new ServModel();
            Servs = ServProd.Fetch(ServModel.ItemType.Service);
        }
        public void OnPostAddServs()
        {
            var chk = Request.Form;

            if (Servs != null)
            {
                foreach (var itm in Servs)
                {
                    int Qty = itm.Qty;
                    decimal prc = itm.Price;
                }
            }
        }

    }
    public class ServModel
    {
        private int _uid;
        public int SchoolUID;
        public int CompID;
        public string? UPC;
        public string ProductName = "";
        public string Service = "";
        public decimal RetailPrice = 0.00M;
        public decimal Price = 0.00M;
        public int Qty = 0;
        public int PromoID = 1;
        public decimal DiscAmount = 0.00M;
        public decimal Discount = 0.00M;
        public decimal TaxAmount = 0.00M;
        public decimal TotalAmount = 0.00M;
        public ItemType TransCode;
        public int ChkServ;
        public int ChkProd;

        public int ProdID { get { return _uid; } }
        public int ServID { get { return _uid; } }

        public enum ItemType
        {
            Product, Service
        }

        public List<ServModel>? Fetch(ItemType type)
        {
            List<ServModel>? Services = new List<ServModel>();

            if (type == ItemType.Service)
            {
                Services.Add(new ServModel { _uid = 1, TransCode = ItemType.Service, Service = "Acrylic/Gel Tips/Overlay", Price = 20.00M, CompID = 1 });
                Services.Add(new ServModel { _uid = 2, TransCode = ItemType.Service, Service = "Acrylic/Natural Nails", Price = 25.00M, CompID = 1 });
                Services.Add(new ServModel { _uid = 10, TransCode = ItemType.Service, Service = "Additional Colors Each", Price = 15.00M, CompID = 1 });
                Services.Add(new ServModel { _uid = 11, TransCode = ItemType.Service, Service = "Airstyle w/brush", Price = 5.00M, CompID = 1 });
                Services.Add(new ServModel { _uid = 17, TransCode = ItemType.Service, Service = "Beard Trim", Price = 3.00M, CompID = 1 });

                return Services;
            }
            else { return null; }
        }
    }
}

IMP: This code implements explicit index. If you would like to test with Sequential Index then

  1. Uncomment following two lines:
int i = 0;
i++;
  1. Comment this line:
<input type="hidden" name='Servs.Index' value='@itm.ServID' />
  1. Replace Servs[@itm.ServID] with Servs[@i]

Solution

  • For model binding, you have to work with the properties (getter/setter) instead of field.

    Change for those fields that you are passing the value from the view to properties in your ServModel class.

    public class ServModel
    {
        public decimal Price { get; set; } = 0.00M;
        public int Qty { get; set; } = 0;
        public int PromoID { get; set; } = 1;
    
        ...
        
        public int ServID 
        { 
            get { return _uid; } 
            set { _uid = value; } 
        }
    
        ...
    }