Introduction
I've spent quiet some time researching on this topic and couldn't find any place with complete implementation. Even working under the ChatGPT assistance, it took me couple of days to have a final clean solution.
Background
Once again, this is for Razor pages webproject in Visual Studio 2022, but it could be applied to MVC as well as MVC has the same concept of Partial views.
How To
We are going to deal with just three parts of the Razor pages web project: Index.cshtml, Index.cshtml.cs and _SelectTable.cshtml partial view. This is a bare minimum UI without any fancy stuff, to show just the concept. Please refer to this folders structure to have an idea of files location (files of our interest are highlighted in yellow).
The Partial View (_SelectTable.cshtml)
@model TestPartialViewWithAjax.Pages.IndexModel
@{
}
<table id="selectTable">
<thead>
<tr>
<th>Select Element</th>
<th>Action</th>
</tr>
</thead>
<tbody>
@if (Model.SelectedValues != null)
{
@for (var i = 0; i < Model.SelectedValues.Count; i++)
{
<tr>
<td>
<select asp-for="SelectedValues[i]">
@foreach (var item in Model.YourSelectList)
{
var selected = Model.SelectedValues[i] == item.Value ?
"selected" : "";
if (Model.SelectedValues[i] == item.Value)
{
<option value="@item.Value" selected>
@item.Text</option>
} else
{
<option value="@item.Value">@item.Text</option>
}
}
</select>
</td>
<td>
<button type="button" class="deleteRowButton">Delete</button>
</td>
</tr>
}
}
</tbody>
</table>
In this page, we render table rows with select elements. Selected option of existing rows is preserved when a new row is added. Rows can also be deleted, deletion is done by means of pure JavaScript block in the parent cshtml page.
The Parent page (Index.cshtml)
@page
@using Newtonsoft.Json;
@model IndexModel
@{
ViewData["Title"] = "Home page";
}
<div class="text-center">
<h1 class="display-4">Welcome</h1>
<p>Learn about <a href="https://docs.microsoft.com/aspnet/core">
building Web apps with ASP.NET Core</a>.</p>
<br /><br />
<h3>List of Nominees</h3>
<form method="post">
<div id="selectTableContainer">
<partial name="_SelectTable" model="Model" />
</div>
<button type="button" id="addRowButton">Add Row</button>
<button type="submit">Submit</button>
</form>
</div>
@section Scripts {
<script>
document.getElementById("addRowButton").addEventListener("click", function () {
var selectedValues = [];
$('select').each(function () {
selectedValues.push($(this).val());
});
var requestData = {
YourSelectList: @Html.Raw
(JsonConvert.SerializeObject(Model.YourSelectList)),
SelectedValues: selectedValues
};
var serializedModel = JSON.stringify(requestData);
$.ajax({
url: '/?handler=AddRow',
method: "POST",
contentType: "application/json; charset=utf-8",
headers: {
RequestVerificationToken:
$('input:hidden[name="__RequestVerificationToken"]').val()
},
data: JSON.stringify(serializedModel),
success: function (result) {
$("#selectTableContainer").html(result);
},
error: function () {
alert("Failed to add a new row.");
}
});
});
$("#selectTableContainer").on("click", ".deleteRowButton", function () {
var row = $(this).closest("tr");
row.remove();
});
</script>
}
We have <partial name="_SelectTable" model="Model" />
partial view included in the <form>
where we add new table rows. Partial view shares parent page's Model. DeleteRow
method uses pure JavaScript and simply manipulates HTML DOM to remove fields from the form. AddRowButton
click uses Ajax call to the page behind AddRow
method. Note, that this post call doesn't re-render the page, it updates partial view portion only.
Submit button is of type "submit" and as such, it posts back and refreshes/redirects the whole page. It submits the array of selections of the whole table, so that the parent knows all user interactions from Partial view.
Here, you could add jquery datatable plugin to have a fancy table stuff like sorting, searching, numbering, drag-and-drop, etc.
The Parent Page Behind (Index.cshtml.cs)
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
using Newtonsoft.Json;
namespace TestPartialViewWithAjax.Pages
{
public class IndexModel : PageModel
{
private readonly ILogger<IndexModel> _logger;
[BindProperty]
public List<string> SelectedValues { get; set; }= new List<string>();
public List<SelectListItem> YourSelectList { get; set; }
public IndexModel(ILogger<IndexModel> logger)
{
_logger = logger;
}
public void OnGet()
{
YourSelectList = new List<SelectListItem>
{
new SelectListItem { Value = "Default", Text = "Default",
Disabled = false,Group = null,Selected = false },
new SelectListItem { Value = "Option1", Text = "Option 1",
Disabled = false,Group = null,Selected = false },
new SelectListItem { Value = "Option2", Text = "Option 2",
Disabled = false,Group = null,Selected = false },
new SelectListItem { Value = "Option3", Text = "Option 3",
Disabled = false,Group = null,Selected = false }
};
SelectedValues.Add("Default");
}
public IActionResult OnPost()
{
return RedirectToPage("./Index");
}
public IActionResult OnPostAddRow([FromBody] string serializedModel)
{
var model = JsonConvert.DeserializeObject<IndexModel>(serializedModel);
model.SelectedValues ??= new List<string>();
model.SelectedValues.Add("Default");
return Partial("_SelectTable", model);
}
}
}
Here, OnPostAddRow
method simply adds one new item to the list and returns Partial view updated HTML code back to DOM adding more fields to the main form.
Please note, to avoid confusion for some developers, when the OnPostAddRow
method is called from Ajax the IndexModel
class is initialized as new one, with all default/empty properties. There is no any binding happens here ([BindProperty]
is ignored), that's why we provide serialized data from the parent to send it to the Partial view.
Conclusion
The code is pasted from the workable VS 2022 solution and should be ready for running/testing/reviewing/debugging. The only thing that might be needed for successful compiling is to add Newtonsoft.Json package (if VS 2022 won't do this automatically for you).
History
- 26th September, 2023: Initial version