یکی از مواردی که در هر پروژهای به چشم میخورد و وجود دارد، نمایش
دادههای ذخیره شدهی در بانک اطلاعاتی، به کاربر میباشد. احتمالا وب سایتهایی را دیدهاید که تمامی اطلاعات را در یک صفحه بدون هیچ صفحه بندی به
کاربر نشان میدهند که حس خوبی را به کاربر استفاده کننده منتقل نمیکند و
نتیجه منفی هم بر روی سئو خواهد گذاشت ( عدم داشتن Urlهای منحصر بفرد به
ازای هر صفحه).
بعضا دیده میشود که برنامه نویس یک Paging را به صورت Ajax ی پیاده سازی
میکند که با تغییر صفحهها، اطلاعات را خوانده و به کاربر نمایش میدهد و
هیچ اتفاقی در آدرس بار صفحه نمیافتد و خیلی خرسند است از کاری که انجام
دادهاست. در چنین مواردی اگر کاربر استفاده کننده از برنامه بخواهد
لینک صفحه دهم گرید و یا لیست اطلاعات را برای کسی بفرستد، باید چکار کند؟
توضیح بالا صرفا به این دلیل بیان شد تا به ضرورت داشتن Urlهای منحصر بفرد
برای هر Page برسیم؛ هر چند بحث جاری درباره سئو نیست. ولی ترجیح دادم در
کنار موضوع مقاله، توجهی هم داشته باشیم به این موضوع که رعایت آن حس بهتری را
به کاربران برنامه میدهد.
کتابخانههای زیادی برای صفحه بندی اطلاعات وجود دارند و یا اینکه بعضی از
برنامه نویسان و یا شرکتها ترجیح میدهند خود چرخ را مطابق میل خود از
نو طراحی کنند. در ادامه قصد داریم به پیاده سازی کتابخانه PagedList که
خیلی محبوب و پر طرفدار میباشد در ASP.NET MVC بپردازیم.
1- ابتدا قبل از هر کاری، با استفاده از دستور زیر اقدام به
نصب کتابخانه آن مینماییم:
Install-Package PagedList.Mvc
2- بعد از نصب این کتابخانه، متد الحاقی ToPagedList در اختیار ما قرار
داده میشود که بر روی IQueryable , IEnumerable در دسترس میباشد.
3- در کنترلر فقط کافیست متد ToPagedList را فراخوانی کرده و مقدار بازگشتی را به View ارسال نمود.
4 - و در نهایت در داخل View (ها) فقط کافیست برای نمایش صفحه بندی، دستور Html.PagedListPager را فراخوانی کنیم.
چهار مورد فوق تقریبا تمام کارهایی است که باید انجام شوند تا قادر باشیم از این
کتابخانه برای صفحه بندی اطلاعات استفاده کنیم. در ادامه نحوه پیاده
سازی آن را به همراه چند مثال بیان میکنیم.
ابتدا یک مدل فرضی را همانند زیر تهیه میکنیم:
public class Post
{
public int Id { get; set; }
public string Title { get; set; }
public string Body { get; set; }
}
و کلاسی را همانند زیر برای دریافت یک لیست از پستها مینویسیم:
public static class PostService
{
public static IEnumerable<Post> posts = new List<Post>() {
new Post{Id=1,Title="Title 1",Body="Body 1"},
new Post{Id=2,Title="Title 2",Body="Body 2"},
new Post{Id=3,Title="Title 3",Body="Body 3"},
new Post{Id=4,Title="Title 4",Body="Body 4"},
new Post{Id=5,Title="Title 5",Body="Body 5"},
new Post{Id=6,Title="Title 6",Body="Body 6"},
new Post{Id=7,Title="Title 7",Body="Body 7"},
new Post{Id=8,Title="Title 8",Body="Body 8"},
new Post{Id=9,Title="Title 9",Body="Body 9"},
new Post{Id=10,Title="Title 10",Body="Body 10"},
new Post{Id=11,Title="Title 11",Body="Body 11"},
new Post{Id=12,Title="Title 12",Body="Body 12"},
};
public static IEnumerable<Post> GetAll()
{
return posts
.OrderBy(row => row.Id);
}
}
ابتدا یک کنترلر را ایجاد نمایید. در اکشن متدی که قصد داریم لیستی از
اطلاعات را به کاربر نمایش دهیم، باید یک متغییر از نوع int برای شماره
صفحه در نظر گرفته شود:
public ActionResult Index(int? page)
{
var pageNumber = page ?? 1;
var posts = PostService.GetAll();
var result = posts.ToPagedList(pageNumber, 10);
ViewBag.posts = result;
return View();
}
و در view مربوطه داریم:
@using PagedList.Mvc;
@using PagedList;
<link href="/Content/PagedList.css" rel="stylesheet" type="text/css" />
<h2>List of posts</h2>
<ul>
@foreach (var post in ViewBag.posts)
{
<li>@post.Title</li>
}
</ul>
@Html.PagedListPager((IPagedList)ViewBag.posts, page => Url.Action("Index", new { page }))
توسط متد postService.Getall، تمامی پستها از دیتابیس خوانده شده که جمعا 12 رکورد میباشند. فراخوانی ToPagedList به تعداد پارامتر دوم رکوردها را بر میگرداند و در متغییر result قرار میدهد و در پایان برای نمایش صفحه بندی، اقدام به فراخوانی متد الحاقی PagedListPager نمودهایم.
بله، درست حدس زدهاید! این روش دارای یک عیب میباشد و آن این است که ابتدا ما تمامی رکوردها را از دیتابیس فراخونی کرده و بعد از آن به تعداد 10 رکورد را از آن انتخاب نمودهایم. هر چند در مثال جاری تعداد رکوردها زیاد نمیباشد، ولی با مرور زمان و حجیم شدن دیتابیس، کوئری فوق امکان دارد به کندی اجرا شود. به همین دلیل نیاز به متدی داریم که با توجه به صفحه جاری، تعداد n رکورد را از دیتابیس خوانده و نمایش دهد. برای این منظور متدی همانند زیر را به کلاس postService اضافه مینماییم:
public static IEnumerable<Post> GetAll(int page, int recordsPerPage,out int totalCount)
{
totalCount = posts.Count();
return posts
.OrderBy(row => row.Id).Skip(page * recordsPerPage).Take(recordsPerPage); // in real projects change like this .skip(()=>resultforSkip).Take(()=>recordsPerPage )
}
از totalCount برای نگه داری جمع کل رکوردها استفاده میکنیم و قصد نداریم تمامی اطلاعات را از دیتابیس واکشی نماییم.
در صفحه بندی به صورت دستی، تا حدودی اکشن Index تغییر خواهد کرد. در این روش داریم:
public ActionResult Index(int? page)
{
var pageIndex = (page ?? 1) - 1;
var pageSize = 10;
int totalPostCount;
var posts = PostService.GetAll(pageIndex, pageSize, out totalPostCount);
var result = new StaticPagedList<Post>(posts, pageIndex + 1, pageSize, totalPostCount);
ViewBag.posts = result;
return View();
}
نکته: مقدار PagedIndex نمیتواند صفر باشد؛ چون شروع اعداد صفحه بندی از یک هست. به این خاطر، PageIndex با یک واحد، جمع شده است. در روش قبلی مقدار پیش فرض آن را 1 قرار دادیم. ولی در این روش ابتدا یک واحد از آن کم میکنیم؛ به این خاطر که در متد Skip شاهد اطلاعات دقیقی باشیم. محتوای view به همان روش قبلی میباشد و نیازی به تغییر آن نیست.
مقدار page به صورت کوئری استرینگ به انتهای url اضافه خواهد شد. جهت نظم بخشیدن به آن میبایست اقدام به افزودن route سفارشی نمایید که در حالت پیش فرض به صورت زیر میباشد:
http://localhost:53192/?page=2
routes.MapRoute(
name: "paging",
url: "{controller}/{action}/{page}",
defaults: new { controller = "Home", action = "Index", page = UrlParameter.Optional }
);
در روشهایی که شرح آنها گذشت، از viewbag برای انتقال دادهها استفاده کردیم. میتوان view مورد نظر را strongly-typed معرفی نمود و دادهها را از طریق return view به سمت view بفرستید. در این حالت در view مربوطه داریم :
@model PagedList.IPagedList <pagedListmvc.Models.Post>
و تغییر حلقه for به صورت زیر :
@foreach (var post in Model)
{
<li>@post.Title</li>
}
سفارشی کردن Url: فرض کنید همراه با کلیک بر روی شمارهی صفحات، بخواهیم یک سری دیتای دیگر را هم به اکشن پاس دهیم؛ برای مثال tag=mvc. برای این منظور داریم:
@Html.PagedListPager( myList, page => Url.Action("Index", new { page = page, tag= "mvc" }) )
استایل خروجی html حاصل از Html.PagedListPager بر اساس کتابخانه
Bootstrap میباشد. در صورتیکه در پروژه خود از این کتابخانه استفاده نمیکنید، میتوانید فقط فایل PagedList.css را از Nuget دریافت نموده و به پروژهی خود اضافه نمایید.
یکی از overloadهای Html.PagedListPager پارامتری را تحت عنوان PagedListRenderOptions دارد که از آن میتوانید برای پیکربندی صفحه بندی استفاده نمایید. برای نمونه نمایش فقط 5 صفحه:
@Html.PagedListPager((IPagedList)ViewBag.posts, page => Url.Action("Index", new { page = page }), PagedListRenderOptions.OnlyShowFivePagesAtATime)
همچنین قادر خواهید بود یکسری تنظیمات دستی را بر روی شماره صفحات تولید شده انجام دهید؛ برای نمونه تغییر عناوین Next , Prev با عناوین فارسی:
@Html.PagedListPager((IPagedList)ViewBag.posts, page => Url.Action("Index", new { page = page }), new PagedListRenderOptions { LinkToFirstPageFormat = "<< ابتدا", LinkToPreviousPageFormat = "< قبلی", LinkToNextPageFormat = "بعدی>", LinkToLastPageFormat = "آخرین >>" })