DataTables With Server Side API
2025-01-15
筆記 DataTables 如何透過 Server Side API 的方式來處理大量資料的情況,並且透過 ASP.NET MVC 5 來實作。
說明
首先準備 Model。需要 DataTablesRequest
和 DataTablesResponse
來處理 DataTables 的 Ajax 請求和回應。
其中 DataTablesRequest
還包含了傳入欄位、搜尋條件與排序條件。
在回應 DataTablesResponse
中,需要包含總筆數、過濾後筆數、當前頁面資料,並且必須要是小寫的名稱,為了保持 C# Class 大寫命名,透過 JsonProperty
來指定小寫名稱。
public class DataTablesRequest
{
public int Draw { get; set; }
public int Start { get; set; }
public int Length { get; set; }
public List<Column> Columns { get; set; }
public Search Search { get; set; }
public List<Order> Order { get; set; }
}
public class Column
{
public string Data { get; set; }
public string Name { get; set; }
public bool Searchable { get; set; }
public bool Orderable { get; set; }
public Search Search { get; set; }
}
public class Search
{
public string Value { get; set; }
public bool Regex { get; set; }
}
public class Order
{
public int Column { get; set; }
public string Dir { get; set; }
}
public class LogViewModel
{
public int Id { get; set; }
[JsonConverter(typeof(IsoDateTimeConverter))]
public DateTime Logged { get; set; }
public string Level { get; set; }
public string Message { get; set; }
public string UserName { get; set; }
}
public class DataTablesResponse<T>
{
[JsonProperty("draw")]
public int Draw { get; set; }
[JsonProperty("recordsTotal")]
public int RecordsTotal { get; set; }
[JsonProperty("recordsFiltered")]
public int RecordsFiltered { get; set; }
[JsonProperty("data")]
public List<T> Data { get; set; }
}
HomeController 的 API 處理,在進行資料回應時,不使用 JsonResult
,而是使用 ContentResult
搭配 Newtonsoft.Json
來進行序列化,以使用 JsonProperty
所指定小寫名稱。
此外定義了三個輔助的方法,BuildQuery
用來建構查詢,ApplySorting
用來進行排序,GetPagedData
用來取得分頁資料。
其中 ApplySorting
使用泛型的方式,減少重複使用時的程式碼異動。但 BuildQuery
和 GetPagedData
則是針對特定的 Model 進行處理,如果要使用必須要調整對應的欄位。
[HttpPost]
public ActionResult GetLogs(DataTablesRequest request)
{
try
{
var totalCount = db.Logs.Count();
var query = BuildQuery(request); // 建構查詢
var filteredCount = query.Count();
query = ApplySorting(query, request); // 進行排序
var data = GetPagedData(query, request); // 取得分頁資料
// Prepare response data
var response = new DataTablesResponse<LogViewModel>
{
Draw = request.Draw,
RecordsTotal = totalCount,
RecordsFiltered = filteredCount,
Data = data
};
return new ContentResult()
{
Content = JsonConvert.SerializeObject(response, Formatting.None),
ContentEncoding = System.Text.Encoding.UTF8,
ContentType = "application/json"
};
}
catch (Exception ex)
{
Debug.WriteLine($"Error in GetLogs: {ex.Message}");
return Json(new { error = ex.Message });
}
}
/// <summary>
/// Apply sorting to the query
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="query"></param>
/// <param name="request"></param>
/// <returns></returns>
private IQueryable<T> ApplySorting<T>(IQueryable<T> query, DataTablesRequest request) where T : class
{
if (request.Order != null && request.Order.Any())
{
var orderColumn = request.Columns[request.Order[0].Column];
var orderDir = request.Order[0].Dir.ToLower();
// Validate column data to prevent injection attacks
if (!string.IsNullOrWhiteSpace(orderColumn.Data))
{
var parameter = Expression.Parameter(typeof(T), "x");
var property = Expression.Property(parameter, orderColumn.Data);
var lambda = Expression.Lambda(property, parameter);
var methodName = orderDir == "asc" ? "OrderBy" : "OrderByDescending";
var method = typeof(Queryable).GetMethods()
.First(m => m.Name == methodName && m.GetParameters().Length == 2)
.MakeGenericMethod(typeof(T), property.Type);
query = (IQueryable<T>)method.Invoke(null, new object[] { query, lambda });
}
}
else
{
var parameter = Expression.Parameter(typeof(T), "x");
var property = Expression.Property(parameter, "Id");
var lambda = Expression.Lambda(property, parameter);
var method = typeof(Queryable).GetMethods()
.First(m => m.Name == "OrderByDescending" && m.GetParameters().Length == 2)
.MakeGenericMethod(typeof(T), property.Type);
query = (IQueryable<T>)method.Invoke(null, new object[] { query, lambda });
}
return query;
}
private IQueryable<Logs> BuildQuery(DataTablesRequest request)
{
var query = db.Logs.AsQueryable();
if (!string.IsNullOrEmpty(request.Search?.Value))
{
var searchValue = request.Search.Value.ToLower();
query = query.Where(x =>
x.Message.ToLower().Contains(searchValue) ||
x.UserName.ToLower().Contains(searchValue) ||
x.Level.ToLower().Contains(searchValue) ||
x.UserName.ToLower().Contains(searchValue)
);
}
return query;
}
private List<LogViewModel> GetPagedData(IQueryable<Logs> query, DataTablesRequest request)
{
return query
.Skip(request.Start)
.Take(request.Length)
.Select(x => new LogViewModel
{
Id = x.Id,
Logged = x.Logged,
Level = x.Level,
Message = x.Message,
UserName = x.UserName
})
.ToList();
}
在 前端 DataTables 呼叫,需要啟用 serverSide
並且設定 processing
為 true
。
並透過加入 ajax
屬性,並且指定 url
、type
、contentType
、dataType
、data
來進行 Ajax 請求,注意這些關鍵屬性都不可以省略,否則會導致錯誤。
此外還必須加入 columns
來對應回傳資料的欄位。
document.addEventListener('DOMContentLoaded', function () {
var table = document.getElementById('tableId');
if (table) {
new DataTable(table, {
ajax: {
url: "Logs/GetLogs",
type: "POST",
contentType: "application/json",
dataType: "json",
data: function (d) {
return JSON.stringify(d);
}
},
processing: true,
serverSide: true,
searching: true,
pageLength: 10,
lengthChange: [10, 50, 100],
order: [[0, 'desc']], // Default Order
columnDefs: [
{ targets: 0, searchable: true, orderable: true },
{ targets: -1, searchable: false, orderable: false }
],
columns: [
{ data: 'Id' },
{ data: 'Logged' },
{ data: 'Level' },
{ data: 'Message' },
{ data: 'UserName' }
],
language: {
lengthMenu: "每頁顯示 _MENU_ 筆",
zeroRecords: "沒有任何查詢結果,請調整搜尋條件後再執行。",
info: "搜尋共有 _TOTAL_ 個項目",
infoEmpty: "",
infoFiltered: "(filtered from _MAX_ total records)",
search: "關鍵字搜尋",
}
});
}
});