I want to register an unknown number of users, and would like to avoid having to do a separate form submit per user.
I think I have figured out the view, where I can add and remove rows of users (Thanks to this article).
But I can't seem to get it to bind to the controller POST method. Users are returning null.
Update
Adding [Bind("Users")] to the controller method fixed the null problem! Now I'm struggeling with the indecies of the rows.
Update 2
Solved! Adding <input type="hidden" name="Users.Index" value="{some unique value}" /> sorts out the indexing problem. I can now consider the index to be non-sequential, and not worry about maintaining the sequence. I have replaced the lastIndex introduced in the answer below, with a timestamp. It could be any random number, as long as it is unique to each row in the form.
This is what I have now:
Models
public class ApplicationUserViewModel
{
public string Email { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
public class BatchUserRegistrationViewModel
{
public List<ApplicationUserViewModel> Users { get; set; }
}
Create view
@model NameSpace.Models.BatchUserRegistrationViewModel
<form asp-action="RegisterManyUsers">
@if (Model.Users != null)
{
// This part is for when the ModelState is not valid, and POST redirects back to the view.
// I want the submitted form data to persist.
for (int i = 0; i < Model.Users.Count; i++)
{
<div id="inputFormRow">
<div class="input-group mb-3">
<input type="hidden" name="Users.Index" value="@i" />
<input type="text" name="users[@i].Email">
<input type="text" name="users[@i].FirstName>
<input type="text" name="users[@i].LastName">
<div class="input-group-append">
<button id="removeRow" type="button">
(-) Remove row
</button>
</div>
</div>
</div>
}
}
else
{
// This part is for a new, blank form:
<div id="inputFormRow">
<div class="input-group mb-3">
<input type="hidden" name="Users.Index" value="0" />
<input type="text" name="users[0].Email">
<input type="text" name="users[0].FirstName">
<input type="text" name="users[0].LastName">
<div class="input-group-append">
<button id="removeRow" type="button">
(-) Remove row
</button>
</div>
</div>
</div>
}
<div id="newRow"></div>
<p>
<button id="addRow" type="button">
(+) Add row
</button>
</p>
<p>
<button type="submit">(v) Register users</button>
</p>
</form>
Client script
<script>
// add row
$("#addRow").click(function () {
let ts = $.now();
var html = '';
html += '<div id="inputFormRow">';
html += ' <div class="input-group mb-3">';
html += ' <input type="hidden" name="Users.Index" value="' + ts + '" />';
html += ' <input type="text" name="users[' + ts + '].Email">';
html += ' <input type="text" name="users[' + ts + '].FirstName">';
html += ' <input type="text" name="users[' + ts + '].LastName">';
html += ' <div class="input-group-append">';
html += ' <button id="removeRow" type="button">';
html += ' (-) Remove row';
html += ' </button>';
html += ' </div>';
html += ' </div>';
html += '</div>';
$('#newRow').append(html);
});
// remove row
$(document).on('click', '#removeRow', function () {
$(this).closest('#inputFormRow').remove();
});
</script>
Controller POST-method
public async Task<IActionResult> RegisterManyUsers([Bind("Users")] BatchUserRegistrationViewModel BatchReg)
{
if (ModelState.IsValid)
{
if (BatchReg.Users != null)
{
List<ApplicationUser> dbUsers = new();
foreach (ApplicationUserViewModel user in BatchReg.Users)
{
dbUsers.Add(new ApplicationUser {
Email = user.Email,
FirstName = user.FirstName,
LastName = user.LastName
});
}
db.AddRange(dbUsers);
await db.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
}
return View(BatchReg);
}