Reputation: 4929
I have a PaginatedList<T>
class inheriting from List<T>
in an ASP.NET Core app that my controllers and views use to render paginated content:
public class PaginatedList<T> : List<T>
{
public int PageNumber { get; private set; } = 1; // page index
public int PageSize { get; private set; } = 25; // item count in each page
public int TotalCount { get; private set; } // total items in all pages
public int PageCount { get; private set; } // total page count
public bool HasPreviousPage => PageNumber > 1;
public bool HasNextPage => PageNumber < PageCount;
public int From => PageSize * (PageNumber - 1) + 1; // index of first item in page
public int To => From + Count - 1; // index of last item in page
//Method bodies omitted for brevity
public static async Task<PaginatedList<T>> CreateAsync(
IQueryable<T> source, int pageSize = 25, int pageNumber = 1) {..}
private PaginatedList(
List<T> items, int count, int pageSize = 25, int pageNumber = 1) {..}
}
Code based on this with additional validation.
I have noticed that almost all of my views serving paginated lists have the same page navigation UI. (For all entities in the app like Person
, Book
, etc.):
@model PaginatedList<Person>
@*View-specific code here...*@
<nav aria-label="Page Navigation">
@if (Model.Count > 1)
{
@* Display the range of entities displayed *@
<div class="text-center text-muted my-2">
<em>Showing @Model.From to @Model.To out of @Model.TotalCount</em>
</div>
}
<ul class="pagination justify-content-center">
@if (Model.HasPreviousPage || Model.HasNextPage)
{
@if (Model.HasPreviousPage)
{
<li class="page-item">
<a asp-controller="Person"
asp-action="Index"
asp-route-repoId="@Model.RepoId"
asp-route-page="@(Model.PageNumber - 1)"
title="Previous" class="page-link" aria-label="Previous">
<span class="sr-only">Previous</span>«
</a>
</li>
}
else
{
<li class="page-item disabled">
<a class="page-link" tabindex="-1">
<span class="sr-only">Previous</span>«
</a>
</li>
}
<li class="page-item active" aria-current="page">
<a class="page-link" href="#">@Model.PageNumber</a>
</li>
@if (Model.HasNextPage)
{
<li class="page-item">
<a
asp-controller="Person"
asp-action="Index"
asp-route-repoId="@Model.RepoId"
asp-route-page="@(Model.PageNumber + 1)" title="Next" class="page-link" aria-label="Next">
<span class="sr-only">Next</span>»
</a>
</li>
}
else
{
<li class="page-item disabled">
<a class="page-link" tabindex="-1">
<span class="sr-only">Next</span>»
</a>
</li>
}
}
</ul>
</nav>
I would like to extract this to a partial view called _PageNav.cshtml
. But there are two things I'm not quite sure about:
PaginatedList<T>
class is generic and I cannot have a partial view with a model called PaginatedList<T>
with an open generic parameter T
. One possible solution I can think of is writing a non-generic interface called IPaginatedList
that has all the properties required for page navigation and then have PaginatedList<T>
implement it. This is because the navigation UI does not need to know anything about the items in the list. Then I can use IPaginatedList
as the model for my partial view:public interface IPaginatedList
{
public int PageNumber { get; }
public int PageSize { get; }
public int TotalCount { get; }
public int PageCount { get; }
public bool HasPreviousPage { get; }
public bool HasNextPage { get; }
public int From { get; }
public int To { get; }
}
public class PaginatedList<T> : List<T>, IPaginatedList
{
...
}
_PageNav.cshtml
:
@model IPaginatedList
@* Navigation UI *@
...
This would kind of solve the first issue.
@* Previous page link *@
<a asp-controller="Persons"
asp-action="Index"
asp-route-repoId="@Model.RepoId"
asp-route-page="@(Model.PageNumber - 1)"
title="Previous"
class="page-link"
aria-label="Previous">
<span class="sr-only">Previous</span>«
</a>
@* Next page link *@
<a asp-controller="Persons"
asp-action="Index"
asp-route-repoId="@Model.RepoId"
asp-route-page="@(Model.PageNumber + 1)"
title="Next"
class="page-link"
aria-label="Next">
<span class="sr-only">Next</span>»
</a>
The above would be the previous and next links for the Index
action of PersonsController
(Person
entity). This would change for another entity like Book
.
Should I have a property for each link tag helper argument in my PaginatedList<T>
like ControllerName
, ActionName
, etc.? Is there a better way?
Can this be solved with a custom tag-helper? What am I missing?
Upvotes: 1
Views: 684
Reputation: 1876
I have solution for using tag helpers
public class PaginationTagHelper : TagHelper
{
[HtmlAttributeName("onclick-method-name")]
public string OnClick { get; set; }
[HtmlAttributeName("current-page-index")]
public int CurrentPage { get; set; }
[HtmlAttributeName("end-page")]
public int EndPage { get; set; }
[HtmlAttributeName("start-page")]
public int StartPage { get; set; }
[HtmlAttributeName("total-page-count")]
public int TotalPages { get; set; }
/// <summary>
/// Process
/// </summary>
/// <param name="context"></param>
/// <param name="output"></param>
public override void Process(TagHelperContext context, TagHelperOutput output)
{
var onclickMethodFormat = OnClick + "({0})"; // 0 - pagenumber .
var onclickMethod = string.Empty;
StringBuilder content = new StringBuilder();
content.Append("<nav aria-label=\"Page navigation example\" class=\"pagin-holder clearfix\"> <ul class=\"pagination pagination-sm\">");
if (EndPage > 1)
{
if (CurrentPage > 1)
{
onclickMethod = string.Format(onclickMethodFormat, CurrentPage - 1);
content.Append("<li class=\"page-item\"> <a href=\"#\" class=\"page-link\" onclick=\"" + onclickMethod + "\">Previous</a></li>");
}
for (var page = StartPage; page <= EndPage; page++)
{
onclickMethod = string.Format(onclickMethodFormat, page);
if (page == CurrentPage)
{
content.Append("<li class=\"page-item active\"><a href=\"#\" class=\"page-link\"onclick=\"" + onclickMethod + "\">").Append(page).Append(" </a>");
}
else
{
content.Append("<li class=\"page-item\"><a href=\"#\" class=\"page-link\"onclick=\"" + onclickMethod + "\">").Append(page).Append(" </a>");
}
}
if (CurrentPage < TotalPages)
{
onclickMethod = string.Format(onclickMethodFormat, CurrentPage + 1);
content.Append("<li class=\"page-item\"> <a href=\"#\" class=\"page-link\" onclick=\"" + onclickMethod + "\">Next</a> </li>");
}
}
output.Content.AppendHtml(content.ToString());
}
}
This is my custom pagination class
public class PagingInfo
{
public int PageIndex { get; set; }
public int PageSize { get; set; }
public string SortOrder { get; set; }
public byte SortColumn { get; set; }
public string SortColumnName { get; set; }
public int TotalRecords { get; set; }
public string SearchText { get; set; }
public byte FilterByColumn { get; set; }
public string FilterByColumnName { get; set; }
}
public class Pager
{
public Pager(long totalItems, int? page, int pageSize = 10)
{
// calculate total, start and end pages
var totalPages = (int)Math.Ceiling((decimal)totalItems / (decimal)pageSize);
var currentPage = page != null ? (int)page : 1;
var startPage = currentPage - 5;
var endPage = currentPage + 4;
if (startPage <= 0)
{
endPage -= (startPage - 1);
startPage = 1;
}
if (endPage > totalPages)
{
endPage = totalPages;
if (endPage > 10)
{
startPage = endPage - 9;
}
}
TotalItems = totalItems;
CurrentPage = currentPage;
PageSize = pageSize;
TotalPages = totalPages;
StartPage = startPage;
EndPage = endPage;
}
public long TotalItems { get; set; }
public int CurrentPage { get; set; }
public int PageSize { get; set; }
public int TotalPages { get; set; }
public int StartPage { get; set; }
public int EndPage { get; set; }
}
Added tag helper in view:
<Pagination onclick-method-name="Index" current-page-index="Model.PagingInfo.CurrentPage" end-page="Model.PagingInfo.EndPage"
start-page="Model.PagingInfo.StartPage" total-page-count="Model.PagingInfo.TotalPages"></Pagination>
Upvotes: 1