Пагинация и чек боксы! Как их подружить? ASP.NET Core

121
05 ноября 2019, 05:30

Код, который получает отмеченные чек боксы и передаёт их методу в контроллер.

<input type="checkbox" id="assetIdCheckBox" value="@item.Id" data-name="@item.Name" onchange="OnChangeCheckBoxFunc(this)" />

checkedCheckBox = [];
function OnChangeCheckBoxFunc(e) {
    var assetId = e.value;
    if (checkedCheckBox.indexOf(assetId) > -1) {
        var elementIndex = checkedCheckBox.indexOf(assetId);
        checkedCheckBox.splice(elementIndex, 1);
    } else {
        checkedCheckBox.push(assetId);
    }
}

То есть, получает все отмеченные чек боксы и сохраняет в массив checkedCheckBox. Который потом передаю в метод через Ajax

   function getCheckedCheckBoxesMove() {
        $.ajax({
            type: 'POST',
            url: '@Url.Action("AssetsMove", "Assets")',
            data: {
                assetId: checkedCheckBox,
            },
            success: function (data) {
                console.log('success!');
            }
        });
    }

Всё работало!
Но потом, я добавил пагиинацию страниц, и вот тут всё ломается. То есть, пока ты выбираешь на одной странице всё норм, но как только переходишь на следующею, то все чек боксы слетают. А, на странице на которую перешёл, всё работает по прежнему как нужно. Как сохранять выбранные чек боксы пока ты переходишь по страницам?

Пагинация:

 public int PageNumber { get; private set; }
        public int TotalPages { get; private set; }
        public PageVM (int count, int pageNumber, int pageSize) {
            PageNumber = pageNumber;
            TotalPages = (int) Math.Ceiling (count / (double) pageSize);
        }
        public bool HasPreviousPage {
            get {
                return (PageNumber > 1);
            }
        }
        public bool HasNextPage {
            get {
                return (PageNumber < TotalPages);
            }
        }
public async Task<IActionResult> Index (int page = 1) {
int pageSizeA = 7;
var mainStorage = _context.Offices.FirstOrDefault (s => s.IsMain);
                    IQueryable<Asset> assetsA = _context.Assets
                        .Include (a => a.Category)
                        .Include (a => a.Supplier)
                        .Include (a => a.Office)
                        .Where (a => a.IsActive)
                        .Where (a => a.InStock)
                        .Where (a => a.OfficeId == mainStorage.Id);
                    var countA = await assetsA.CountAsync ();
                    var itemsA = await assetsA.Skip ((page - 1) * pageSizeA).Take (pageSizeA).ToListAsync ();
                    PageVM pageVMA = new PageVM (countA, page, pageSizeA);
                    IndexVM viewModelA = new IndexVM {
                        PageVM = pageVMA,
                        Assets = itemsA
                    };
}

               <div>
                    @if (Model.PageVM.HasPreviousPage)
                        {
                            <a asp-action="Index"
                                asp-route-page="@(Model.PageVM.PageNumber - 
                                   1)"
                                class="btn btn-w-m btn-info" style="margin- 
                                right: 10px;">
                                 <i class="fa fa-chevron-left"></i>
                                Назад
                            </a>
                        }
                        @if (Model.PageVM.HasNextPage)
                        {
                            <a asp-action="Index"
                                asp-route-page="@(Model.PageVM.PageNumber + 
                                 1)"
                                class="btn btn-w-m btn-info">
                                Вперед
                                <i class="fa fa-chevron-right"></i>
                            </a>
                        }
                </div>
Answer 1

В первую очередь рекомендую посмотреть ссылки, которую я достаточно часто упоминаю (тут, например) в ответах по asp.net - приложение на основе CQRS + Mediatr. Вот ссылка на видео, вот ссылка на само приложение

Не знаю, будете ли вы использовать Mediatr, но в любом случае мы будем говорить о двух классах:

  • Query - класс для входных параметров запроса
  • Result - класс для ответа на запрос

В терминах MediatR я написал бы это так:

public class Query : IRequest<Result>
{
}

