Fetch and Display Paginated Data in an HTML Table Using Web API with Async/Await

← Prev

ASP.NET developers often rely on the powerful GridView control to present data in a structured, tabular format. One of its standout features is built-in paging, which enhances user experience by breaking large datasets into manageable chunks. In this tutorial, you'll learn how to replicate that functionality using a lightweight, client-side approach. We'll demonstrate how to fetch paginated data from a SQL Server database via an ASP.NET Web API, written in both C# and VB.NET and render it dynamically in an HTML table using plain JavaScript with async and await. This guide builds upon my previous article, where I showcased how to implement pagination using jQuery Ajax and Web API. Now, we're taking a modern, JavaScript-native route for better performance and cleaner code.

Build a Paginated Data Table with Web API and Async/Await in JavaScript

SQL Server Table Setup

To begin, we'll extract data from a SQL Server database table named Books, which stores essential information such as book titles, categories, prices, and more. This table serves as the foundation for our data-driven application. You can view the table structure here.

Once the table is created, the next step is to build a Web API using ASP.NET. This API will include a controller to handle HTTP requests and a model to represent the data structure. Together, they form the backbone of a clean and scalable architecture for retrieving and displaying paginated book data using modern JavaScript with async and await.

The Web API

🔹 Model (Books.cs in C#)

Let's begin by creating a Model for our API that will have few properties in it.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace BooksApp.Models
{
    public class Books
    {
        public int BookId { get; set; }
        public string BookName { get; set; }
        public string Category { get; set; }
        public decimal Price { get; set; }
        public decimal Total { get; set; }
    }
}

🔹 Model (Books.vb)

Imports System.Web

Namespace BooksApp.Models
    Public Class Books
        Public Property BookID() As Integer
            Get
                Return m_BookID
            End Get
            Set(value As Integer)
                m_BookID = value
            End Set
        End Property
        Private m_BookID As Integer

        Public Property BookName() As String
            Get
                Return m_BookName
            End Get
            Set(value As String)
                m_BookName = value
            End Set
        End Property
        Private m_BookName As String

        Public Property Category() As String
            Get
                Return m_Category
            End Get
            Set(value As String)
                m_Category = value
            End Set
        End Property
        Private m_Category As String

        Public Property Price() As Decimal
            Get
                Return m_Price
            End Get
            Set(value As Decimal)
                m_Price = value
            End Set
        End Property
        Private m_Price As Decimal

        Public Property Total() As Decimal
            Get
                Return m_Total
            End Get
            Set(value As Decimal)
                m_Total = value
            End Set
        End Property
        Private m_Total As Decimal
    End Class
End Namespace

🔹 The Controller "BooksController.cs" (C#)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.http;
using System.Web.http;

using BooksApp.Models;
using System.Data.SqlClient;

namespace BooksApp.Controllers
{
    public class BooksController : ApiController
    {
        List object will hold and return a list of books.
        List<Books> MyBooks = new List<Books>();

        string sConnString = "Data Source=DEMO;Persist Security Info=False;" +
            "Initial Catalog=DNA_Classified;User Id=sa;Password=sss;Connect Timeout=30;";

        public IEnumerable<Books> Get(int iPg)
        {
            getTheBooks(iPg);
            return MyBooks;
        }
        public void getTheBooks(int iPage)
        {
            int iPageSize = 5;      // You can change the page size according to your requirement.
            decimal iTotalPage = 0;

            SqlConnection myConn = new SqlConnection(sConnString);

            // SQL query to fetch books.
            string sSql = +
            "SELECT *, (SELECT COUNT(*) FROM dbo.Books) TotalBooks " +
                "FROM (SELECT ROW_NUMBER() OVER ( ORDER BY BookID ) AS RowNum, * " +
                        "FROM dbo.Books " +
                        ") AS r " +
                        "WHERE RowNum >= ((" + iPageSize + " * " + (iPage == 1 ? 0 : iPage - 1) + " ) + 1) " +
                    "AND RowNum <= (" + iPageSize + " * " + iPage + ") " +
                "ORDER BY RowNum";

            SqlCommand objComm = new SqlCommand(sSql, myConn);
            myConn.Open();

            SqlDataReader reader = objComm.ExecuteReader();

            // Populate the list with data.
            while (reader.Read())
            {
                iTotalPage = (int)reader["TotalBooks"];
                iTotalPage = Math.Round(iTotalPage / iPageSize, 0);

                MyBooks.Add(new Books { 
                    BookId = (int)reader["BookID"], 
                    BookName = reader["BookName"].ToString(),
                    Category = reader["Category"].ToString(),
                    Price = Convert.ToDecimal(reader["Price"]),
                    Total = iTotalPage
                });
            }

            myConn.Close();
        }
    }
}

🔹 The Controller "BooksController.vb" (VB.NET)

Option Explicit On

Imports System.Net.http
Imports System.Web.http

Imports System.Data.SqlClient
Imports BooksApp.BooksApp.Models

Namespace BooksApp
    Public Class BooksController
        Inherits ApiController

        List object will hold and return a list of books.
        Dim MyBooks As New List(Of Books)()

        Const sConnString As String = "Data Source=DEMO;Persist Security Info=False;" & _
           "Initial Catalog=DNA_Classified;User Id=sa;Password=sss;Connect Timeout=30;"

        Public Function [Get](iPg As Integer) As IEnumerable(Of Books)
            getTheBooks(iPg)
            Return MyBooks
        End Function

        Public Sub etTheBooks(ByVal iPage As Integer)

            Dim iPageSize As Integer = 5        // You can change the page size according to your requirement.
            Dim iTotalPage As Integer = 0

            Using con As SqlConnection = New SqlConnection(sConnString)

                // SQL query to fetch books.
                Dim sSQL = & _
                "SELECT *, (SELECT COUNT(*) FROM dbo.Books) TotalBooks " & _
                    "FROM (SELECT ROW_NUMBER() OVER ( ORDER BY BookID ) AS RowNum, * " & _
                            "FROM dbo.Books " & _
                            ") AS r " & _
                    "WHERE RowNum >= ((" & iPageSize & " * " & IIf(iPage = 1, 0, iPage - 1) & " ) + 1) " & _
                        "AND RowNum <= (" & iPageSize & " * " & iPage & ") " & _
                    "ORDER BY RowNum"

                Dim objComm As New SqlCommand(sSQL, con)
                con.Open()

                Dim reader As SqlDataReader = objComm.ExecuteReader()

                ' Populate the list with data.
                While reader.Read()
                    iTotalPage = reader("TotalBooks") / iPageSize

                    MyBooks.Add(New Books() With { _
                        .BookID = CInt(reader("BookID")), _
                        .BookName = reader("BookName").ToString(), _
                        .Category = reader("Category").ToString(), _
                        .Price = CDbl(reader("Price")), _
                        .Total = iTotalPage
                     })
                End While

                con.Close()
            End Using
        End Sub

    End Class
End Namespace

The Markup: Structuring the Page for Dynamic Data Display

The page starts with a simple <div> container. Using JavaScript, I'll dynamically build an HTML <table> with headers, rows, and pagination, all styled with custom CSS classes. These styles not only enhance the look but also drive the paging logic for a smooth user experience.

<!DOCTYPE html>
<html lang="en">
<head>
    <title>Display Paginated Data from SQL Server using Web API and Async/Await</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 20px;
        }

        #showData {
            max-width: 600px;
            margin: auto;
        }

        table {
            width: 100%;
            border-collapse: collapse;
            margin-bottom: 10px;
        }

        th, td {
            border: 1px solid #eee;
            padding: 8px;
            text-align: center;
        }

        th {
            background-color: #f5f5f5;
        }
        .pageCountActive, .pageCountInactive {
            width: 20px;
            float: left;
            border: solid 1px #EEE;
            text-align: center;
            padding: 2px 3px;
            margin: 1px;
            cursor: pointer;
            color: #1464F4;
        }
        .pageCountInactive {
            color:#999;
            cursor:default;
        }
        .pageCountActive a {
            text-decoration:none;
        }
    </style>
