asp.net-core-mvchttp-post

ASP.NET Core MVC Edit GET works but POST results in ArgumentNullException: Value cannot be null. (Parameter 'items') despite debugging showing data


I'm sure this is something really obvious but I've spent hours (including a lot of Googling) on this and I am stuck.

I am able to create a record (along with the Fluent Validations firing where need be) but immediately upon clicking on a record's Edit button, I'm getting an error

ArgumentNullException: Value cannot be null. (Parameter 'items')

When I debug, I can see that within the Edit GET code, the return View(docTrailList) is being populated correctly.

Then, when I click on the first part of the code in the HttpPost section, I get the above noted null message. While in debug mode, if I hover over the code within the HttpPost section, I can see that it is capturing the data. This model's code is identical to other models' code and they are working fine.

What am I missing? Help most gratefully received.

using System;
using System.Collections;
using System.Linq;
using System.Threading.Tasks;
using DocumentFormat.OpenXml.Office2010.Excel;
using FluentValidation;
using FluentValidation.AspNetCore;
using FluentValidation.Results;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore;
using Research.Data;
using ValidatorAttribute = ServiceStack.FluentValidation.Attributes.ValidatorAttribute;

using Research.Models;
using Microsoft.CodeAnalysis.VisualBasic.Syntax;

namespace Research.Controllers
{
    public class DocTrailListsController : Controller
    {
        private readonly ResearchContext _context;

        public DocTrailListsController(ResearchContext context)
        {
            _context = context;
        }

        // GET: DocTrailLists
        public async Task<IActionResult> Index(
            string sortOrder,
            string currentFilter,
            string searchString,
            int? pageNumber)
        {
            ViewData["CurrentSort"] = sortOrder;
            ViewData["ProjIDSortParm"] = String.IsNullOrEmpty(sortOrder) ? "Proj_desc" : "";

            if (searchString != null)
            {
                pageNumber = 1;
            }
            else
            {
                searchString = currentFilter;
            }

            ViewData["CurrentFilter"] = searchString;

            var dTrailL = from s in _context.DocTrailList
                          select s;

            if (!String.IsNullOrEmpty(searchString))
            {
                dTrailL = dTrailL.Where(s => s.ProjectID.HasValue && s.ProjectID.Value.ToString().Contains(searchString));
            }

            switch (sortOrder)
            {
                case "Proj_desc":
                    dTrailL = dTrailL.OrderByDescending(s => s.ProjectID);
                    break;

                default:
                    dTrailL = dTrailL.OrderBy(s => s.ProjectID);
                    break;
            }

            int pageSize = 100;

            return View(await PaginatedList<DocTrailList>.CreateAsync(dTrailL.AsNoTracking(), pageNumber ?? 1, pageSize));
        }

        // GET: DocTrailLists/Details/5
        public async Task<IActionResult> Details(int? id)
        {
            if (id == null || _context.DocTrailList == null)
            {
                return NotFound();
            }

            var docTrailList = await _context.DocTrailList
                .Include(d => d.PIF)
                .FirstOrDefaultAsync(m => m.DetailID == id);

            if (docTrailList == null)
            {
                return NotFound();
            }

            return View(docTrailList);
        }

        // GET: DocTrailLists/Create
        [HttpGet]
        public IActionResult Create()
        {
            ViewData["ProjectID"] = new SelectList(_context.PIF, "ProjectID", "ProjectID");

            var dtname = _context.DocType!.ToList();
            ViewBag.dtname = dtname;

            var current = _context.YesNoList!.ToList();
            ViewBag.current = current;

            return View();
        }

        public Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary GetModelState()
        {
            return ModelState;
        }

