Search code examples
formsasp.net-corepostmodel-bindingmediatr

How can I bind form fields to a nested model on post?


I am coding a solution where the user will submit a form, posting the values back to my ASP.NET MVC controller. My model is complex and the form fields are contained in a nested object (I'm using CQRS via MediatR). When I submit the form, the values come across as null. How can I get the complex model to recognize the form fields?

Here is my code:

Controller:

        [HttpPost]
        [Route("edit")]
        public async Task<IActionResult> Edit(UpdateApplicationCommand command)
        {
            await _mediator.Send(command)
                .ConfigureAwait(false);

            return RedirectToAction("Index");
        }

Models:

    public class UpdateApplicationCommand : IRequest<Unit>
    {
        public ApplicationEditGeneralViewModel ApplicationEditGeneralViewModel { get; set; } = null!;
    }

    public class ApplicationEditGeneralViewModel
    {
        [Required]
        public string Name { get; set; } = string.Empty;

        public string Description { get; set; } = string.Empty;
    }

View:

    @model ApplicationEditGeneralViewModel

        <form method="post" asp-action="Edit" asp-controller="Applications">
            <div class="form-floating mb-3">
                @Html.TextBoxFor(m => m.Name, new { @class = "form-control", placeholder = "Application Name"})
                <label for="Name">Application Name</label>
            </div>
            <div class="form-floating mb-3">
                @Html.TextBoxFor(m => m.Description, new { @class = "form-control", placeholder = "Application Description"})
                <label for="Description">Application Description</label>
            </div>
            <div class="d-flex flex-row-reverse bd-highlight">
                <input type="submit" value="Submit" class="btn btn-primary mt-2" />
            </div>
        </form>

I've tried to reduce the complex model to its fields, by placing the contents of the ApplicationEditGeneralViewModel directly into the UpdateApplicationCommand class. This worked, but I'd really like to keep the nested structure so that I can reuse the ApplicationEditGeneralViewModel object.

I saw this solution here:

But I'd rather avoid adding the name as a route object (if possible) for every form field. Is there another, more simple way that I can do this?


Solution

  • The first way, you can custom model binding like below:

    public class CustomModelBinder : IModelBinder
    {
        public Task BindModelAsync(ModelBindingContext bindingContext)
        {
            if (bindingContext == null)
                throw new ArgumentNullException(nameof(bindingContext));
            var model = new UpdateApplicationCommand()
            {
                ApplicationEditGeneralViewModel = new ApplicationEditGeneralViewModel()
                {
                    Description = bindingContext.ValueProvider.GetValue("Description").ToString(),
                    Name = bindingContext.ValueProvider.GetValue("Name").ToString()
                }
            };
            bindingContext.Result = ModelBindingResult.Success(model);
            return Task.CompletedTask;
        }
    }
    

    Apply the custom model binding like below:

    [HttpPost]
    public async Task<IActionResult> Edit([ModelBinder(typeof(CustomModelBinder))]UpdateApplicationCommand model)
    {
        //.....
    }
    

    The second way, just change your razor view like below:

    @model UpdateApplicationCommand
    
    <form method="post">
        <div class="form-floating mb-3">
            @Html.TextBoxFor(m => m.ApplicationEditGeneralViewModel.Name, new { @class = "form-control", placeholder = "Application Name"})
            <label for="Name">Application Name</label>
        </div>
        <div class="form-floating mb-3">
            @Html.TextBoxFor(m => m.ApplicationEditGeneralViewModel.Description, new { @class = "form-control", placeholder = "Application Description"})
            <label for="Description">Application Description</label>
        </div>
        <div class="d-flex flex-row-reverse bd-highlight">
            <input type="submit" value="Submit" class="btn btn-primary mt-2" />
        </div>
    </form>