In this series, I will use the Northwind sample database to provide familiar data,and Entity Framework Core for data access. Bootstrap is included as the default UI framework for all Razor Pages applications built using the standard ASP.NET Core Web Application project template:
The sample application for this article will display a list of products, and clicking in a button will invoke a modal displaying details for that product:
The HTML for the initial list of products is generated on the server. When the use clicks on a Details button, an AJAX call is made to obtain the product details. The AJAX call can return HTML or JSON. This article covers both options.
Communication with the database is separated into a service class,
ProductService
, containing the following code:
public class ProductService : IProductService { private readonly NorthwindContext context; public ProductService(NorthwindContext context) => this.context = context; public async Task<Dictionary<int, string>> GetProductListAsync() => await context.Products.ToDictionaryAsync(k => k.ProductId, v => v.ProductName); public async Task<Product> GetProductAsync(int id) => await context.Products.Include(p => p.Category).Include(p => p.Supplier).FirstOrDefaultAsync(p => p.ProductId == id); }
It contains two methods, one that returns the product Ids and names as a dictionary, and one that returns details of a particular product. The service is registered in
Startup
, along with the context, which in my examples makes use of a Sqlite version of Northwind:
public void ConfigureServices(IServiceCollection services) { services.AddRazorPages(); services.AddDbContext<NorthwindContext>(options => { options.UseSqlite($"Data Source={Environment.ContentRootPath}/Data/Northwind.db"); }); services.AddScoped<IProductService, ProductService>(); }
Now I can inject the service into the PageModel class constructor to make use of
it to populate the ProductList
dictionary:
public class MasterDetailsModel : PageModel { private readonly IProductService productService; public MasterDetailsModel(IProductService productService) => this.productService = productService; public Dictionary<int, string> ProductList { get; set; } = new Dictionary<int, string>(); public async Task OnGetAsync() { ProductList = await productService.GetProductListAsync(); } }
The dictionary is displayed in a table:
<table class="table table-sm table-borderless" style="max-width:50%"> @foreach (var item in Model.ProductList) { <tr> <td>@item.Value</td> <td><button class="btn btn-sm btn-dark details" data-id="@item.Key">Details</button></td> </tr> } </table>
So far so good - you can see that the table uses standard Bootstrap styling
classes, and that each product is accompanied by a Bootstrap styled button that
has a data
attribute set to the value if the product's ID. At the moment, nothing happens if you click the button. This first pass
will demonstrate adding a modal and populating it with a snippet of HTML. The
steps required are to:
- Add a Bootstrap modal
- Create a method that generates HTML to display a product's details
- Add an AJAX call to obtain the HTML and pass it to the Modal
- Wire the Modal up to the buttons
Here is the HTML for the modal. It is added to the page containing the list of products:
<div class="modal fade" tabindex="-1" role="dialog" id="details-modal"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title">Product Details</h5> <button type="button" class="close" data-dismiss="modal" aria-label="Close"> <span aria-hidden="true">×</span> </button> </div> <div class="modal-body"></div> </div> </div> </div>
This is a standard modal more or less copied directly from the
Bootstrap Modal docs.
It's been given an id
attribute so that it can be referenced
elsewhere, and it has the fade
CSS class applied so that its appearance is animated.
The div element with the CSS class of modal-body
is empty. This
will be populated with HTML obtained via an AJAX call to a
named page handler method. The HTML will be generated by a
Razor
partial page (_ProductDetails.cshtml):
@model Product <div> <span class="d-inline-block">Product:</span><span>@Model.ProductName</span> </div> <div> <span class="d-inline-block">Category:</span><span>@Model.Category.CategoryName</span> </div> <div> <span class="d-inline-block">Quantity Per Unit :</span><span>@Model.QuantityPerUnit</span> </div> <div> <span class="d-inline-block">Unit Price:</span><span>@Model.UnitPrice</span> </div> <div> <span class="d-inline-block">Units In Stock:</span><span>@Model.UnitsInStock</span> </div> <div> <span class="d-inline-block">Units On Order:</span><span>@Model.UnitsOnOrder</span> </div> <div> <span class="d-inline-block">Discontinued</span><span><input type="checkbox" readonly checked="@Model.Discontinued" /></span> </div> <div> <span class="d-inline-block">Date Discontinued</span><span>@Model.DiscontinuedDate?.ToShortDateString()</span> </div> <div> <span class="d-inline-block">Supplier</span><span>@Model.Supplier.CompanyName</span> </div>
The model for the partial is a Product
entity whose details are rendered
within a series of span elements. Next step is to add a handler method that
makes use of the partial and returns HTML:
public async Task<PartialViewResult> OnGetProductAsync(int id) { return Partial("_ProductDetails", await productService.GetProductAsync(id)); }
Then a route template is added to the page
directive so that the
handler method name can be incorporated into the URL as a segment instead of a
query string value:
@page "{handler?}"
The next step is to create some JavaScript to call the page model handler method,
passing in the Id of the selected product. This is obtained from the data-id
attribute on the button:
@section scripts{ <script> $(function () { $('button.details').on('click', function () { $('.modal-body').load(`/masterdetails/product?id=${$(this).data('id')}`); }); }) </script> }
The script makes use of the jQuery load
method, which performs a
GET
request and places the response into the matched element that
the load
method is called on, in this case modal-body
. Finally, the buttons added to the table need to be modified to trigger the
modal's visibility by adding data-toggle
and data-target
attributes, the second of which references the id
of the modal:
<button class="btn btn-sm btn-dark details" data-id="@item.Key" data-toggle="modal" data-target="#details-modal">Details</button>
When the button is clicked, an asynchronous call is made to the
OnGetProductAsync
handler
And the modal is invoked, with the HTML response loaded in to the body:
If you have to, or prefer to work with JSON, the data binding takes place in the browser rather than on the server so there is no need for a Partial. Instead, the handler method returns the data serialised as JSON:
public async Task<JsonResult> OnGetProductAsJsonAsync(int id) { return new JsonResult(await productService.GetProductAsync(id)); }
The modal body is set, with very similar HTML to the partial
<div class="modal fade" tabindex="-1" role="dialog" id="details-modal"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title">Product Details (JSON)</h5> <button type="button" class="close" data-dismiss="modal" aria-label="Close"> <span aria-hidden="true">×</span> </button> </div> <div class="modal-body"> <div> <span class="d-inline-block">Product:</span><span id="product"></span> </div> <div> <span class="d-inline-block">Category:</span><span id="category"></span> </div> <div> <span class="d-inline-block">Quantity Per Unit :</span><span id="quantity"></span> </div> <div> <span class="d-inline-block">Unit Price:</span><span id="price"></span> </div> <div> <span class="d-inline-block">Units In Stock:</span><span id="instock"></span> </div> <div> <span class="d-inline-block">Units On Order:</span><span id="onorder"></span> </div> <div> <span class="d-inline-block">Discontinued</span><span><input type="checkbox" readonly id="discontinued" /></span> </div> <div> <span class="d-inline-block">Date Discontinued</span><span id="discontinued-date"></span> </div> <div> <span class="d-inline-block">Supplier</span><span id="supplier"></span> </div> </div> </div> </div> </div>
The spans that will hold the data values have an id
attribute to
make referencing in script easier, speaking of which, here is the revised script
for calling the new handler method, and processing the response:
$('button.details').on('click', function () { $.getJSON(`/masterdetails/productasjson?id=${$(this).data('id')}`).done(function (product) { $('#product').text(product.productName); $('#category').text(product.category.categoryName); $('#quantity').text(product.quantityPerUnit); $('#price').text(product.unitPrice); $('#instock').text(product.unitsInStock); $('#onorder').text(product.unitsOnOrder); $('#discontinued').text(product.discontinued); $('#discontinued-date').text(product.discontinuedDate); $('#supplier').text(product.supplier.companyName); }); });
This time, the jQuery getJSON
method is used, and the individual
data values are plugged in to the DOM. The end result is the same.
Summary
This article looks at a couple of ways to manage master/detail scenarios using Bootstrap modals and Razor Pages. The first demonstrated using a partial to generate HTML to be plugged in to the modal, and the second looked at returning JSON, and binding that data in the client, using jQuery. Working with HTML is easier, especially as you have Intellisense support in the partial view. If you work with JSON, jQuery is fine for simple scenarios, but you might also want to consider a more formal templating solution such as Knockout or Vuejs for databinding in the client.