</head>
<body>
    <main>
        <section id="showData" aria-label="Book Data Table"></section>
    </main>
</body>

The Script

<script>
    function BindControls() {
        var page = 1;
        getData(page);

        async function getData(pg) {
            try {
                const response = await fetch(`/api/books/${pg}`);
                if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`);

                const data = await response.json();

                let myBooks = [];
                let col = [];

                data.forEach((book, i) => {
                    myBooks.push(book);
                });

                // Create table
                const table = document.createElement('table');

                // Get headers
                myBooks.forEach(book => {
                    for (let key in book) {
                        if (!col.includes(key)) {
                            col.push(key);
                        }
                    }
                });

                // Create header row
                let tr = table.insertRow(-1);
                col.forEach(header => {
                    const th = document.createElement("th");
                    th.innerHTML = header;
                    tr.appendChild(th);
                });

                // Add data rows
                myBooks.forEach(book => {
                    tr = table.insertRow(-1);
                    col.forEach(header => {
                        const tabCell = tr.insertCell(-1);
                        tabCell.innerHTML = book[header];
                    });
                });

                // Display the table
                const divContainer = document.getElementById('showData');
                divContainer.innerHTML = "";
                divContainer.appendChild(table);

                // Create pagination
                const divPager = document.createElement('div');
                for (let i = 1; i <= myBooks[0].Total; i++) {
                    const divPageCount = document.createElement('div');
                    divPageCount.innerHTML = pg === i
                        ? `<span class="pageCountInactive">${i}</span>`
                        : `<span class="pageCountActive" id="${i}">${i}</span>`;
                    divPager.append(divPageCount);
                }

                divContainer.append(divPager);

            } catch (error) {
                alert(`Error: ${error.message}`);
            }
        }

        // Enable pagination by listening for clicks on active page numbers.
        document.addEventListener('click', function (event) {
            const target = event.target;
    
            if (target.classList.contains('pageCountActive')) {
                const page = parseInt(target.id, 10);
                if (!isNaN(page)) {
                    getData(page);
                }
            }
        });
    }

    BindControls();
</script>
    

That's it.

Why async/await Is Better Than jQuery Ajax

As mentioned earlier, my previous example used jQuery Ajax to call the Web API and retrieve data. While it achieved the intended result, this updated approach offers a cleaner and more modern alternative. Here's why:

The code is more readable and cleaner

1. async/await makes asynchronous code look and behave like synchronous code.
2. You avoid deeply nested callbacks, which are common in jQuery Ajax and lead to "callback hell".

Simplified Error Handling

1. With jQuery Ajax, error handling is done via separate error callbacks.
2. With async/await, you can use try...catch blocks, making it easier to manage and debug errors.

No External Dependency

1. async/await uses native JavaScript features—no need to load jQuery or rely on its syntax.
2. This reduces page weight and improves performance.

← Previous