[ASP.NET Core] Using X.PagedList
Tags: aspnetcore, csharp
Published: 2021-03-20 13:29:26

## Intro
This time, I will try X.PagedList for pagenation for Razor.

* [GitHub - dncuug/X.PagedList: Library for easily paging through any IEnumerable/IQueryable in ASP.NET/ASP.NET Core](https://github.com/dncuug/X.PagedList)

### Environments
* .NET ver.6.0.100-preview.2.21155.3
* Microsoft.EntityFrameworkCore ver.6.0.0-preview.2.21154.2
* Npgsql.EntityFrameworkCore.PostgreSQL ver.6.0.0-preview2
* NLog.Web.AspNetCore ver.4.11.0
* Microsoft.EntityFrameworkCore.Design ver.6.0.0-preview.2.21154.2
* X.PagedList ver.8.0.7
* X.PagedList.Mvc.Core ver.8.0.7

#### package.json (To use bootstrap)
```json
{
    "browserslist": [
        "last 2 version"
    ],
    "dependencies": {
        "autoprefixer": "^10.2.5",
        "bootstrap": "^4.6.0",
        "postcss": "^8.2.8",
        "postcss-cli": "^8.3.1",
        "postcss-import": "^14.0.0"
    },
    "scripts": {
        "css": "npx postcss postcss/*.css -c postcss.config.js -d wwwroot/css -w"
    }
}
```

### Base project
Except the package versions, I used this project.

* [[EntityFramework Core] Try relationship 1](https://dev.to/masanori_msl/entityframework-core-try-relationship-1-2ppn)

## Use X.PagedList
According to the Example, I just need 1.getting "IQueryable" items, 2.converting to IPagedList, and 3.setting into "@Html.PagedListPager". 

* [PagedList/README.markdown at master · troygoode/PagedList · GitHub](https://github.com/troygoode/PagedList/blob/master/README.markdown#example)

#### BookService.cs
```cs
using System.Linq;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using BookStoreSample.Applications;
using BookStoreSample.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

namespace BookStoreSample.Books
{
    public class BookService: IBookService
    {
...
        public IQueryable<Book> GetBooks(int? authorId)
        {
            if(authorId != null && authorId > 0)
            {
                return context.Books.Include(b => b.Author)
                    .Include(b => b.Genre)
                    .Where(b => b.AuthorId == authorId);    
            }
            return context.Books.Include(b => b.Author)
                .Include(b => b.Genre);
        }
        public async Task<List<Author>> GetAuthorsAsync()
        {
            return await context.Authors.ToListAsync();
        }
    }
}
```
#### HomeController.cs
```cs
using System.Collections.Generic;
using System.Threading.Tasks;
using BookStoreSample.Applications;
using BookStoreSample.Books;
using BookStoreSample.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using X.PagedList;

namespace BookStoreSample.Controllers
{
    public class HomeController: Controller
    {
...
        [Route("Pages/Razor")]
        public IActionResult OpenRazorPage(int? Page)
        {
            // Get items as IQueryable<Book> and convert to IPagedList<Book>
            // Page min value is 1.
            ViewData["Books"] = books.GetBooks().ToPagedList(Page ?? 1, 5);
            ViewData["Title"] = "Hello Pagenation";
            return View("Views/RazorPagination.cshtml");
        }
    }
}
```
#### site.css
```css
@import "../node_modules/bootstrap/dist/css/bootstrap.min.css";
#book_search_result {
    height: 70vh;
    width: 100%;
}
.book_search_result_row {
    border: 1px solid black;
    display: flex;
    flex-direction: row;
    align-items: center;
    height: 20%;
    width: 100%;
}
```
#### RazorPagination.cshtml
```html
@using X.PagedList;
@using X.PagedList.Mvc.Core;
@using X.PagedList.Mvc.Core.Common;
@using BookStoreSample.Controllers;
@using BookStoreSample.Models;
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers;
@{
    IPagedList<Book>? bookPages = ViewData["Books"] as IPagedList<Book>;
}
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8" />
        <title>@ViewData["Title"]</title>
        <link rel="stylesheet" href="/css/site.css">
    </head>
<body>
    <div id="book_search_result">
        @if(bookPages != null && bookPages.Count > 0)
        {
            @foreach (var item in bookPages)
            {
                <div class="book_search_result_row">
                    <div>@item.Id</div>
                    <div>@item.Name</div>
                </div>
            }
        }
    </div>

    @Html.PagedListPager(bookPages, page => Url.Action("OpenRazorPage", "Home", 
        new { Page = page }),
        new PagedListRenderOptions {
            LiElementClasses = new string[] { "page-item" },
            PageClasses = new string[] { "page-link" }
    })
    </body>
</html>
```

* [Routing to controller actions in ASP.NET Core | Microsoft Docs](https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/routing?view=aspnetcore-5.0)
* [PagedListRenderOptions does not exist for X.PagedList.Mvc.Core 2.2 · Issue #140 · dncuug/X.PagedList · GitHub](https://github.com/dncuug/X.PagedList/issues/140)

### How many times are queries executed?
From output log, the answer is 4 times in total.

1. getting all "Author" rows
2. getting "Book" rows to show result
3. getting "Book" count for X.PagedList
4. getting "Book" rows for X.PagedList

2.and 4. are totally same.
When their execution times are so slow, I may have to choose another way for pagination.

## Use ViewModel
How about cases what using "ViewModel"?

#### RazorPaginationViewModel.cs
```cs
using X.PagedList;
using BookStoreSample.Models;

namespace BookStoreSample.ViewModels
{
    public record RazorPaginationViewModel
    {
        public int? Page { get; init; }
        public int? AuthorId { get; init; }
        public IPagedList<Book>? Books { get; init; } 
    }
}
```

First, I added "Model.AuthorId" into the anonimous class of @Html.PagedListPager.

#### HomeController.cs
```cs
...
namespace BookStoreSample.Controllers
{
    public class HomeController: Controller
    {
...
        [Route("Pages/Razor")]
        public IActionResult OpenRazorPage(RazorPaginationViewModel? viewModel)
        {
            ViewData["Books"] = books.GetBooks(viewModel?.AuthorId).ToPagedList(viewModel?.Page ?? 1, 5);
            ViewData["Title"] = "Hello Pagenation";
            return View("Views/RazorPagination.cshtml");
        }
    }
}
```
#### RazorPagination.cshtml (Failed)
```html
@using X.PagedList;
@using X.PagedList.Mvc.Core;
@using X.PagedList.Mvc.Core.Common;
@using BookStoreSample.Books;
@using BookStoreSample.Controllers;
@using BookStoreSample.Models;
@using BookStoreSample.ViewModels;
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers;
@inject IBookService books;
@model RazorPaginationViewModel;
@{
    IPagedList<Book>? bookPages = ViewData["Books"] as IPagedList<Book>;
    List<Author> authors = await books.GetAuthorsAsync();
}
...
    <div>
        @Html.DropDownListFor(
            model => Model!.AuthorId,
            authors.Select(a => new SelectListItem{ Value = a.Id.ToString(), Text = a.Name }),
            "Author",
            new { @class = "form-control" })
    </div>
...
    <!-- Don't do this -->
    @Html.PagedListPager(bookPages, page => Url.Action("OpenRazorPage", "Home", 
        new { Page = page,
            AuthorId = Model?.AuthorId    
        }),
        new PagedListRenderOptions {
            LiElementClasses = new string[] { "page-item" },
            PageClasses = new string[] { "page-link" }
    })
...
```

### Model is null?
But the link of page links were like "http://localhost:5000/Pages/Razor?Page=1".
Because "Model?.AuthorId" was always null.

I think the reason is creating page links before Model instance are set.
So I set the value through "ViewData".

#### HomeController.cs
```cs
...
namespace BookStoreSample.Controllers
{
    public class HomeController: Controller
    {
...
        [Route("Pages/Razor")]
        public IActionResult OpenRazorPage(RazorPaginationViewModel? viewModel)
        {
            ViewData["Books"] = books.GetBooks(viewModel?.AuthorId).ToPagedList(viewModel?.Page ?? 1, 5);
            ViewData["AuthorId"] = viewModel?.AuthorId;
            ViewData["Title"] = "Hello Pagenation";
            return View("Views/RazorPagination.cshtml");
        }
    }
}
```

#### RazorPagination.cshtml (Failed)
```html
...
    @Html.PagedListPager(bookPages, page => Url.Action("OpenRazorPage", "Home", 
        new { Page = page,
            AuthorId = ViewData["AuthorId"]      
        }),
        new PagedListRenderOptions {
            LiElementClasses = new string[] { "page-item" },
            PageClasses = new string[] { "page-link" }
    })
...
```

* [Razor syntax reference for ASP.NET Core | Microsoft Docs](https://docs.microsoft.com/en-us/aspnet/core/mvc/views/razor?view=aspnetcore-5.0)