Razor Pages And Bootstrap - Modal Master Details

This is the first in a series of posts exploring how to use some of the components provided by Bootstrap within a database-driven Razor Pages web application. In this article, I look at using the Bootstrap Modal in a Master/Details scenario, to display the details of the selected record in a master list.

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:

Project Templates

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:

Master/Details

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<intstring>> 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<IProductServiceProductService>();
}

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<intstring> ProductList { getset; } = new Dictionary<intstring>();
    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:

  1. Add a Bootstrap modal
  2. Create a method that generates HTML to display a product's details
  3. Add an AJAX call to obtain the HTML and pass it to the Modal
  4. 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">&times;</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

Razor Pages Bootstrap

And the modal is invoked, with the HTML response loaded in to the body:

Razor Pages Bootstrap

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">&times;</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.