Фильтрация постраничного списка в Blazor - PullRequest
0 голосов
/ 29 апреля 2020

У меня есть приложение Blazor, где у меня есть список объектов в списке. Я установил компонент пейджера (показан ниже), и он прекрасно работает. Затем я настроил функциональность окна поиска для набора полей из списка. Все это прекрасно работает, если пейджер находится на первой странице. Любые другие страницы и результаты поиска показывают непредсказуемые результаты. Иногда даже не правильно фильтрует элементы на этой странице. Любые советы будут полезны. Спасибо.

Pager.razor

@typeparam TItem

<div class="row d-flex col-9">
<div class="justify-content-center">
    @if (PageCount > 1 && List.Count > PageSize)
    {
        <ul class="pagination justify-content-center">
            <li><button @onclick="@(() => ChangePage(1))" class="btn">&laquo;</button></li>

                @for (var i = StartIndex; i <= FinishIndex; i++)
                {
                    var currentIndex = i;
                    @if (i == CurrentPage)
                    {
                        <li class="page-item active"><span class="btn">@i</span></li>
                    }
                    else
                    {
                        <li class="page-item"><button class="btn page-link" @onclick="@(() => ChangePage(currentIndex))">@i</button></li>
                    }
                }

            <li><button @onclick="@(() => ChangePage(PageCount))" class="btn">&raquo;</button></li>
        </ul>
    }   
</div>
<select class="custom-select offset-1 col-1 ml-auto" bind="@PageSize" @onchange="@(e => ChangePageSize(e))">
    <option value="10">10</option>
    <option value="25">25</option>
    <option value="50">50</option>
</select>
</div>

@code {
    [Parameter]
    public List<TItem> List { get; set; }

    public List<TItem> Display { get; set; }

    [Parameter]
    public Action<List<TItem>> DisplayChanged { get; set; }

    [Parameter]
    public Action<bool> Rendered { get; set; }

    private int PageSize { get; set; } = 10;
    private int CurrentPage { get; set; } = 1;
    private int StartIndex { get; set; }
    private int FinishIndex { get; set; }
    private int PageCount { get; set; }

    protected override void OnAfterRender(bool firstRender)
    {
        base.OnAfterRender(firstRender);
        Rendered?.Invoke(true);
    }

    private void ChangePageSize(ChangeEventArgs e)
    {
        PageSize = int.Parse(e.Value.ToString());
        ChangePage(1);
    }

    private void ChangeDisplay()
    {
        DisplayChanged?.Invoke(
            List
                .Skip((CurrentPage - 1) * PageSize)
                .Take(PageSize)
                .ToList()
        );
    }

    protected override void OnParametersSet()
    {
        ResetIndex();

        ChangeDisplay();
        base.OnParametersSet();
    }

    protected void ChangePage(int page)
    {
        CurrentPage = page;
        ResetIndex();
        ChangeDisplay();
    }

    private void ResetIndex()
    {
        PageCount = List.Count / PageSize;

        if (List.Count % PageSize > 0)
        {
            PageCount++;
        }

        StartIndex = Math.Max(CurrentPage - 5, 1);
        FinishIndex = Math.Min(CurrentPage + 5, PageCount);
    }
}

Использование пейджера <Pager List="@FilteredUsers" DisplayChanged="@DisplayChanged" Rendered="@PagerRendered" />

Функция поиска

<input class="form-control" type="text" @bind-value="@SearchTerm" @bind-value:event="oninput" />
<select class="form-control" @bind-value="@Property" @bind-value:event="onchange">
    <option value="FirstName">First Name</option>
    <option value="LastName">Last Name</option>
    <option value="Role">Role</option>
    <option value="Property">Property</option>
</select>

private string searchTerm;

private string SearchTerm
{
    get => searchTerm;
    set
    {
        searchTerm = value;
        Filter();
    }
}

Фильтр

