Amal K
Amal K

Reputation: 4929

How to create a page navigation partial view?

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>&laquo;
                    </a>
                </li>
            }
            else
            {
                <li class="page-item disabled">
                    <a class="page-link" tabindex="-1">
                        <span class="sr-only">Previous</span>&laquo;
                    </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>&raquo;
                    </a>
                </li>
            }
            else
            {
                <li class="page-item disabled">
                    <a class="page-link" tabindex="-1">
                        <span class="sr-only">Next</span>&raquo;
                    </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:

  1. The 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.

  1. The second issue is that each view with a paginated list has a different previous and next page link. (the area/controller/action name for the link and different route values):
@* 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>&laquo;
</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>&raquo;
</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

Answers (1)

Kiran Joshi
Kiran Joshi

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

Related Questions