Bài viết này trình bày cách tiếp cận để xây dựng chức năng phân trang dữ liệu trong ứng dụng C#. Chúng ta sẽ phân tích nguyên lý hoạt động, thiết kế một lớp hỗ trợ phân trang và minh họa cách tích hợp với giao diện người dùng.
1. Phân tích Nguyên lý Truy vấn Dữ liệu Phân trang
Nguyên lý cốt lõi của phân trang dữ liệu là thực hiện các truy vấn SQL có giới hạn và loại bỏ các bản ghi đã hiển thị ở các trang trước. Dưới đây là các bước cơ bản với ví dụ truy vấn trên cơ sở dữ liệu mẫu `DataPagerDB`:
Ví dụ Truy vấn SQL:
-- Lấy dữ liệu cho trang đầu tiên (5 bản ghi)
SELECT TOP 5 StudentId, StudentName, Gender, Birthday, PhoneNumber
FROM Students
WHERE Birthday > '1989/10/12';
-- Lấy dữ liệu cho trang thứ hai
-- Loại bỏ 5 bản ghi đầu tiên dựa trên StudentId đã sắp xếp
SELECT TOP 5 StudentId, StudentName, Gender, Birthday, PhoneNumber
FROM Students
WHERE Birthday > '1989/10/12'
AND StudentId NOT IN (
SELECT TOP 5 StudentId
FROM Students
WHERE Birthday > '1989/10/12'
ORDER BY StudentId ASC
)
ORDER BY StudentId ASC;
-- Lấy dữ liệu cho trang thứ ba
-- Loại bỏ 10 bản ghi (5 bản ghi/trang * (3 - 1) trang)
SELECT TOP 5 StudentId, StudentName, Gender, Birthday, PhoneNumber
FROM Students
WHERE Birthday > '1989/10/12'
AND StudentId NOT IN (
SELECT TOP 10 StudentId
FROM Students
WHERE Birthday > '1989/10/12'
ORDER BY StudentId ASC
)
ORDER BY StudentId ASC;
-- Tính tổng số bản ghi thỏa mãn điều kiện
SELECT COUNT(*)
FROM Students
WHERE Birthday > '1989/10/12';
-- Tính tổng số trang (ví dụ: 5 bản ghi, PageSize = 3, 5, 8)
-- Phép chia nguyên và phép chia có dư cho thấy cách tính tổng số trang.
-- Tổng số trang = Tổng số bản ghi / Số bản ghi mỗi trang (+1 nếu có dư)
PRINT 5 / 3; -- Kết quả: 1 (Nếu hiểu là phép chia số học, không tính phần dư)
PRINT 5 / 5; -- Kết quả: 1
PRINT 5 / 8; -- Kết quả: 0
-- Cách tính toán thực tế hơn
PRINT 5 / 3 + 1; -- Kết quả: 2
PRINT 5 / 5; -- Kết quả: 1
PRINT 5 / 8 + 1; -- Kết quả: 1
-- Sử dụng phép toán modulo (%) để xác định có cần cộng thêm 1 hay không.
PRINT 5 % 3; -- Kết quả: 2 (Có dư, cần cộng 1)
PRINT 5 % 5; -- Kết quả: 0 (Không dư)
PRINT 5 % 8; -- Kết quả: 5 (Có dư, cần cộng 1)
Nguyên tắc thiết kế lớp phân trang:
- Nguyên tắc chung: Tách biệt phần cố định và phần thay đổi. Phần cố định là logic xử lý chung, phần thay đổi được đóng gói dưới dạng tham số.
- Các thuộc tính thay đổi cần tham số hóa:
- Số lượng bản ghi mỗi trang.
- Các trường (thuộc tính) cần hiển thị.
- Tên bảng.
- Điều kiện truy vấn.
- Khóa chính hoặc khóa duy nhất của bảng (để sử dụng trong mệnh đề
NOT IN). - Số lượng bản ghi cần lọc (tính toán dựa trên số trang và kích thước trang).
- Số trang hiện tại.
- Điều kiện sắp xếp.
- Phần cố định: Cấu trúc câu lệnh truy vấn SQL cốt lõi.
- Các thuộc tính kết quả trả về:
- Tổng số bản ghi thỏa mãn điều kiện.
- Tổng số trang (được tính toán sau khi có tổng số bản ghi).
2. Thiết kế Lớp Hỗ trợ Phân trang (SqlDataPager)
Lớp SqlDataPager được thiết kế để đóng gói logic phân trang. Trong một dự án thực tế, lớp này nên nằm trong lớp truy cập dữ liệu (DAL).
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Text;
namespace CSDataPager
{
/// <summary>
/// Lớp tiện ích cho việc phân trang dữ liệu SQL.
/// </summary>
public class SqlDataPager
{
#region Thuộc tính chung
/// <summary>
/// Số lượng bản ghi hiển thị trên mỗi trang.
/// </summary>
public int PageSize { get; set; }
/// <summary>
/// Danh sách các trường cần lấy, phân tách bằng dấu phẩy.
/// </summary>
public string FieldNames { get; set; } // Đổi tên để rõ ràng hơn
/// <summary>
/// Tên của bảng hoặc view cần truy vấn.
/// </summary>
public string TableName { get; set; }
/// <summary>
/// Điều kiện lọc cho truy vấn (mệnh đề WHERE, không bao gồm từ khóa WHERE).
/// </summary>
public string FilterCondition { get; set; } // Đổi tên để rõ ràng hơn
/// <summary>
/// Tên khóa chính hoặc cột duy nhất của bảng.
/// </summary>
public string PrimaryKey { get; set; }
/// <summary>
/// Số trang hiện tại đang được hiển thị.
/// </summary>
public int CurrentPage { get; set; }
/// <summary>
/// Điều kiện sắp xếp (mệnh đề ORDER BY, không bao gồm từ khóa ORDER BY).
/// </summary>
public string SortOrder { get; set; } // Đổi tên để rõ ràng hơn
#endregion
#region Thuộc tính chỉ đọc
private int _recordCount;
/// <summary>
/// Tổng số bản ghi thỏa mãn điều kiện lọc. Thuộc tính này được cập nhật sau khi truy vấn.
/// </summary>
public int RecordCount
{
get { return _recordCount; }
// Không cho phép gán trực tiếp từ bên ngoài để đảm bảo tính toàn vẹn.
}
/// <summary>
/// Tổng số trang dựa trên RecordCount và PageSize.
/// </summary>
public int TotalPages
{
get
{
if (_recordCount == 0)
{
// Nếu không có bản ghi nào, đặt lại CurrentPage về 1 và trả về 0 trang.
this.CurrentPage = 1;
return 0;
}
// Tính toán tổng số trang: làm tròn lên
return (_recordCount + PageSize - 1) / PageSize;
}
}
#endregion
/// <summary>
/// Xây dựng câu lệnh SQL hoàn chỉnh cho việc phân trang và đếm tổng số bản ghi.
/// </summary>
/// <returns>Câu lệnh SQL dạng [SELECT ... ; SELECT COUNT ...]</returns>
private string BuildPagedQuerySql()
{
// Tính toán số lượng bản ghi cần bỏ qua ở các trang trước.
int recordsToSkip = PageSize * (CurrentPage - 1);
// Xây dựng câu lệnh SQL chính để lấy dữ liệu phân trang.
// Sử dụng TOP để giới hạn số lượng bản ghi.
// Sử dụng NOT IN với một truy vấn con để loại bỏ các bản ghi đã xem.
string mainQuery = $@"
SELECT TOP ({PageSize}) {FieldNames}
FROM {TableName}
WHERE {FilterCondition} AND {PrimaryKey} NOT IN (
SELECT TOP ({recordsToSkip}) {PrimaryKey}
FROM {TableName}
WHERE {FilterCondition}
ORDER BY {SortOrder}
)
ORDER BY {SortOrder};";
// Xây dựng câu lệnh SQL để đếm tổng số bản ghi thỏa mãn điều kiện.
string countQuery = $@"
SELECT COUNT(*)
FROM {TableName}
WHERE {FilterCondition};";
// Kết hợp hai câu lệnh SQL.
return $"{mainQuery}\n{countQuery}";
}
/// <summary>
/// Thực thi truy vấn phân trang và trả về kết quả dưới dạng DataTable.
/// </summary>
/// <returns>DataTable chứa dữ liệu của trang hiện tại.</returns>
public DataTable GetPagedData()
{
// Lấy kết quả từ phương thức thực thi SQL (giả định có lớp SQLHelper).
DataSet dataSet = SQLHelper.ExecuteDataset(BuildPagedQuerySql());
// Cập nhật tổng số bản ghi từ bảng kết quả thứ hai.
// ds.Tables[1] chứa kết quả của câu lệnh COUNT(*).
if (dataSet.Tables.Count > 1 && dataSet.Tables[1].Rows.Count > 0)
{
_recordCount = Convert.ToInt32(dataSet.Tables[1].Rows[0][0]);
}
else
{
_recordCount = 0; // Xử lý trường hợp không có dữ liệu trả về.
}
// Trả về bảng dữ liệu phân trang (bảng kết quả đầu tiên).
return dataSet.Tables[0];
}
}
}
3. Triển khai Giao diện Người dùng (UI)
Phần giao diện người dùng bao gồm các điều khiển để người dùng tương tác, chẳng hạn như DataGridView để hiển thị dữ liệu, các nút điều hướng trang (Đầu, Trước, Tiếp, Cuối), ô nhập liệu để chuyển đến trang cụ thể, và các nhãn hiển thị thông tin về tổng số bản ghi, tổng số trang và trang hiện tại.
Dưới đây là một ví dụ về cách xử lý sự kiện cho các nút điều khiển:
using System;
using System.Windows.Forms;
namespace YourApplicationNamespace // Thay thế bằng namespace của bạn
{
public partial class FrmDataPager : Form
{
private SqlDataPager dataPager = null;
public FrmDataPager()
{
InitializeComponent();
// Khởi tạo các giá trị mặc định cho các điều khiển.
this.dtpBirthday.Value = new DateTime(1988, 1, 1); // Sử dụng Value thay vì Text
// Khởi tạo đối tượng phân trang.
dataPager = new SqlDataPager()
{
PrimaryKey = "StudentId",
TableName = "Students",
FieldNames = "StudentId,StudentName,Gender,Birthday,PhoneNumber", // Đổi tên thuộc tính
CurrentPage = 1,
SortOrder = "StudentId ASC" // Đổi tên thuộc tính
};
// Thiết lập giá trị mặc định cho ComboBox hiển thị số lượng bản ghi.
this.cboRecordList.SelectedIndex = 1; // Giả sử index 1 là giá trị mong muốn.
this.dgvStudentList.AutoGenerateColumns = false; // Tắt tự động tạo cột.
// Vô hiệu hóa các nút điều hướng ban đầu.
EnableNavigationButtons(false);
this.btnToPage.Enabled = false; // Nút chuyển trang cũng bị vô hiệu hóa ban đầu.
}
// Phương thức chung để thực hiện truy vấn và cập nhật UI.
private void RefreshDataView()
{
// Kích hoạt các nút điều hướng khi có dữ liệu.
EnableNavigationButtons(true);
// 1. Thiết lập điều kiện lọc và kích thước trang cho đối tượng phân trang.
dataPager.FilterCondition = $"Birthday > '{this.dtpBirthday.Value.ToString("yyyy-MM-dd")}'"; // Định dạng ngày tháng chuẩn
dataPager.PageSize = Convert.ToInt32(this.cboRecordList.SelectedItem.ToString()); // Lấy giá trị từ SelectedItem
// 2. Thực hiện truy vấn và cập nhật DataGridView.
this.dgvStudentList.DataSource = dataPager.GetPagedData();
// 3. Cập nhật các nhãn hiển thị thông tin phân trang.
this.lblRecordsCount.Text = dataPager.RecordCount.ToString(); // Tổng số bản ghi
this.lblPageCount.Text = dataPager.TotalPages.ToString(); // Tổng số trang
this.lblCurrentPage.Text = dataPager.CurrentPage.ToString(); // Trang hiện tại
// Điều chỉnh trạng thái các nút dựa trên thông tin phân trang.
bool hasMultiplePages = dataPager.TotalPages > 1;
bool isFirstPage = dataPager.CurrentPage == 1;
bool isLastPage = dataPager.CurrentPage == dataPager.TotalPages;
// Kích hoạt nút chuyển trang nếu có nhiều hơn 1 trang.
this.btnToPage.Enabled = hasMultiplePages;
// Điều chỉnh trạng thái nút đầu/trước.
this.btnFirst.Enabled = hasMultiplePages && !isFirstPage;
this.btnPre.Enabled = hasMultiplePages && !isFirstPage;
// Điều chỉnh trạng thái nút tiếp/cuối.
this.btnNext.Enabled = hasMultiplePages && !isLastPage;
this.btnLast.Enabled = hasMultiplePages && !isLastPage;
// Nếu tổng số trang là 0 hoặc 1, vô hiệu hóa tất cả nút điều hướng.
if (dataPager.TotalPages <= 1)
{
EnableNavigationButtons(false);
this.btnToPage.Enabled = false;
}
}
// Phương thức hỗ trợ bật/tắt các nút điều hướng trang.
private void EnableNavigationButtons(bool enable)
{
this.btnFirst.Enabled = enable;
this.btnNext.Enabled = enable;
this.btnPre.Enabled = enable;
this.btnLast.Enabled = enable;
}
// Sự kiện xử lý nút "Tìm kiếm".
private void btnQuery_Click(object sender, EventArgs e)
{
dataPager.CurrentPage = 1; // Luôn bắt đầu từ trang đầu tiên khi tìm kiếm mới.
RefreshDataView();
// Sau khi tìm kiếm, nút "Đầu" và "Trước" nên bị vô hiệu hóa.
this.btnFirst.Enabled = false;
this.btnPre.Enabled = false;
}
// Sự kiện xử lý nút "Trang đầu".
private void btnFirst_Click(object sender, EventArgs e)
{
dataPager.CurrentPage = 1;
RefreshDataView();
// Vô hiệu hóa nút "Đầu" và "Trước" khi ở trang đầu.
this.btnFirst.Enabled = false;
this.btnPre.Enabled = false;
}
// Sự kiện xử lý nút "Trang kế tiếp".
private void btnNext_Click(object sender, EventArgs e)
{
dataPager.CurrentPage++;
RefreshDataView();
// Nếu đã ở trang cuối, vô hiệu hóa nút "Tiếp" và "Cuối".
if (dataPager.CurrentPage == dataPager.TotalPages)
{
this.btnNext.Enabled = false;
this.btnLast.Enabled = false;
}
}
// Sự kiện xử lý nút "Trang trước".
private void btnPre_Click(object sender, EventArgs e)
{
dataPager.CurrentPage--;
RefreshDataView();
// Nếu đã về trang đầu, vô hiệu hóa nút "Đầu" và "Trước".
if (dataPager.CurrentPage == 1)
{
this.btnPre.Enabled = false;
this.btnFirst.Enabled = false;
}
}
// Sự kiện xử lý nút "Trang cuối".
private void btnLast_Click(object sender, EventArgs e)
{
dataPager.CurrentPage = dataPager.TotalPages;
RefreshDataView();
// Vô hiệu hóa nút "Tiếp" và "Cuối" khi ở trang cuối.
this.btnNext.Enabled = false;
this.btnLast.Enabled = false;
}
// Sự kiện xử lý nút "Chuyển đến trang".
private void btnToPage_Click(object sender, EventArgs e)
{
if (string.IsNullOrWhiteSpace(this.txtToPage.Text))
{
MessageBox.Show("Vui lòng nhập số trang cần chuyển đến!", "Thông báo", MessageBoxButtons.OK, MessageBoxIcon.Information);
this.txtToPage.Focus();
return;
}
if (int.TryParse(this.txtToPage.Text.Trim(), out int targetPage))
{
if (targetPage < 1 || targetPage > dataPager.TotalPages)
{
MessageBox.Show($"Số trang phải nằm trong khoảng từ 1 đến {dataPager.TotalPages}.", "Thông báo", MessageBoxButtons.OK, MessageBoxIcon.Warning);
this.txtToPage.Focus();
this.txtToPage.SelectAll(); // Chọn toàn bộ nội dung để người dùng dễ dàng sửa.
return;
}
dataPager.CurrentPage = targetPage;
RefreshDataView();
// Cập nhật trạng thái nút sau khi chuyển trang.
this.btnPre.Enabled = dataPager.CurrentPage > 1;
this.btnFirst.Enabled = dataPager.CurrentPage > 1;
this.btnNext.Enabled = dataPager.CurrentPage < dataPager.TotalPages;
this.btnLast.Enabled = dataPager.CurrentPage < dataPager.TotalPages;
}
else
{
MessageBox.Show("Định dạng số trang không hợp lệ. Vui lòng nhập số nguyên.", "Thông báo", MessageBoxButtons.OK, MessageBoxIcon.Warning);
this.txtToPage.Focus();
this.txtToPage.SelectAll();
}
}
// Sự kiện xử lý nút "Đóng".
private void btnClose_Click(object sender, EventArgs e)
{
this.Close();
}
}
}