private void Filter()
{
    switch (Property)
    {
        case "FirstName":
            FilteredUsers = Users.Where(u => u.FirstName.ToLower().Contains(SearchTerm.ToLower())).ToList();
            break;
        case "LastName":
            FilteredUsers = Users.Where(u => u.LastName.ToLower().Contains(SearchTerm.ToLower())).ToList();
            break;
        case "Role":
            FilteredUsers = Users.Where(u => u.Role.ToString().ToLower().Contains(SearchTerm.ToLower())).ToList();
            break;
        case "Property":
            if (string.IsNullOrEmpty(SearchTerm))
            {
                FilteredUsers = Users;
            }
            else
            {
                FilteredUsers = Users.Where(u => TicketingRosters.Any(t => t.Property.PropertyName.ToLower().Contains(SearchTerm.ToLower()) && u.UserId == t.SellerId)).ToList();
            }
            break;
    }
    StateHasChanged();
}

Редактировать

Вот другие функции и свойства, которыми вы можете быть ищу:

private List<UserDto> Users { get; set; }
private List<UserDto> FilteredUsers { get; set; }
private List<UserDto> Display { get; set; }

private bool IsPagerRendered { get; set; }
private void DisplayChanged(List<UserDto> display)
{
    Display = display;
}

private void PagerRendered(bool rendered)
{
    IsPagerRendered = rendered;
    StateHasChanged();
}

Редактировать 2 private List<TicketingRoster> TicketingRosters { get; set; } = new List<TicketingRoster>();

UserDto

#nullable enable
using System;
using System.ComponentModel.DataAnnotations;

namespace TicketingSolutions.Models.DTOs
{
    public class UserDto
    {
        public long UserId { get; set; }
        [Required(AllowEmptyStrings = false, ErrorMessage = "First name cannot be empty.")]
        [StringLength(75, ErrorMessage = "First Name too long.  (75 characters)")]
        public string? FirstName { get; set; }
        [Required(AllowEmptyStrings = false, ErrorMessage = "Last Name cannot be empty.")]
        [StringLength(75, ErrorMessage = "Last Name too long. (75 characters)")]
        public string? LastName { get; set; }
        [StringLength(75, ErrorMessage = "Middle Name too long. (75 characters)")]
        public string? MiddleName { get; set; }
        [StringLength(150, ErrorMessage = "Title too long. (150 characters)")]
        public string? Title { get; set; }
        [DataType(DataType.EmailAddress)]
        [StringLength(150, ErrorMessage = "Email Address too long. (150 characters)")]
        public string? EmailAddress { get; set; }
        [RegularExpression(@"((\(\d{3}\) ?)|(\d{3}-))?\d{3}-\d{4}", ErrorMessage = "Not a valid phone number format: (xxx) xxx-xxxx")]
        [StringLength(50, ErrorMessage = "Phone Number too long. (50 characters)")]
        public string? OfficePhone { get; set; }
        [RegularExpression(@"((\(\d{3}\) ?)|(\d{3}-))?\d{3}-\d{4}", ErrorMessage = "Not a valid phone number format: (xxx) xxx-xxxx")]
        [StringLength(50, ErrorMessage = "Phone Number too long. (50 characters)")]
        public string? OtherPhone { get; set; }
        public Guid? AdUserId { get; set; }
        public bool IsActive { get; set; }
        public int? RegionId { get; set; }
        public Region? Region { get; set; }
        public DateTime CreatedOn { get; set; }
        public long CreatedBy { get; set; }
        public DateTime ModifiedOn { get; set; }
        public long ModifiedBy { get; set; }

        public Roles Role { get; set; }

        public int? RoleId { get; set; }
        public CommissionRole? CommissionRole { get; set; }
    }
}

TicketingRoster

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Threading.Tasks;

namespace TicketingSolutions.Models
{
    [Table("TicketingRosters")]
    public class TicketingRoster : IValidatableObject
    {
        [Key]
        public long TicketingRosterId { get; set; }
        public int PropertyId { get; set; }
        public Property Property { get; set; }
        public long SellerId { get; set; }
        public User Seller { get; set; }
        public bool IsActive { get; set; }
        public DateTime ValidFrom { get; set; }
        public DateTime? ValidTo { get; set; }
        public DateTime CreatedOn { get; set; }
        public long CreatedBy { get; set; }
        public DateTime ModifiedOn { get; set; }
        public long ModifiedBy { get; set; }

