javascriptc#asp.netasp.net-mvcasp.net-core-mvc

Submit changed FormData in ASP.NET form to have normal behavior with ModelState and redirections


I'm developing a web app with APS.NET Core MVC (.NET 5.0). I have an entity (Service) that have a dynamic list of another entity (OpeningHours). So a service can have different opening hours, for example:

You can set different time slots, as many as you want. I didn't know how to implement this case and looking for the solution I found How to dynamically add items from different entities to lists in ASP.NET Core MVC and followed the answer adapting it to my entities. Simplifying a bit, this would be the code:

Models (or ViewModels):

public class Service
{
    public int Id { get; set; }
    [Required]
    public string Name { get; set; }
    [Required]
    public string Description { get; set; }
    public List<ServiceOpeningHours> OpeningHours { get; set; } = new List<ServiceOpeningHours>();
}

public class ServiceOpeningHours
{
    public TimeSpan From { get; set; }
    public TimeSpan To { get; set; }
}

Create.cshtml (View):

@model MyWebApp.Services.Models.Service

...

<form asp-action="Create" name="createForm" id="createForm">
    <div asp-validation-summary="ModelOnly" class="text-danger"></div>
        <div class="form-group">
            <label class="control-label">Name</label>
            <input asp-for="Name" class="form-control" />
            <span asp-validation-for="Name" class="text-danger"></span>
        </div>
        <div class="form-group">
            <label class="control-label">Description</label>
            <input asp-for="Description" class="form-control" />
            <span asp-validation-for="Description" class="text-danger"></span>
        </div>
        <fieldset>
            <legend>Opening Hours</legend>
            <div id="openingHoursContainer">
                @foreach (ServiceOpeningHours item in Model.OpeningHours)
                {
                    <partial name="_OpeningHourEditor" manifest model="item" />
                }
            </div>
        </fieldset>

        <div>
            <div class="form-group">
                <input id="addOpeningHourItem" type="button" value="Add Opening Hour" class="btn btn-primary" />
            </div>
            <div class="form-group">
                <input type="submit" id="submit" value="Create" class="btn btn-primary" />
            </div>
        </div>
    </form>

...

@section Scripts {
    $('#addOpeningHourItem').click(function (event) {
        event.preventDefault();
        $.ajax({
            async: true,
            data: $('#form').serialize(),
            type: 'POST',
            url: '/Services/AddBlankOpeningHour',
            success: function (partialView) {
                $('#openingHoursContainer').append(partialView);
            }
        });
    });
    $('#submit').click(function (event) {
        event.preventDefault();
        var formData = new FormData();

        formData.append("Name", $('input[name="Name"]').val());
        formData.append("Description", $('input[name="Description"]').val());

        $("input[name='From']").each(function (i) {
            var from = $(this).val();
            formData.append("OpeningHours[" + i + "].From", from);
        });
        $("input[name='To']").each(function (i) {
            var to = $(this).val();
            formData.append("OpeningHours[" + i + "].To", to);
        });

        formData.append("__RequestVerificationToken", $('form[name="createForm"] input[name="__RequestVerificationToken"]').val());
        
        $.ajax({
            method: 'POST',
            url: '/Services/Create',
            data: formData,
            processData: false,
            contentType: false,
            success: function (returnValue) {
                console.log('Success: ' + returnValue);
            }
        });
    });
}

_OpeningHourEditor.cshtml (partial view for opening hour item):

@model MyWebApp.Models.ServiceOpeningHours

<div class="row">
    <div class="col-md-6">
        <div class="form-group">
            <label class="control-label">From</label>
            <input asp-for="From" class="form-control" />
            <span asp-validation-for="From" class="text-danger"></span>
        </div>
    </div>
    <div class="col-md-6">
        <div class="form-group">
            <label class="control-label">To</label>
            <input asp-for="To" class="form-control" />
            <span asp-validation-for="To" class="text-danger"></span>
        </div>
    </div>
</div>

In the javascript code a FormData is created and filled with all fields and the model successfully arrives at the Create action.

ServiceController:

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Create(Service service)
{
    if (ModelState.IsValid)
    {
        // Save data in data base...

        return this.RedirectToAction("Index");
    }
    return View(service);
}

[HttpPost]
public ActionResult AddBlankOpeningHour()
{
    return PartialView("_OpeningHourEditor", new ServiceOpeningHours());
}

With this code, when the Create action returns the response, it reaches the success block of $.ajax(), but this does not cause the page to reload with the new data.

I think you shouldn't do it with AJAX. How should I make the call so that everything works normally? That is, if the Name or Description fields are not filled in, the ModelState error message should be displayed, and if all goes well it should be redirected to the Index action.


Solution

  • Modified the submit script to remove ajax and initially change the name of the input fields so that the list will be bound to the model properly.

    $('#submit').click(function (event) {
       // initially prevent form submit
       event.preventDefault();
       
       // loop through all input with name From, and change their name with index
       $("input[name='From']").each(function (i) {
          $(this).attr("name", "OpeningHours[" + i + "].From");
       });
       
       // loop through all input with name To, and change their name with index
       $("input[name='To']").each(function (i) {
          $(this).attr("name", "OpeningHours[" + i + "].To");
       });
       
       // submit the form
       $("#createForm").submit();
    });