Соответственно, контроллер выглядел бы как-то так:

public class PlaceController : BaseController
{
    public PlaceController(IMediator mediator)
        : base(mediator)
    {
    }
    public async Task<IActionResult> List(Places.List.Query query, CancellationToken cancellationToken)
    {
        if (!this.ModelState.IsValid)
            // какая-то обработка ошибок
        var result = await this.Mediator.Send(query, cancellationToken);
        return this.View((query, result));
    }

(Вы можете не использовать кортежи из c#, а скажем создать свой класс-обёртку, можно сразу generic'ом)

В случае когда вы захотите перейти на SPA-приложение на react/vue/angular - ваш api контроллер станет ещё проще: на вход подаётся Query, на выход отдаёте Result:

public class CategoryController : BaseApiController
{
    public CategoryController(IMediator mediator)
        : base(mediator)
    {
    }
    [HttpPost]
    public async Task<object> List(Categories.List.Query query, CancellationToken cancellationToken)
    {
        var result = await this.Mediator.Send(query, cancellationToken);
        return result;
    }
}

Аналогично упрощается если вы хотите перейти на Razor Pages:

public class ListModel : PageBaseModel
{
    public ListModel(IMediator mediator)
        : base(mediator)
    {
    }
    [BindProperty(SupportsGet = true)]
    public Accounts.List.Query Query { get; set; }        
    public Accounts.List.Result Result { get; private set; }
    public async Task<IActionResult> OnGetAsync(CancellationToken cancellationToken)
    {
        this.Result = await this.Mediator.Send(query, cancellationToken);
        return this.Page();
    }
}

В случае же с asp.net приложением вам нужно отдать на выход и Result и Query - чтобы по данным, содержащимся в Query ваш View отрисовал на странице параметры запроса, а именно:

  • фильтры
  • сортировку
  • пагинацию

Это как раз содержимое вашего Query, в частных случаях может не быть каких-то частей, например в таблице не предусмотрено сортировки.

Можете прямо разбить свой класс Query на три части, группировав поля при помощи region или сделав дополнительный отступ между группами полей. Скажем, так:

public class Query
{
    // Filters
    public bool HideArchieved { get; set; }
    public DateTime? FromDate { get; set; }
    public DateTime? ToDate { get; set; }
    // Pagination
    public int PageSize { get; set; }
    public int PageNumber { get; set; }
}

Я предпочитаю выносить пагинацию в отдельный класс:

public class PagedQuery
{
    public int PageSize { get; set; }
    public int PageNumber { get; set; }
    public int ToSkip => (this.PageNumber - 1) * this.PageSize;
    public Paging GetPaging(int total, string url)
    {
        return new Paging(this.PageSize, this.PageNumber, total, url);
    }
}

И у меня получается как-то так:

public class Query : PagedQuery, IRequest<Result>
{
    public bool HideArchieved { get; set; }
    public DateTime? FromDate { get; set; }
    public DateTime? ToDate { get; set; }
}

Небольшое отступление, пара слов о том, как выглядит обработка запроса. Для MediatR вы будете писать как-то так:

[UsedImplicitly]
public class Handler : BaseHandler, IRequestHandler<Query, Result>
{
    public Handler(ApplicationDbContext applicationDbContext)
        : base(applicationDbContext)
    {
    }
    public async Task<Result> Handle(Query request, CancellationToken cancellationToken)
    {
        IQueryable<Place> query = this.ApplicationDbContext.Places;
        if (request.HideArchieved)
        {
            query = query.Where(x => x.Archieved == false);
        }
        var places = await query.OrderByDescending(x => x.Id)
                                .Skip(request.ToSkip)
                                .Take(request.PageSize)
                                .AsNoTracking()
                                .ToArrayAsync(cancellationToken);
        return new Result
        {
            Places = places,
            PlacesCount = query.Count(),
        };
    }
}

Можете завернуть в репозиторий, главное понимать, что у вас внутри обработчика выделяется чётко все те же три фазы: итоговый query собирается из фильтров, сортировок по столбцам и параметров пагинации.

И вот теперь пора поговорить о том, как выглядит ваше представление.

@using Microsoft.AspNetCore.Http.Extensions
@model (Places.List.Query Query, Places.List.Result Result)
@{
    ViewBag.Title = "Места игр";
}
<h2>@ViewBag.Title</h2>
<p><a asp-action="Create" class="btn btn-outline-secondary">Создать новое место игр</a></p>

Оно также будет состоять из нескольких частей: сначала будет показываться набор фильтров, мы туда передадим параметры Query:

<partial name="ListFilter" model="Model.Query" />

После этого мы нарисуем собственно таблицу со столбцами и сортировками:

<partial name="ListResult" model="Model.Result.Places" />

И после этого отрисуем пагинацию, шаблон этого представления спокойно можно положить в shared view, оно одинаково для всех:

<partial name="Pagination" model="Model.Query.GetPaging(Model.Result.PlacesCount, Context.Request.GetDisplayUrl())" />

Посмотрим на первую часть - фильтры:

@model Places.List.Query
<div class="row">
    <div class="col-sm-12 col-md-12">
        <div class="alert alert-info">
            <form method="get" class="form-inline">
                <div class="form-group">
                    <label asp-for="HideArchieved"></label>
                    <input asp-for="HideArchieved" class="form-control" />
                </div>
                <input type="hidden" asp-for="PageSize" />
                <input type="hidden" asp-for="PageNumber" />
                <button type="submit" class="btn btn-secondary">filter</button>
            </form>
        </div>
    </div>
</div>

Мы аккуратненько отрисовали фильтры, а также положили в hidden параметры сортировки. Если кто-то жмякнет кнопку "фильтр", то у нас в GET-параметрах все данные для запроса сохранены (а те, что пользователь поменял - лежат тут же).

Отрисовка таблицы банальна, опускаю.

Отрисовка пагинации на первый взгляд проста:

@model Paging
<div class="row">
    <div class="col-sm-12 col-md-12">
        <nav aria-label="Page navigation example">
            <ul class="pagination">
                @if (Model.IsFirst())
                {
                    <li class="page-item disabled"><a class="page-link" href="@Model.FirstUrl"><<</a></li>
                    <li class="page-item disabled"><a class="page-link" href="@Model.PrevUrl"><</a></li>
                }
                else
                {
                    <li class="page-item"><a class="page-link" href="@Model.FirstUrl"><<</a></li>
                    <li class="page-item"><a class="page-link" href="@Model.PrevUrl"><</a></li>
                }
                @foreach (var i in Model.GetPaging())
                {
                    if (Model.IsCurrent(i))
                    {
                        <li class="page-item active">
                            <a class="page-link disabled" href="@Model.GetUrl(i)">@i</a>
                        </li>
                    }
                    else
                    {
                        <li class="page-item">
                            <a class="page-link" href="@Model.GetUrl(i)">@i</a>
                        </li>
                    }
                }
                @if (Model.IsLast())
                {
                    <li class="page-item disabled"><a class="page-link" href="@Model.NextUrl">></a></li>
                    <li class="page-item disabled"><a class="page-link" href="@Model.LastUrl">>></a></li>
                }
                else
                {
                    <li class="page-item"><a class="page-link" href="@Model.NextUrl">></a></li>
                    <li class="page-item"><a class="page-link" href="@Model.LastUrl">>></a></li>
                }
            </ul>
        </nav>
    </div>
</div>

Всё интересное реализуется классом Paging:

public class Paging
{
    public Paging(int pageSize, int pageNumber, int total, string url)
    {
        this.PageSize = pageSize;
        this.PageNumber = pageNumber;
        this.Total = total;
        this.Url = url;
    }
    private int PageSize { get; set; }
    private int PageNumber { get; set; }
    private int Total { get; set; }
    private string Url { get; set; }
    public int[] GetPaging()
    {
        const int maxBefore = 5;
        const int maxAfter = 5;
        var first = this.PageNumber - maxBefore;
        if (first < 1)
            first = 1;
        var last = this.PageNumber + maxAfter;
        if (last > this.MaxPageCount)
            last = this.MaxPageCount;
        return Enumerable.Range(first, last - first).ToArray();
    }
    private int MaxPageCount => (int)Math.Ceiling((double)this.Total / this.PageSize);
    public bool IsCurrent(int i) => this.PageNumber == i;
    public bool IsFirst() => this.IsCurrent(1);
    public bool IsLast() => this.IsCurrent(this.MaxPageCount);
    public string GetUrl(int i)
    {
        var uri = new Uri(this.Url);
        var baseUri = uri.GetComponents(UriComponents.Scheme | UriComponents.Host | UriComponents.Port | UriComponents.Path, UriFormat.UriEscaped);
        var query = QueryHelpers.ParseQuery(uri.Query);
        var items = query.SelectMany(x => x.Value, (col, value) => new KeyValuePair<string, string>(col.Key, value)).ToList();
        items.RemoveAll(x => x.Key == "PageNumber");
        var qb = new QueryBuilder(items);
        qb.Add("PageNumber", i.ToString());
        var fullUri = baseUri + qb.ToQueryString();
        return fullUri;
    }
    public string FirstUrl => this.GetUrl(1);
    public string LastUrl => this.GetUrl(this.MaxPageCount);
    public string PrevUrl => this.GetUrl(this.PageNumber - 1);
    public string NextUrl => this.GetUrl(this.PageNumber + 1);
}

Как это сделано? Принцип простой: берётся текущий url страницы, в нём удаляется get-параметр PageNum и добавляется заново, но уже с нужным нам значением. (Сделано это при помощи двух замечательных nuget-пакетов - Microsoft.AspNetCore.WebUtilities и Microsoft.AspNetCore.Http.Extensions, за подробностями отсылаю вот к этой статье)

То есть при переходе по кнопкам пагинации у нас в URL остаются лежать все наши параметры фильтрации, только меняется номер текущей страницы.

Итого, у нас получается очень простая модель работы с CRUD-страницами (точнее - списками, требующими фильтрации, сортировок и пагинации):

  • Мы оперируем двумя терминами входные данные и выходные данные, типовой request - response, при этом понимаем, что request состоит из трёх частей (фильтры, сортировки и пагинация)
  • Мы держим состояние в нашем url, при желании любой желающий может скопировать url и оправить в мессенджер - эту страницу увидят точно также (ну, если данные не поменяются в базе) как и увидите вы.
  • Мы делаем минимальное число изменений в наших входных данных как при изменении фильтров, так и сортировок, так и при пагинации.

Ну и будет у вас в итоге что-то подобное:

Дополнительно по теме:

  • Постраничный вывод
  • Создание пагинации
  • X.PagedList для asp.net core
Answer 2

В методе контроллера на сервере Вам нужно знать, какие чекбоксы в данный момент присутствуют на странице, и сохранять только их.

<input type="checkbox" class="assetIdCheckBox" value="@item.Id" 
  data-name="@item.Name" onchange="OnChangeCheckBoxFunc(this)" />
function getCheckedCheckBoxesMove() {
  var all = [];
  $(".assetIdCheckBox").each(function(){
    all.push(this.value);
  });
  $.ajax({
    type: 'POST',
    url: '@Url.Action("AssetsMove", "Assets")',
    data: {
      assetId: checkedCheckBox,
      all: all
    },
    success: function (data) {
      console.log('success!');
    }
  });
}
READ ALSO
Как удалить значение из input?

Как удалить значение из input?

Есть такая задача: Есть модальное окно, которое открывается при клике на InputВ этом модальном юзер выбирает определенные категории

318
Выпадающее меню на всю высоту

Выпадающее меню на всю высоту

Хочу сделать выпадающее меню на всю высоту экранаСделал через Height: 100vh

175
Как изменять видимость пароля в IE7

Как изменять видимость пароля в IE7

Я делаю скрытие/открытие пароля по нажатию кнопки

143
Body zoom для разных устройств

Body zoom для разных устройств

Как сделать так, чтобы на компьютере масштаб body был 90%, а на телефонах 150%?

151