        public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
        {
            if (ValidTo <= ValidFrom)
            {
                yield return new ValidationResult("ValidTo cannot be set to a date before or equal to ValidFrom", new[] { nameof(ValidTo) });
            }
        }
    }
}

1 Ответ

0 голосов
/ 29 апреля 2020

Немного сложно следовать, поскольку есть некоторые внешние методы, которые вы не показали в этом примере (возможно, отредактируйте для ясности?), Но похоже, что вы используете что-то вне вашего Pager.razor диктовать, как работает ваш пейджинг. Дешевая распродажа - обратный вызов события DisplayChanged. Я могу предложить вам решить эту проблему, разобрав проблемы, и вы можете заставить все работать. (Я смог довольно быстро запустить его, работая с тем, что у вас есть)

Во-первых, давайте настроим пейджер, поэтому все, что он делает, это обрабатывает информацию на странице, и он содержит все свои собственные логи c сделать это в автономном, многократно используемом компоненте. дайте ему список TItem и RenderFragment<TItem>, и он знает, что делать.

@typeparam TItem

<div class="row d-flex col-9">
    <div class="justify-content-center">

        @if (List != null)
        {
            @foreach (var item in DisplayList)
            {
                @ChildContent(item)
            }
        }

        @if (PageCount > 1 && List.Count > PageSize)
        {

            ...Nothing here was changed, eliminated for brevity...


        }
    </div>
    <select class="custom-select offset-1 col-1 ml-auto" bind="@PageSize" @onchange="@(e => ChangePageSize(e))">
        <option value="10">10</option>
        <option value="25">25</option>
        <option value="50">50</option>
    </select>
</div>

@code {
    [Parameter]
    public List<TItem> List { get; set; }

    public List<TItem> DisplayList { get; set; } = new List<TItem>();

    [Parameter]
    public RenderFragment<TItem> ChildContent { get; set; }

    //[Parameter]
    //public Action<List<TItem>> DisplayChanged { get; set; }

    //[Parameter]
    //public Action<bool> Rendered { get; set; }

    private int PageSize { get; set; } = 10;
    private int CurrentPage { get; set; } = 1;
    private int StartIndex { get; set; }
    private int FinishIndex { get; set; }
    private int PageCount { get; set; }

    //protected override void OnAfterRender(bool firstRender)
    //{
    //  base.OnAfterRender(firstRender);
    //  Rendered?.Invoke(true);
    //}

    private void ChangePageSize(ChangeEventArgs e)
    {
        PageSize = int.Parse(e.Value.ToString());
        ChangePage(1);
    }

    private void ChangeDisplay()
    {
        DisplayList = List
                .Skip((CurrentPage -1) * PageSize)
                .Take(PageSize)
                .ToList();
    }

    protected override void OnParametersSet()
    {
        // Edited
        ChangePage(1);
    }

    protected void ChangePage(int page)
    {
        CurrentPage = page;
        ResetIndex();
        ChangeDisplay();
    }

    private void ResetIndex()
    {
        PageCount = List.Count / PageSize;

        if (List.Count % PageSize > 0)
        {
            PageCount++;
        }

        StartIndex = Math.Max(CurrentPage - 5, 1);
        FinishIndex = Math.Min(CurrentPage + 5, PageCount);
    }
}

Вы увидите, что несколько вещей в блоке @code закомментированы. Вам не нужно это, чтобы заставить это работать. Мы обработаем значения в начальном списке через минуту в родительском компоненте. Вы увидите параметр RenderFragment<TItem> и новое свойство для DisplayList, которое НЕ является параметром. Вы также заметите в разметке, что у нас есть блок @foreach, отображающий экземпляр нашего RenderFragment для каждого элемента в свойстве DisplayList. Если вы будете следовать логике c из метода OnParametersSet и вашим обработчикам для нажатия номеров страниц и стрелок, вы увидите, что мы создаем и отображаем подсписок нашего параметра List на основе количества страниц и номер страницы, и это все, что отображается. Этот компонент теперь отвечает за подкачку элементов из списка, который он дал, и у него нет внешних зависимостей для запуска, кроме списка для отображения, и инструкций о том, как отображать каждый элемент в виде RenderFragment<TItem>.