        // POST: DocTrailLists/Create
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Create([Bind("DetailID,ProjectID,DocumentType,Version,Dated,DateReceived,IsThisCurrent")] DocTrailList docTrailList,
            string action,
            string returnUrl)
            //string returnUrl, 
            //Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary modelState)
        {
            DocTrailListsValidator doctraillistsvalidator = new DocTrailListsValidator(_context);
           
            ValidationResult result = doctraillistsvalidator.Validate(docTrailList);

            // When validation failed, return to Index View
            if (!ModelState.IsValid)
            {
                foreach (var failure in result.Errors)
                {
                    ModelState.AddModelError(failure.PropertyName, failure.ErrorMessage);
                }

                ViewData["ProjectID"] = new SelectList(_context.PIF, "ProjectID", "ProjectID", docTrailList.ProjectID);

                var dtname = _context.DocType!.ToList();
                ViewBag.dtname = dtname;

                var current = _context.YesNoList!.ToList();
                ViewBag.current = current;

                return View(docTrailList);
            }

            // Perform DB update after validation is passed
            if (action == "SaveAndBack" && !String.IsNullOrEmpty(returnUrl))
                {
                    try
                    {
                        _context.Add(docTrailList);
                        await _context.SaveChangesAsync();


                    }
                    catch (DbUpdateConcurrencyException)
                    {
                        if (!DocTrailListExists(docTrailList.DetailID))
                        {
                            return NotFound();
                        }
                        else
                        {
                            throw;
                        }
                    }

                    // Ensure all code paths return a value
                    // Redirection after model validation passed and successfully update to database
                    var redirectUrl = Url.Action("Details", "PIFs", new { id = docTrailList.ProjectID });

                    //if (action == "SaveAndBack" && !String.IsNullOrEmpty(returnUrl + 1))
                    if (string.IsNullOrEmpty(redirectUrl))
                    {
                        return NotFound(); // Handle the case where the URL could not be generated
                    }
                    return Redirect(redirectUrl);
                }
                else
                {
                    try
                    {
                        _context.Add(docTrailList);
                        await _context.SaveChangesAsync();
                    }
                    catch (DbUpdateConcurrencyException)
                    {
                        if (!DocTrailListExists(docTrailList.DetailID))
                        {
                            return NotFound();
                        }
                        else
                        {
                            throw;
                        }
                        //return View(sAE);

                    }
                    return RedirectToAction(nameof(Index));
                }
            }

        }

        // GET: SAEs/Edit/5
        [HttpGet]
        public async Task<IActionResult> Edit(int? id)
        {
            if (id == null || _context.DocTrailList == null)
            {
                return NotFound();
            }

            var docTrailList = await _context.DocTrailList.FindAsync(id);
            if (docTrailList == null)
            {
                return NotFound();
            }

            ViewData["ProjectID"] = new SelectList(_context.PIF, "ProjectID", "ProjectID", docTrailList.ProjectID);

            var dtname = _context.DocType!.ToList();
            ViewBag.dtname = dtname;

            var current = _context.YesNoList!.ToList();
            ViewBag.current = current;

            return View(docTrailList);
        }

        // POST: SAEs/Edit/5
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Edit(int id,[Bind("DetailID,ProjectID,DocumentType,Version,Dated,DateReceived,IsThisCurrent")] DocTrailList docTrailList,
           string action,
           string returnUrl)
        {
            if (id != docTrailList.DetailID)
            {
                return NotFound();
            }

                DocTrailListsValidator docTrailListsValidator = new DocTrailListsValidator(_context);
                ValidationResult result = docTrailListsValidator.Validate(docTrailList);

            {
                if (!ModelState.IsValid)
                {
                    foreach (var failure in result.Errors)
                    {
                        ModelState.AddModelError(failure.PropertyName, failure.ErrorMessage);
                    }
                    ViewData["ProjectID"] = new SelectList(_context.PIF, "ProjectID", "ProjectID", docTrailList.ProjectID);

                    var dtname = _context.DocType!.ToList();
                    ViewBag.dtname = dtname;

                    var current = _context.YesNoList!.ToList();
                    ViewBag.current = current;

                    return View(docTrailList);
                }
                    // Perform DB update after validation is passed
                if (action == "SaveAndBack" && !String.IsNullOrEmpty(returnUrl))
                {
                    try
                    {
                        _context.Update(docTrailList);
                        await _context.SaveChangesAsync();
                    }
                    catch (DbUpdateConcurrencyException)
                    {
                        if (!DocTrailListExists(docTrailList.DetailID))
                        {
                            return NotFound();
                        }
                        else
                        {
                            throw;
                        }
                    }

                    // Ensure Url.Action does not return null before passing it to Redirect
                    var redirectUrl = Url.Action("Details", "PIFs", new { id = docTrailList.ProjectID });

                    if (string.IsNullOrEmpty(redirectUrl))
                    {
                        return NotFound(); // Handle the case where the URL could not be generated
                    }

                    return Redirect(redirectUrl);
                }
                else
                {
                    try
                    {
                        _context.Update(docTrailList);
                        await _context.SaveChangesAsync();
                    }
                    catch (DbUpdateConcurrencyException)
                    {
                        if (!DocTrailListExists(docTrailList.DetailID))
                        {
                            return NotFound();
                        }
                        else
                        {
                            throw;
                        }
                    }

                    return RedirectToAction(nameof(Index));
                }
            }
        }
  

Edit.cshtml:

@model Research.Models.DocTrailList
@using System.Web.Optimization
@using Microsoft.AspNetCore.Mvc
@using System.Web
@using Microsoft.AspNetCore.Html
@using FluentValidation.AspNetCore
@using FluentValidation
@using FluentValidation.Results;

@{
    ViewData["Title"] = "Edit";
}
@Html.ValidationSummary(true)


<h2>Edit:  Document Trail</h2>
<style>
    body {
        background-color: blanchedalmond;
    }

