datatable js، کتابخانهای جهت ساخت جداول است و نسبت به رقیب اصلی خودش یعنی kendo telerik، از سادگی بیشتری برخوردار هست و امکانات خوبی هم دارد.
اگر برای جداول صفحات خود، از کتابخانهی جیکوئری
datatable استفاده میکنید، بعد از مدتی که تعداد رکوردها زیاد میشوند، شاهد کند شدن صفحه خود خواهید شد. برای رفع این مشکل نیاز به پیاده سازی pagination دارید که به صورت خیلی سادهای قابل پیاده سازی هست و شما تغییر کمی را در سمت سرور اعمال میکنید و سایر موارد توسط خود کتابخانه انجام میشود.
در ابتدا به بررسی کدها و تغییرات سمت فرانتاند و صفحهی cshtml میپردازیم:
1- تابع Ajax ای که وظیفهی دریافت اطلاعات را دارد، به کل پاک کنید. چون Ajax به صورت یک آبجکت، به درون خود دیتاتیبل منتقل خواهد شد.
2- در صفحه خود، کد زیر را قرار دهید (جهت جلوگیری از 400 bad request) که این کار فقط برای هندلرهای razor page و یا controller نیاز است و اگر از API استفاده میکنید، مسلما نیازی به این مدل تنظیمات نیست.
3- سپس کد زیر را به startup خود اضافه کنید (در قسمتی که دارید اینترفیسها را ثبت میکنید):
//Post in Ajax
services.AddAntiforgery(o => o.HeaderName = "XSRF-TOKEN");
4- حالا نوبت کانفیگهای دیتاتیبل هست:
function initDataTables() {
table.destroy();
table = $("#tblJs").DataTable({
processing: true,
serverSide: true,
filter: true,
ajax: {
url: '@Url.Page("yourPage","yourHandler")',
beforeSend: function (xhr) {
xhr.setRequestHeader("XSRF-TOKEN",
$('input:hidden[name="__RequestVerificationToken"]').val());
},
type: "POST",
datatype: "json"
},
language: {
url: "/Persian.json"
},
responsive: true,
select: true,
columns: scheme,
select: true,
});
}
5-کد بالا، کل تابعی را نشان میدهد که وظیفهی ساخت دیتاتیبل را دارد؛ ولی شما تنها نیاز دارید قسمت زیر را اضافه کنید:
processing: true,
serverSide: true,
filter: true,
ajax: {
url: '@Url.Page("yourPage","yourHandler ")',
beforeSend: function (xhr) {
xhr.setRequestHeader("XSRF-TOKEN",
$('input:hidden[name="__RequestVerificationToken"]').val());
},
type: "POST",
datatype: "json"
},
حالا باید کدهای سمت سرور را بنویسیم. برای این منظور باید ابتدا مقادیری را که دیتاتیبل برای ما ارسال میکند، از ریکوئست دریافت کنیم.
6- کل دیتایی که دیتا تیبل برای ما میفرستد، به مدل زیر خلاصه میشود:
public class FiltersFromRequestDataTable
{
public string length { get; set; }
public string start { get; set; }
public string sortColumn { get; set; }
public string sortColumnDirection { get; set; }
public string sortColumnIndex { get; set; }
public string draw { get; set; }
public string searchValue { get; set; }
public int pageSize { get; set; }
public int skip { get; set; }
}
نکتهی مهم این است که پراپرتیها باید با اسم کوچک به سمت فرانتاند ارسال شوند.
* (من از razor page استفاده میکنم؛ ولی مسلما در controller هم به همین شکل و راحتتر خواهد بود)
7- سپس دادههای ارسال شدهی توسط دیتاتیبل، به سمت سرور را با استفاده از متد زیر دریافت میکنیم:
public static void GetDataFromRequest(this HttpRequest Request, out FiltersFromRequestDataTable filtersFromRequest)
{
//TODO: Make Strings Safe String
filtersFromRequest = new();
filtersFromRequest.draw = Request.Form["draw"].FirstOrDefault();
filtersFromRequest.start = Request.Form["start"].FirstOrDefault();
filtersFromRequest.length = Request.Form["length"].FirstOrDefault();
filtersFromRequest.sortColumn = Request.Form["columns[" + Request.Form["order[0][column]"].FirstOrDefault() + "][name]"].FirstOrDefault();
filtersFromRequest.sortColumnDirection = Request.Form["order[0][dir]"].FirstOrDefault();
filtersFromRequest.searchValue = Request.Form["search[value]"].FirstOrDefault();
filtersFromRequest.pageSize = filtersFromRequest.length != null ? Convert.ToInt32(filtersFromRequest.length) : 0;
filtersFromRequest.skip = filtersFromRequest.start != null ? Convert.ToInt32(filtersFromRequest.start) : 0;
filtersFromRequest.sortColumnIndex = Request.Form["order[0][column]"].FirstOrDefault();
filtersFromRequest.searchValue = filtersFromRequest.searchValue?.ToLower();
}
8- نحوهی استفاده از این متد در handler یا action مورد نظر:
Request.GetDataFromRequest(out FiltersFromRequestDataTable filtersFromRequest);
9- با استفاده از متد زیر، مقادیر مورد نیاز دیتاتیبل را به آن ارسال میکنیم:
public static PaginationDataTableResult<T> ToDataTableJs<T>(this IEnumerable<T> source, FiltersFromRequestDataTable filtersFromRequest)
{
int recordsTotal = source.Count();
CofingPaging(ref filtersFromRequest, recordsTotal);
var result = new PaginationDataTableResult<T>()
{
draw = filtersFromRequest.draw,
recordsFiltered = recordsTotal,
recordsTotal = recordsTotal,
data = source.OrderByIndex(filtersFromRequest).Skip(filtersFromRequest.skip).Take(filtersFromRequest.pageSize).ToList()
};
return result;
}
private static void CofingPaging(ref FiltersFromRequestDataTable filtersFromRequest, int recordsTotal)
{
if (filtersFromRequest.pageSize == -1)
{
filtersFromRequest.pageSize = recordsTotal;
filtersFromRequest.skip = 0;
}
}
private static IEnumerable<T> OrderByIndex<T>(this IEnumerable<T> source, FiltersFromRequestDataTable filtersFromRequest)
{
var props = typeof(T).GetProperties();
string propertyName = "";
for (int i = 0; i < props.Length; i++)
{
if (i.ToString() == filtersFromRequest.sortColumnIndex)
propertyName = props[i].Name;
}
System.Reflection.PropertyInfo propByName = typeof(T).GetProperty(propertyName);
if (propByName is not null)
{
if (filtersFromRequest.sortColumnDirection == "desc")
source = source.OrderByDescending(x => propByName.GetValue(x, null));
else
source = source.OrderBy(x => propByName.GetValue(x, null));
}
return source;
}
که باهر دیتاتایپی کار میکند و خودش به صورت خودکار، عملیات مرتبسازی را انجام میدهد (ابدایی خودم)
10- نحوه استفاده در هندلر یا اکشن:
var result = query.ToDataTableJs(filtersFromRequest);
return new JsonResult(result);
یک نکته: پراپرتیهای شما باید باحروف کوچک باشد وگرنه در سمت جاوااسکریپت، خطای undefined را مشاهده خواهید کرد. در این حالت باید پراپرتیها را با حروف کوچک شروع کنید؛ ولی اگر دارید با کتابخانهی newtonSoft و jsonCovert سریالایز میکنید، میتوانید از این attribute بالای پراپرتیها استفاده کنید:
[JsonProperty("name")]
درکل باید یک iqueryrable را آماده و به متد ToDataTableJs ارسال کنید.
- برای سرچ هم در columnها هم میتوانید به شکل زیر عمل کنید.
ابتدا دو متد زیر را به یک کلاس static اضافه کنید:
public static IEnumerable<TSource> WhereSearchValue<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)
{
return source.Where(predicate);
}
public static bool ContainsSearchValue(this string source, string toCheck)
{
return source != null && toCheck != null && source.IndexOf(toCheck, StringComparison.OrdinalIgnoreCase) >= 0;
}
بعد به این شکل از آنها بین ایجاد iqueryrable و جایی که متد todatatableJs فراخوانی میشود، استفاده کنید:
if (!string.IsNullOrEmpty(filtersFromRequest.searchValue))
query = query.WhereSearchValue(x => x.title.ContainsSearchValue(filtersFromRequest.searchValue) || x.id.ToString().ContainsSearchValue(filtersFromRequest.searchValue)).AsQueryable();
برای افزایش کارآیی بهتر است مدل اصلی را به ویوو ارسال نکنید و از همان اول یک IQueryrable از جنس ویوومدل یا dto داشته باشید و این سرچ را هم بر روی همان انجام دهید.
کد کامل هندلر یا action (که ترکیب کدهای بالا هستش):
public JsonResult OnPostList()
{
Request.GetDataFromRequest(out FiltersFromRequestDataTable filtersFromRequest);
var query = _Repo.GetQueryable().Select(x => new VmAdminList()
{
title = x.Title,
}
);
if (!string.IsNullOrEmpty(filtersFromRequest.searchValue))
query = query.WhereSearchValue(x => x.title.ContainsSearchValue(filtersFromRequest.searchValue) || x.id.ToString().ContainsSearchValue(filtersFromRequest.searchValue)).AsQueryable();
var result = query.ToDataTableJs(filtersFromRequest);
return new JsonResult(result);
}
چند نکته:
1- ممکناست که بخواهید یکسری فیلتر را بجز مقادیر پیش فرض به سمت سرور ارسال کنید. برای اینکار کد زیر را به قسمت Ajax فرانتاند اضافه کنید:
data: function (d) {
d.parentId = parentID;
d.StartDateTime= StartDateTime;
},
و آنها را به این شکل در سمت سرور دریافت کنید:
if (!int.TryParse(Request.Form["parentId"].FirstOrDefault(), out int parentId))
throw new NullReferenceException();
2- در کانفیگهای Ajax مربوط به دیتاتیبل، دیگر کلید Success را نداریم؛ ولی به این شکل میتوانید این قسمت را شبیه سازی کنید:
dataSrc: function (json) {
$("#count").val(json.data.length);
var sum = 0;
json.data.forEach(function (item) {
if (!isNullOrEmpty(item.credit))
sum += parseInt(item.credit);
})
$("#sum").val(separate(sum));
return json.data;
}
که return آن الزامی هست؛ وگرنه به خطا میخورید.
تا به اینجا کار کاملا تمام شده؛ ولی من برای داینامیک کردن schema و columnها هم کلاسی را نوشتهام که فکر میکنم کار را راحتتر کند. چون شما برای تعداد ستونها باید یک آبجکت را به شکل زیر تعریف کنید:
columns: [
{ data: 'name' },
{ data: 'position' },
{ data: 'salary' },
{ data: 'office' }
]
در اینجا اگر کلیدها و یا ستونها (<th>) جابجا باشند، خطا میدهد و توسعه را بعدا سخت میکند؛ چون بعد هر بار تغییر، باید دستی این آبجکتها و ستونها را هم جابجا کنید. ولی با استفاده از کدهای زیر، خودش به صورت داینامیک تولید میشود. کدزیر این کار رو انجام میدهد:
public class JsDataTblGeneretaor<T>
{
public readonly DataTableSchemaResult DataTableSchemaResult = new();
public JsDataTblGeneretaor<T> CreateTableSchema()
{
var props = typeof(T).GetProperties();
foreach (var prop in props)
{
DataTableSchemaResult.SchemaResult.Add(new()
{
data = prop.Name,
sortable = (prop.PropertyType == typeof(int)) || (prop.PropertyType == typeof(bool)) || (prop.PropertyType == typeof(DateTime)),
width = "",
visible = (prop.PropertyType != typeof(DateTime))
});
}
return this;
}
public JsDataTblGeneretaor<T> CreateTableColumns()
{
var props = typeof(T).GetProperties();
CustomAttributeData displayAttribute;
foreach (var prop in props)
{
string displayName = prop.Name;
displayAttribute = prop.CustomAttributes.FirstOrDefault(x => x.AttributeType.Name == "DisplayAttribute");
if (displayAttribute != null)
{
displayName = displayAttribute.NamedArguments.FirstOrDefault().TypedValue.Value.ToString();
}
DataTableSchemaResult.Colums.Add(displayName);
}
return this;
}
public JsDataTblGeneretaor<T> AddCustomSchema(string data, bool? sortable = null, bool? visible = null, string width = null, string className = null)
{
if (DataTableSchemaResult.SchemaResult == null || !DataTableSchemaResult.SchemaResult.Any())
return this;
foreach (var item in DataTableSchemaResult.SchemaResult.Where(x => x.data == data))
{
if (sortable != null)
item.sortable = sortable.Value;
if (visible != null)
item.visible = visible.Value;
if (width != null)
item.width = width;
if (className != null)
item.className = className;
}
return this;
}
public JsDataTblGeneretaor<T> SerializeSchema()
{
if (DataTableSchemaResult.SchemaResult == null || !DataTableSchemaResult.SchemaResult.Any())
return this;
DataTableSchemaResult.SerializedSchemaResult = JsonSerializer.Serialize(DataTableSchemaResult.SchemaResult);
return this;
}
}
public class DataTableSchema
{
public string data { get; set; }
public bool sortable { get; set; }
public string width { get; set; }
public bool visible { get; set; }
public string className { get; set; }
}
public class DataTableSchemaResult
{
public readonly List<DataTableSchema> SchemaResult = new();
public readonly List<string> Colums = new();
public string SerializedSchemaResult = "";
}
متد CreateTableSchema، آبجکت هایی را که دیتاتیبل نیاز دارد، ایجاد میکند (فرقی ندارد مدلت شما از چه جنسی باشد) که شامل یک لیست از آبجکتهاست و شما میتوانید بااستفاده از متد AddCustomSchema، آن را سفارشی سازی کنید؛ مثلا بگوئید فلان کلید نمایش داده نشود و یا عرضش را مشخص کنید و ...
متد CreateTableColumns خیلی ساده هست و فقط یک لیست از استرینگها را برمیگرداند.
SerializeSchema هم که لیست آبجکتهای مورد نیاز دیتاتیبل را سریالایز میکند.
نحوه استفاده:
در متد آغازین برنامه باید این کلاس را صدا بزنید و با هر روشی که دوست دارید، به view یا razor page ارسال کنید:
public void OnGet()
{
//Create Data Table Js Schema and Columns Dynamicly
JsDataTblGeneretaor<yourVM> tblGeneretaor = new();
DataTableSchemaResult = tblGeneretaor.CreateTableColumns().CreateTableSchema().SerializeSchema().DataTableSchemaResult;
}
نحوه سفارشی سازی:
.AddCustomSchema("yourProperty",visible:false)
که به سمت View ارسال میشودو حالا نحوهی استفاده کردن از scheme ساخته شده:
var scheme = JSON.parse('@Html.Raw(Model.DataTableSchemaResult.SerializedSchemaResult)')
و استفادهی از آن در گزینههای دیتاتیبل
columns: scheme, نحوه ساخت ستونها در view:
@foreach (var col in Model.DataTableSchemaResult.Colums)
{
<th>@col</th>
}