ساخت تب‌های Lazy Loading با Razor Pages و Bootstrap

دوشنبه 27 مرداد 1399

رابط‌های تب‌بندی شده یک روش عالی برای مدیریت نمایش مقادیر زیادی از اطلاعات در پنل‌های جداگانه هستند، که داده‌های هر پنل به تنهایی معنادار می‌باشد، و در هر زمان فقط یک پنل قابل مشاهده است. تب‌های مرورگر نمونه‌ای عالی از این مورد هستند. از نظر توسعه‌دهنده Razor Pages، تب‌ها مخصوصا برای کنترل نمایش داده‌های پیچیده در برنامه‌های تجاری سودمند هستند.

ساخت تب‌های Lazy Loading با Razor Pages و Bootstrap

از ابتدای کار، توسعه‌دهندگان می‌آموزند میزان فراخوانی‌های دیتابیس را به حداقل برسانند تا فقط داده‌های مورد نیاز را برای نمایش بگیرند. به طور معمول، در یک رابط تب‌بندی شده، نمای قابل مشاهده فقط محتوای اولین تب است:

در این مثال کاربر فقط ممکن است نیاز به مشاهده contacts داشته باشد، اگر شما داده‌های مربوط به سایر تب‌های که غیر ضروری است را بگیرید، این کار می‌تواند به عملکرد برنامه شما آسیب برساند. منابع برای استخراج داده‌ها از دیتابیس، تولید HTML برای تب‌ها، و رندر کردن آن در مرورگر، لازم هستند. اگر کاربران پی ببرند که برنامه شما کند است، به زودی از آن ناامید شده و به احتمال زیاد از این برنامه می‌گذرند.

راه‌حل این است که داده‌های تب‌های دیگر فقط در صورت تقاضا لود شوند؛ الگویی که به عنوان Lazy Loading شناخته می‌شود.

مثال زیر استفاده از این الگو در برنامه Razor Pages را نشان می‌دهد. برای شروع اینجا یک PageModel  داریم که product را از دیتابیس دریافت می‌کند:

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);
    }
}

در اینجا یک رابط تب‌بندی است که از بوت‌استرپ استفاده می‌کند:

<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>

رابط تب‌بندی توسط یک لیست نامنظم (اگرچه مجبور نیستید شما هم از ul استفاده کنید) با به کارگیری کلاس nav و nav-tabs تولید شده است. هر آیتم لیست یک تب را تشکیل می‌دهد و anchor element موجود در آیتم لیست برای تولید برچسب تب، و کنترل انتخاب استفاده می‌شود. در اینجا سه تب وجود دارد که یکی از آن‌ها دارای کلاس active است. همه این کارها برای این است که استایل متفاوتی را برای تب اعمال کنیم.

در این مثال، اتربیوت data-toggle برای کنترل سوئیچ بین تب‌ها استفاده شده است. در صورت تمایل می‌توانید این اتربیوت را حذف کنید و کدی را برای نشان دادن و پنهان کردن تب‌ها بنویسید. محتوا در divها با کلاس tab-pane در داخل یک div با کلاس tab-content قرار گرفته است. این ترکیب کلاس‌ها برای کنترل قابلیت دیدن محتوای تب فعال استفاده می‌شود. اولین تب همچنین کلاس‌های fade، show و active را دارد. از active برای تنظیم تب پیش‌فرض استفاده می‌شود. کلاس fade برای افکت نمایش محتوا استفاده می‌‌شود. کلاس show با fade استفاده می‌شود تا محتوا به طور پیش‌فرض قابل مشاهده باشد. محتوای اولین تب بر روی سرور تولید می‌شود و بخشی از نمای اولیه را تشکیل می‌دهد. سایر تب‌ها خالی هستند. آن‌ها در صورت تقاضا لود می‌شوند.

ساده‌ترین راه برای مدیریت لود محتوای HTML مورد تقاضا، استفاده از نتایج Partial  بر روی سرور و فراخوانی آن‌ها با استفاده از AJAX است. بنابراین مرحله بعدی اصلاح PageModel با استفاده از افزودن دو متد جدید برای تولید HTML برای تب‌ها است:

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 یک سرویس متد را فراخوانی می‌کند که جزئیات supplier را از دیتابیس می‌گیرد و سپس داده‌ها را به یک صفحه Partial می‌فرستد، و HTML تولید‌شده را در پاسخ باز می‌گرداند. متد دوم، OnGetOrdersAsync از طریق همان فرآیند پیش می‌رود تا سفارشات محصول را بگیرد. اینجا پارشیال OrdersByProduct_ است:

@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>

در نهایت، اینجا اسکریپت سمت کلاینت است که در پاسخ به shown.bs.tab فعال می‌شود، که یک رویداد سفارشی Bootstrap jQuery است که بعد از نمایش تب فعال می‌شود:

@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>
}

بوت‌استرپ اعلان‌شده در شروع اسکریپت برای تشخیص اینکه آیا یک تب خاص قبلا لود شده است یا نه استفاده می‌شود. اتربیوت aria-controls تب که کلیک شده است برای تشخیص اینکه کدام تب کلیک شده است استفاده می‌شود. اگر آن تب قبلا لود نشده باشد، تابع load جی‌کوئری برای فراخوانی handler method نام‌گذاری‌شده صحیح تب استفاده می‌شود و پاسخ فراخوانی AJAX را در تب قرار می‌دهد.

اگر شما ترجیح می‌دهید از جاوااسکریپت ساده (بدون جی‌کوئری) استفاده کنید، در اینجا همان قابلیت، با استفاده از Fetch API برای ساخت فراخوانی AJAX است:

@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>
}

توجه داشته باشید که event handler در حال حاضر رویداد click است، نه shown.bs.tab، زیرا نمی‌توانید از addEventListener با رویدادهای سفارشی جی‌کوئری استفاده کنید.

خلاصه

این دمو نشان می‌دهد لازم نیست همه داده‌ها برای یک رابط کاربری پیچیده یک باره بارگیری شوند. شما می‌توانید در صورت لزوم از lazy loading برای لود داده‌ها استفاده کنید. بوت‌استرپ رویدادهای سفارشی‌ای را ارائه می‌دهد که می‌توانید هنگام استفاده از رویکردهای جی‌کوئری برای کار با تب‌ها استفاده کنید. اما استفاده از یک راهکار غیر جی‌کوئری، در صورت تمایل، بسیار ساده است.

برنامه نویسان

نویسنده 3201 مقاله در برنامه نویسان

کاربرانی که از نویسنده این مقاله تشکر کرده اند

تاکنون هیچ کاربری از این پست تشکر نکرده است

در صورتی که در رابطه با این مقاله سوالی دارید، در تاپیک های انجمن مطرح کنید