Right from the start, developers are taught to minimse the amount of database calls to only those necessary to obtain the data for a view. Tyically, in a tabbed interface, the view consists of the content of the first tab only:
The user may only need to see the contacts in this example. If you obtain the data for all the other tabs unnecessarily, this can hurt your application's performance. Resources are required to extract the data from the database, generate the HTML for the other tabs, and render that in the browser. If users perceive your application to be sluggish, they soon become frustrated with it, leading to a greater likelihood of the application being rejected.
The solution is to only load data for other tabs on demand - a pattern known as Lazy Loading.
The following example illustrates use of the pattern within a Razor Pages application. To begin, here is a PageModel that gets a product from a database:
public class TabsModel : PageModel { private readonly IOrderService orderService; public TabsModel(IProductService productService) { this.productService = productService; } [BindProperty(SupportsGet = true)] public int ProductId { get; set; } = 1; public Product Product { get; set; } public async Task OnGetAsync() { Product = await productService.GetProductAsync(ProductId); } }
Here's a tabbed interface that utilises Bootstrap:
<h1 class="display-4">Lazy Loading Tabs From Database</h1> <h3>@Model.Product.ProductName</h3> <input type="hidden" asp-for="Product.ProductId" /> <ul class="nav nav-tabs" id="myTab" role="tablist"> <li class="nav-item"> <a class="nav-link active" id="product-tab" data-toggle="tab" href="#product" aria-controls="product" aria-selected="true">Details</a> </li> <li class="nav-item"> <a class="nav-link" id="supplier-tab" data-toggle="tab" href="#supplier" aria-controls="supplier" aria-selected="false">Supplier</a> </li> <li class="nav-item"> <a class="nav-link" id="orders-tab" data-toggle="tab" href="#orders" aria-controls="orders" aria-selected="false">Orders</a> </li> </ul> <div class="tab-content p-3 border-right border-left"> <div class="tab-pane fade show active" id="product" role="tabpanel" aria-labelledby="product-tab"> <dl class="row"> <dt class="col-sm-2">Quantity Per Unit</dt><dd class="col-sm-10">@Model.Product.QuantityPerUnit</dd> <dt class="col-sm-2">Unit Price:</dt><dd class="col-sm-10">@Model.Product.UnitPrice</dd> <dt class="col-sm-2">Units In Stock:</dt><dd class="col-sm-10">@Model.Product.UnitsInStock</dd> <dt class="col-sm-2">Units On Order:</dt><dd class="col-sm-10">@Model.Product.UnitsOnOrder</dd> </dl> </div> <div class="tab-pane fade" id="supplier" role="tabpanel" aria-labelledby="supplier-tab"></div> <div class="tab-pane fade" id="orders" role="tabpanel" aria-labelledby="orders-tab"></div> </div>
The tabbed interface is generated by an unordered list (although you don't
have to use a ul
element) with the classes nav
and
nav-tabs
applied. Each list item forms the actual tab, and the
anchor element within the list item is used to generate the tab label, and
to control selection. There are three tabs here, one of which has the
active
class applied to its anchor element. All this does is to apply
a different style to the tab.
In this example, the data-toggle
attribute is used to
declaritively control switching between tabs. You could remove this
attribute and write code to show and hide tabs if you prefer. The content is
placed in divs with a tab-pane
class inside a div with a
tab-content
class. This combination of classes is used to control
visibility of the active tab content. The first tab pane also has fade
,
show
and active
classes. You use active
to set the default tab. The fade
class is used to animate the
display of the content. The show
class is used with fade
to make the content visible by default. The content for the first tab is
generated on the server and forms part of the initial view. The other tab
panes are emtpy. They will be loaded on demand.
The easiest way to manage loading HTML content on demand is to use Partial results on the server and call them using AJAX. So the next step is to alter the PageModel by adding two new methods to generate the HTML for the two tabs:
public class TabsModel : PageModel { private readonly IOrderService orderService; private readonly IProductService productService; private readonly ISupplierService supplierService; public TabsModel(IOrderService orderService, IProductService productService, ISupplierService supplierService) { this.orderService = orderService; this.productService = productService; this.supplierService = supplierService; } [BindProperty(SupportsGet = true)] public int ProductId { get; set; } = 1; public Product Product { get; set; } public async Task OnGetAsync() { Product = await productService.GetProductAsync(ProductId); } public async Task<PartialViewResult> OnGetSupplierAsync() { var supplier = await supplierService.GetSupplierForProduct(ProductId); return Partial("_SupplierDetails", supplier); } public async Task<PartialViewResult> OnGetOrdersAsync() { var details = await orderService.GetOrdersForProduct(ProductId); return Partial("_OrdersByProduct", details); } }
OnGetSupplierAsync
calls a service method that obtains
details of a supplier from a database and then passes the data to a Partial
page, returning the generated HTML in the response. The second method,
OnGetOrdersAsync
goes through the same process to obtain orders for
the product. Here is the _OrdersByProduct partial:
@model List<OrderDetails> <table class="table-sm table"> <thead class="thead-light"> <tr> <th>Customer</th> <th>Date</th> <th>Total Ordered</th> <th>Total Value</th> </tr> </thead> @foreach (var order in Model.OrderByDescending(o => o.Order.OrderDate)) { <tr> <td>@order.Order.Customer.CompanyName</td> <td>@order.Order.OrderDate.ToShortDateString()</td> <td>@order.Quantity</td> <td>@(order.UnitPrice * order.Quantity)</td> </tr> } </table>
Finally, here is a client side script that fires in response to shown.bs.tab
, which is a custom
Bootstrap jQuery event that fires after the tab has been shown:
@section scripts{ <script> var supplierLoaded = false; var ordersLoaded = false; var productid = $('#Product_ProductId').val(); $(function () { $('a[data-toggle="tab"]').on('shown.bs.tab', function (e) { switch ($(e.target).attr('aria-controls')) { case "supplier": if (!supplierLoaded) { $('#supplier').load(`/tabs/supplier?productid=${productid}`) supplierLoaded = true; } break; case "orders": if (!ordersLoaded) { $('#orders').load(`/tabs/orders?productid=${productid}`) ordersLoaded = true; } break; } }); }); </script> }
The booleans declared at the beginning of the script are used to detect
whether a specific tab has already been loaded or not. The aria-controls
attribute of the tab that was clicked is used to determine which tab was
clicked. If that tab has not already been loaded, the jQuery load
function is used to call the correct named handler method for the tab, and to
place the response of the AJAX call into the tab pane.
If you prefer to use plain JavaScript (i.e. no jQuery), here is the same fuctionality, using the Fetch API to make the AJAX call:
@section scripts{ <script> var supplierLoaded = false; var ordersLoaded = false; var productid = document.getElementById('Product_ProductId').value; load = function (url, el) { fetch(url) .then((response) => { return response.text(); }) .then((result) => { document.getElementById(el).innerHTML = result; }); } document.querySelectorAll('a[data-toggle="tab"]').forEach(el => el.addEventListener('click', (e) => { switch (e.target.getAttribute('aria-controls')) { case "supplier": if (!supplierLoaded) { load(`/tabs/supplier?productid=${productid}`, 'supplier'); } supplierLoaded = true; break; case "orders": if (!ordersLoaded) { load(`/tabs/supplier?productid=${productid}`, 'orders') ordersLoaded = true; } break; } })); </script> }
Note that the event handler is now the click
event, not
shown.bs.tab
, because you can't use addEventListener
with
custom jQuery events.
Summary
This demo shows that you don't have to load all of the data for a complex UI in one go. You can use lazy loading to only load data if and when it is required. Bootstrap provides custom events that you can hook into when you use the jQuery approach to work with tabs. But it's just as easy to use a non-jQuery solution if you prefer.