    .ProjID {
        height: 80px;
        margin: 20px;
        border: 5px solid;
        background-color: burlywood;
    }

    .DocType {
        height: 50px;
        margin: 20px;
        border: 5px solid;
        background-color: darkkhaki;
    }

    .Version {
        height: 80px;
        margin: 20px;
        border: 5px solid;
        background-color: lightblue;
    }

    .DReceived {
        height: 80px;
        margin: 20px;
        border: 5px solid;
        background-color: lavender;
    }

</style>
<body>
    <div class="container">
        <div class="row justify-content-start">
            <form asp-action="Edit">
                <div asp-validation-summary="ModelOnly" class="text-danger"></div>
                <input type="hidden" asp-for="DetailID" />
                <div class="ProjID">
                    <div class="row justify-content-start">
                        <div class="col">
                            <div class="form-group">
                                <label asp-for="ProjectID" class="control-label"></label>
                                <select asp-for="ProjectID" class="form-control" asp-items="ViewBag.ProjectID"></select>
                                <span asp-validation-for="ProjectID" class="text-danger"></span>
                                 <div class="col-md-10"> 
                                   @Html.ValidationMessageFor(model => model.ProjectID, null, new { @class = "text-danger" })
                                </div> 
                            </div>
                        </div>
                        <br />
                    </div>
                </div>
                <div class="DocType">
                    <div class="row justify-content-start">
                        <div class="col">
                            <div class="form-group"> 
                                <label asp-for="DocumentType" class="control-label"></label>
                                @Html.DropDownList("DocumentType", new SelectList(ViewBag.dtnamelist,"DocumentTypeName","DocumentTypeName"),"")
                            </div>
                        </div>
                        <br />
                    </div>
                </div>
                <div class="Version">
                    <div class="row justify-content-start">
                        <div class="col">
                            <div class="form-group">
                                <label asp-for="Version" class="control-label"></label>
                                <input asp-for="Version" class="form-control" />
                                <span asp-validation-for="Version" class="text-danger"></span>
                            </div>
                        </div>
                        <div class="col">
                            <div class="form-group">
                                <label asp-for="Dated" class="control-label"></label>
                                <input asp-for="Dated" class="form-control" />
                                <span asp-validation-for="Dated" class="text-danger"></span>
                            </div>
                        </div>
                        <br />
                    </div>
                </div>
                <div class="DReceived">
                    <div class="row justify-content-start">
                        <div class="col">
                            <div class="form-group">
                                <label asp-for="DateReceived" class="control-label"></label>
                                <input asp-for="DateReceived" class="form-control" type="date" max='' id="dateReceived"/>
                                <span asp-validation-for="DateReceived" class="text-danger"></span>
                            </div>
                        </div>
                        <div class="col">
                            <div class="form-group"> 
                                <label asp-for="IsThisCurrent" class="control-label"></label>
                                @Html.DropDownList("IsThisCurrent", new SelectList(ViewBag.currentlist,"YesNo","YesNo"),"")
                            </div>
                        </div>
                        <br />
                    </div>
                </div>
                <input type="hidden" id="returnUrl" name="returnUrl" value="" />
                <div class="form-group">
                    <input type="submit" name="action" value="Save" class="btn btn-primary" />
                    &nbsp;
                    <button type="submit" name="action" value="SaveAndBack" class="btn btn-primary">Save and go back to the PIF</button>
                    &nbsp;
                    <a class="btn btn-info" asp-action="Index" role="button">Back to List</a>
                </div>
            </form>
        </div>
    </div>
</body>

@section Scripts {
     <script>
        //I use js to allow users to select only the latest date up to today
        function addZero(n) {
            return parseInt(n) >= 10 ? n.toString() : '0' + n;
        }
        let dateNow = new Date(),
            yearNow = dateNow.getFullYear(),
            monthNow = dateNow.getMonth() + 1,
            dayNow = dateNow.getDate(),
            maxDate = yearNow + '-' + addZero(monthNow) + '-' + addZero(dayNow);
        let inp = document.querySelector('#dateReceived');
       
        
        inp.setAttribute('max', maxDate);
    </script>

    <script>
        document.getElementById("returnUrl").value = document.referrer;
    </script>


    <script src="@Url.Content("~/Scripts/jquery.validate.js")" type="text/javascript"></script>
    <script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.js")" type="text/javascript"></script>
    @{
        await Html.RenderPartialAsync("_ValidationScriptsPartial");
    }
}

Fluent Validation:

using FluentValidation;
using Research.Data;

namespace Research.Models
{
    public class DocTrailListsValidator : AbstractValidator<DocTrailList>
    {
        private ResearchContext _context;