Далее, в компоненте Parent мы настраиваем вызов на пейджер следующим образом:

<Pager TItem="User" List="FilteredUsers">
    <h6>@context.FirstName @context.LastName is in @context.Role </h6>
</Pager>

Вы можете настроить это так, как хотите, я использовал <h6> теги для иллюстрации, но следуйте указаниям здесь , если вам нужно больше глубины, создание таблиц, списков и т. д. c. Этот компонент теперь принимает дочерний контент между тегами <Pager> и отображает 1 для каждого элемента в своем собственном перемещаемом списке.

Пока что мы отделили логику пейджера c от остальной части страницы, так что теперь это только инструмент рендеринга, а логика пейджинга c является внутренней. Теперь мы можем сосредоточиться на фильтрации и забыть о подкачке.

Фильтрация поиска: Во-первых, я установил начальное значение для поиска "Свойство" и установил вспомогательное поле следующим образом:

private string property = "FirstName";

private string Property
{
    get => property;
    set
    {
        property = value;
        Filter();
    }
}

Теперь оно выравнивается по начальное значение раскрывающегося списка, поскольку <select> обновляет его только при изменении, а также обновляет результаты поиска при изменении раскрывающегося списка.

Я оставил вашу собственность SearchTerm, как у вас.

Теперь в методе Filter:

private void Filter()
{
    if (string.IsNullOrEmpty(SearchTerm))
    {
        FilteredUsers = Users;
    }
    else
    {
        switch (Property)
        {
            case "FirstName":
                FilteredUsers = Users.Where(u => u.FirstName.ToLower().Contains(SearchTerm.ToLower())).ToList();
                break;
            case "LastName":
                FilteredUsers = Users.Where(u => u.LastName.ToLower().Contains(SearchTerm.ToLower())).ToList();
                break;
            case "Role":
                FilteredUsers = Users.Where(u => u.Role.ToString().ToLower().Contains(SearchTerm.ToLower())).ToList();
                break;
            case "Property":
                FilteredUsers = Users.Where(u => u.Role.ToString().ToLower().Contains(SearchTerm.ToLower())).ToList();
                //FilteredUsers = Users.Where(u => TicketingRosters.Any(t => t.Property.PropertyName.ToLower().Contains(SearchTerm.ToLower()) && u.UserId == t.SellerId)).ToList();
                break;
            default:
                FilteredUsers = Users;
                break;
        }
    }
    StateHasChanged();
}

Теперь он сначала проверяет значение поиска и возвращает полный список Users, если он пуст. Если нет, то вы вводите переключатель. Все дела работают так, как у меня, но вы увидите, что я прокомментировал Where logi c, который вы имели изначально. Честно говоря, TicketingRosters мне не знаком, и я могу сказать, что ваша логика c находится в домене, о котором я ничего не знаю, так что вам придется самостоятельно рассуждать об этом последнем случае. НО, если все остальное работает, теперь у вас есть целенаправленный подход к тому, где найти вашу ошибку.

Итак, теперь метод Filter устанавливает отфильтрованный список, пейджер берет весь этот список и обрабатывает пейджинг самостоятельно, и у вас есть красиво разделенные проблемы, поэтому, если у вас есть проблема, вы знаете, где искать. Не правильно пейджинг -> это в пейджере. Неправильная фильтрация -> это в логи фильтра c.

Редактировать

После того как ОП добавил определения классов TicketingRoster и UserDto, я допустил ошибку, думая, что нашел проблему в запросе. Тем не менее, через некоторые операции OP выяснилось, что Pager, который я описал выше, все еще не функционировал совершенно правильно, и запросы были, и после другого взгляда я очистил метод OnParametersSet для вызова ChnagePage(1), что оба сбрасывают текущую страницу на 1 и сбрасывают количество страниц. Тест на моей стороне подтвердил, что проблема ОП с моим первоначальным ответом была устранена.

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

...