Search code examples
c#formsrazorasp.net-core-mvcasp.net-mvc-viewmodel

How do I debug form data being sent to a controller?


I'm developing a web application in C# from an ASP.NET Core MVC template in Visual Studio 2022, using .NET 6.0.

I created a PersonController as an MVC controller with views, using Entity Framework scaffolded item, and it's not receiving all the form data I'm sending from the Create.cshtml view. I'm using a CreateViewModel as an intermediary, which has the following properties:

namespace MyApp.Models
{
    public class CreateViewModel
    {
        [BindRequired]
        public Person Person { get; set; } = new Person();
        public List<SelectListItem>? Sectors { get; set; }

        [Required(ErrorMessage = "Please choose a sector.")]
        public int ChosenSectorID { get; set; }
    }
}

Here's a snippet of the Person class:

using System.ComponentModel.DataAnnotations;

namespace MyApp.Models.TableModels
{
    public class Person
    {
        public int PersonId { get; set; }

        [Required]
        [StringLength(20)]
        [Display(Name = "First Name")]
        public string FirstName { get; set; }

        [Required]
        [StringLength(30)]
        [Display(Name = "Last Name")]
        public string LastName { get; set; }

        [Required]
        public int CityID { get; set; }

        public int SectorID { get; set; }

        [StringLength(50)]
        public string? Nickname { get; set; }

        [Required]
        [DataType(DataType.DateTime)]
        public DateTime BirthDate { get; set; }

        [DataType(DataType.Date)]
        public DateTime? SomeOtherDate { get; set; }
        ...
    }
}

Person is a class I created, and has properties such as string FirstName, string LastName, DateTime BirthDate, int SectorID, etc. Sectors is a list of Sectors arranged as 'key, value' pairs of SectorID and SectorName, which are used to populate a dropdown list in the create view.

The 'key, value' pairs are provided by a 'Get Create' function in the controller. ChosenSectorID is just what it says: that gets set when the user chooses a sector from the dropdown. The create view itself looks something like this:

@model CreateViewModel

@{
    ViewData["Title"] = "Create Person";
}

<h1>New Person</h1>
<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Create">
            <div asp-validation-summary="ModelOnly class="text-danger"></div>
            <div class="form-group">
                <label asp-for="Person.FirstName" class="control-label"></label>
                <input asp-for="Person.FirstName" class="form-control" />
                <span asp-validation-for="Person.FirstName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Person.LastName" class="control-label"></label>
                <input asp-for="Person.LastName" class="form-control" />
                <span asp-validation-for="Person.LastName" class="text-danger"></span>
            </div>
            ...
            <div class="form-group">
                <label asp-for="ChosenSectorID" class="control-label col-md-2"></label>
                <div class="col-md-10">
                    @Html.DropDownListFor(model => model.ChosenSectorID, Model.Sectors, "Choose a Sector", new { @class = "form-control" })
                    <span asp-validation-for="ChosenSectorID" class="text-danger"></span>
                </div>
            </div>
            <div class="form-group">
                <label asp-for="Person.BirthDate" class="control-label"></label>
                <input asp-for="Person.BirthDate" class="form-control" />
                <span asp-validation-for="Person.BirthDate" class="text-danger"></span>
            </div>
            ...
            <div class="form-group">
                <input type="submit" value="Submit" class="btn btn-primary" />
            </div>
        </form>
...

For the record, every other field in this View is setup the same as 'FirstName', and they are all properties of Person. And this is a snippet from my post Create function (as well as the Get Create function that supplies the data for CreateViewModel.Sectors, just in case the error is there) in my PersonController, where this form data should be getting sent when the form is submitted:

// GET: Person/Create
public async Task<IActionResult> Create()
{
    var sectors = await _context.Sectors.ToListAsync();
    var viewModel = new CreateViewModel
    {
        Person = new Person(),
        Sectors = sectors.Select(s => new SelectListItem
        {
            Value = s.SectorId.ToString(),
            Text = s.SectorName
        }).ToList()
    };

    return View(viewModel);
}