        public DocTrailListsValidator(ResearchContext context)
        {
            this._context = context ?? throw new ArgumentNullException(nameof(context));

            RuleFor(x => x.DocumentType).NotNull().WithMessage("A required field");
            RuleFor(x => x.Version).NotNull().WithMessage("A required field");
            RuleFor(x => x.Dated).NotNull().WithMessage("A required field");
            RuleFor(x => x.DateReceived).NotNull().WithMessage("A required field");
            RuleFor(x => x.Version).NotNull().WithMessage("A required field");
            RuleFor(x => x.IsThisCurrent).NotNull().WithMessage("A required field");
            RuleFor(x => x.ProjectID)
                .Must((root, projectID, context) => !IsDuplicate(root))
                .WithMessage("A document of the same type is also marked as current.  This version will not be saved if answered Yes")
                .When(x => x.IsThisCurrent != null && x.IsThisCurrent.Equals("Yes"), ApplyConditionTo.CurrentValidator);
        }

        private bool IsDuplicate(DocTrailList c)
        {
            var existingValues = _context.DocTrailList!.ToList();

            return existingValues.Any(x => x.ProjectID == c.ProjectID &&
                                           x.DocumentType == c.DocumentType &&
                                           x.IsThisCurrent == c.IsThisCurrent);
        }
    }
}

Edit button on Index.cshtml:

<div class="btn-group">
    <a asp-action="Edit" asp-route-id="@item.DetailID">
        <i class="fa fa-edit" style="font-size:24px"></i>
    </a>
    &nbsp;
    <a asp-action="Delete" asp-route-id="@item.DetailID">
        <i class="fa fa-trash-o" style="font-size:24px; color:red"></i>
    </a>
 </div>

Error Message screen: Screen that appears upon clicking the Edit button


Solution

  • Aside from the comments mentioned that the variable names conflict in the controller action and the view,

    Recommend to have a proper naming indicates the value is a list/array. Also, you can just assign the value directly to the ViewData/ViewBag

    ViewData["ProjectIDList"] = new SelectList(_context.PIF!.ToList(), "ProjectID", "ProjectID", docTrailList.ProjectID);
    
    ViewBag.DtnameList = new SelectList(_context.DocType!.ToList(), "DocumentTypeName", "DocumentTypeName");
    
    ViewBag.CurrentList = new SelectList(_context.YesNoList!.ToList(), "YesNo", "YesNo");
    

    And switch to asp-items tag helper for consistency.

    <select asp-for="ProjectID" class="form-control" asp-items="ViewBag.ProjectIDList"></select>
    
    <select asp-for="DocumentType" class="form-control" asp-items="ViewBag.DtnameList"></select>
    
    <select asp-for="IsThisCurrent" class="form-control" asp-items="ViewBag.CurrentList"></select>