2

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:

  • From 08:00 to 20:00
  • From 08:00 to 13:00 and from 17:00 to 20:00

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.

Jon
  • 891
  • 13
  • 32

1 Answers1

2

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();
});
Jerdine Sabio
  • 5,688
  • 2
  • 11
  • 23
  • if the model is not capturing the list `OpeningHours`.. looks like you need to include the partial view in your question, since the input fields there should have indexes in the html name attribute. – Jerdine Sabio Mar 01 '22 at 14:44
  • You are right, removing ```$('#submit').click()``` ```OpeningHours``` is not captured properly, because all input fields have the same name: ```From``` and ```To```. I'm going to edit the question with needed code. – Jon Mar 01 '22 at 14:47
  • Partial view and controller action added. – Jon Mar 01 '22 at 14:56
  • @Jon I updated the code, actually we could just rename the input fields so that they can be bound properly – Jerdine Sabio Mar 01 '22 at 14:57
  • I tested it but the form is not submited. I changed ```$("#createForm").submit();``` with ```document.getElementById('createForm').submit();``` and with this I get this error in the console: ```Uncaught TypeError: document.getElementById(...).submit is not a function```. – Jon Mar 01 '22 at 15:19
  • Ok, it is because ```submit``` ```input```'s ```id``` is ```submit``` too, changing this ```id``` works perferct! Thank you very much!!! – Jon Mar 01 '22 at 15:20