// POST: Person/Create
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([FromForm] [Bind("Person.FirstName,Person.LastName,Person.Nickname,...,Person.BirthDate,ChosenSectorID")] CreateViewModel viewModel)
{
    Console.WriteLine("Create function called.");

    // action code
    ...

It should be noted that there is a Person.SectorID, but it does not show up as a form element in the Create view, nor is it listed in the bindings on the controller; believe me, I've tripled-checked.

So, here's the problem: if I run my web application from Visual Studio, and I go to the Create page and fill all the fields in the form with valid data, then click the Submit button, only one element of the form data goes through: the ChosenSectorID.

If I open my network tab in the developer's tools (in Edge) when I'm submitting the form, I can see all the form data is filled in under the "Payload" header.

The form data looks like this:

Person.FirstName: Carl
Person.LastName: Carlson
Person.CityID: 2
ChosenSectorID: 3
Person.Nickname: Nucular Guy
...
Person.BirthDate: 1969-04-01T00:00
_RequestVerificationToken: ...

But if I place a breakpoint on the first line of code (Console.WriteLine("function called");) in my PersonController, and check the view model, ChosenSectorID has a value of 3, but all of the properties of Person, e.g. Person.FirstName, have 'null' values. Or 0, for the integers, and BirthDate has {1/1/0001 12:00:00 AM}.

FirstName, BirthDate, and a few others have a [Required] tag in the Person model. There's also a datetime field in Person that is used on the form, is not marked as [Required], is in the [Bind] attribute on the controller's Create function, and comes through as 'null', same as all the string fields, regardless of whether I input a value in the field or not.

I'm at a complete loss, here.

I've been through and verified that all the fields and properties used are spelled correctly, the application builds and runs, so no compiler errors. I've triple checked that the [Bind] attributes match the fields in the form on the Create view. It should be noted that the form submission was working, when I was using @model Person in the Create view, handling SectorID just like every other field (as an integer input), and just had the properties listed in the [Bind] attribute like so: [Bind]("FirstName,LastName,SectorID,BirthDate,.... This problem only showed up once I tried to use a ViewModel, and changed SectorID on the form from a number field to a dropdown menu.

Originally, I wasn't initializing Person in CreateViewModel, and in that case, the entire Person property in the viewModel received in the controller was 'null,' though ChosenSectorID still had the entered value from the form. Having or removing [BindRequired] or [Required(ErrorMessage = "Please choose a sector.")] from the CreateViewModel doesn't seem to make a difference. I've also checked the ModelState.IsValid in the PersonController, and that comes through with its ValidationState as Invalid, and all of its Values are Invalid except for the one associated with the key "ChosenSectorID". And just to reiterate, like I've stated previously, if I check the viewModel object being passed in to the controller, I'll find that ChosenSectorID is 3, Person.FirstName is null, Person.BirthDate is 1/1/0001, Person.Nickname is null, so on and so forth.

I've been through all sorts of troubleshooting yesterday, with the assistance of Bing AI. I got a couple extra pairs of eyes looking at the code, and they couldn't see anything 'off' about the code. I don't know how I broke this so badly. If I need to provide more information, let me know.


Solution

  • I've solved the problem: The [Bind] attribute was missing the Person object reference, itself. So, what I did to fix this was a simple change to the [Bind] attribute:

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> Create([FromForm] [Bind("Person,Person.FirstName,Person.LastName,Person.Nickname,...,Person.BirthDate,ChosenSectorID")] CreateViewModel viewModel)
    {
        Console.WriteLine("It works, now.");
        ...
    

    As I was reworking what I was trying to accomplish in this branch, adding in my changes step by step, it occurred to me that I was binding properties from the Person object, without binding the Person object itself, so I figured "What the hey!" and gave it a try.

    I think what tipped me off to this was that my changes broke the functionality of the controller right when I changed the input of the Create function to receive the new CreateViewModel viewModel instead of the old Person person input that I had been using before. Where I was getting a Person object with the necessary fields filled in before, I was now, once again, getting a CreateViewModel object with no values in the necessary fields. And this was with just the Person property in CreateViewModel, so it just became clear at that point what the issue was.