havit / Havit.Blazor

Free Bootstrap 5 components for ASP.NET Blazor + optional enterprise-level stack for Blazor development (gRPC code-first, layered architecture, localization, auth, ...)
https://havit.blazor.eu
MIT License
496 stars 67 forks source link

[Request] [HxGrid] - Extending CurrentUserState #869

Closed StarGazer202424 closed 2 weeks ago

StarGazer202424 commented 2 months ago

Hi

As per the demo on the Havit demo page for HxGrid "Persisting State" seems to only store some of the properties of the grid through GirdUserState

This does not take into consideration such as storing "PageSize" so even though the PageIndex maybe stored and restored through state if the "PageSize" is changeable and different from the default size coming back to the grid and having it be restored might lead to not the same data being displayed (ie you have a default page size of 10 but user sets it to 50 and then leaves and comes back).

If I have something like this implemented (I did not implement the storing of it to localstorage) but if I look at what is stored I do not see PageSize.

@inject IDemoDataService DemoDataService

<HxGrid TItem="EmployeeDto"
        DataProvider="GetGridData"
        @bind-CurrentUserState="gridUserState"
        @bind-CurrentUserState:after="StoreGridUserState"
        PageSize="_pageSize"
        Responsive="true">
    <Columns>
        <HxGridColumn HeaderText="Name" ItemTextSelector="employee => employee.Name" />
        <HxGridColumn HeaderText="Phone" ItemTextSelector="employee => employee.Phone" />
        <HxGridColumn HeaderText="Salary" ItemTextSelector="@(employee => employee.Salary.ToString("c0"))" />
        <HxGridColumn HeaderText="Position" ItemTextSelector="employee => employee.Position" />
        <HxGridColumn HeaderText="Location" ItemTextSelector="employee => employee.Location" />
    </Columns>
    <PaginationTemplate Context="pagination">
        @{
            int totalPages = (pagination.TotalCount + pagination.PageSize - 1) / pagination.PageSize;
            int firstItemPosition = pagination.CurrentUserState.PageIndex * pagination.PageSize + 1;
            int lastItemPosition = Math.Min(firstItemPosition + pagination.PageSize - 1, pagination.TotalCount);
        }
        <div class="row">
            <div class="col d-flex gap-2 align-items-center">
                Rows per page: <HxSelect @bind-Value="@_pageSize" Data="_pageSizes" Nullable="false" AutoSort="false" InputSize="InputSize.Small" />
            </div>
            <div class="col d-flex justify-content-center align-items-center">
                Showing @firstItemPosition to @lastItemPosition of @pagination.TotalCount entries
            </div>
            <div class="col d-flex align-items-center justify-content-end">
                <HxButton Icon="BootstrapIcon.ChevronBarLeft" Enabled="pagination.CurrentUserState.PageIndex > 0" OnClick="() => pagination.ChangeCurrentPageIndexAsync(0)" Color="ThemeColor.Link" />
                <HxButton Icon="BootstrapIcon.ChevronLeft" Enabled="pagination.CurrentUserState.PageIndex > 0" OnClick="() => pagination.ChangeCurrentPageIndexAsync(pagination.CurrentUserState.PageIndex - 1)" Color="ThemeColor.Link" />
                <HxButton Icon="BootstrapIcon.ChevronRight" Enabled="pagination.CurrentUserState.PageIndex + 1 < totalPages" OnClick="() => pagination.ChangeCurrentPageIndexAsync(pagination.CurrentUserState.PageIndex + 1)" Color="ThemeColor.Link" />
                <HxButton Icon="BootstrapIcon.ChevronBarRight" Enabled="pagination.CurrentUserState.PageIndex + 1 < totalPages" OnClick="() => pagination.ChangeCurrentPageIndexAsync(totalPages - 1)" Color="ThemeColor.Link" />
            </div>
        </div>
    </PaginationTemplate>
</HxGrid>

<HxInputTextArea Label="Grid user state JSON serialization (editable)" @bind-Value="serializedGridUserState" @bind-Value:after="LoadSerializedUserState" />

@code {
    private GridUserState gridUserState = new();
    private string serializedGridUserState; // replace this field with your own storage (e.g. local storage or database)

    private int _pageSize = 5;
    private readonly List<int> _pageSizes = [5, 10, 20];

    private async Task<GridDataProviderResult<EmployeeDto>> GetGridData(GridDataProviderRequest<EmployeeDto> request)
    {
        return new GridDataProviderResult<EmployeeDto>()
            {
                Data = await DemoDataService.GetEmployeesDataFragmentAsync(request.StartIndex, request.Count, request.CancellationToken),
                TotalCount = await DemoDataService.GetEmployeesCountAsync(request.CancellationToken)
            };
    }

    private void StoreGridUserState()
    {
        serializedGridUserState = JsonSerializer.Serialize(gridUserState);
    }

    private void LoadSerializedUserState()
    {
        try
        {
            gridUserState = JsonSerializer.Deserialize<GridUserState>(serializedGridUserState);
        }
        catch (JsonException)
        {
            // invalid state serialization
        }
    }

    public class EmployeeDto
    {
        public int Id { get; internal set; }
        public string Name { get; internal set; }
        public string Email { get; internal set; }
        public string Phone { get; internal set; }
        public string Position { get; internal set; }
        public string Location { get; internal set; }
        public decimal Salary { get; internal set; }
    }

}

Secondly, it would be nice if when using the "select with checkboxes" if you could optionally store the choices to restore them again if desired.

Mudblazor does this to an extent where at least if you switch between pages and go back the checked ones will be still checked. I do however like Havit implementation of the "select all" as I think it is better that only visible ones on the page are checked and not everything.

Thanks for your consideration for these future enhancements

hakenr commented 2 months ago

The PageSize value is not part of the CurrentUserState and is exposed as a separate parameter (it's expected to be managed externally more often than those CurrentUserState properties, where the grid handles them). You are responsible for persisting the value if you allow the user to change it. The same applies to any filter you add outside the grid that influences the data. To restore the state, you need to restore the filter.

Regarding multi-selection, we are planning to support "remember the selection from other pages," (#576) but this is currently in the backlog and awaiting budget approval for implementation. However, this feature won't be part of CurrentUserState, so you'll need to persist the SelectedItems on your own if you want to preserve the selection across user visits.

StarGazer202424 commented 2 months ago

The PageSize value is not part of the CurrentUserState and is exposed as a separate parameter (it's expected to be managed externally more often than those CurrentUserState properties, where the grid handles them). You are responsible for persisting the value if you allow the user to change it. The same applies to any filter you add outside the grid that influences the data. To restore the state, you need to restore the filter.

Regarding multi-selection, we are planning to support "remember the selection from other pages," (#576) but this is currently in the backlog and awaiting budget approval for implementation. However, this feature won't be part of CurrentUserState, so you'll need to persist the SelectedItems on your own if you want to preserve the selection across user visits.

Okay, that is how I am handling PageSize by managing it with my own state. It just took be off guard a bit as when you do set that 'CurrentUserState' I was expecting them to play nicely together. Maybe some documentation of it on the demo page would help?

Great to hear that it multi-slection to have "remember feature" has been identified as a future feature.

I was trying to store "SelectedItems" in my own state what is no problem, but when I try to reapply those selected items back they don't show checked. I think this because of the "GetGridData" even if it returns the same set of data they are considered "different" objects then the ones I stored as I think you just do a "contains" to check and the default contains looks at it and thinks they are different.

I tried to write an override the equals on my object to do more checking and that does work but then I get into the problem of how to track everything. Do I every time store all the results that GetGridData returns and then do a check in the "GetGridData" and then compare it to the "SelectedItems" and then use those if I get a match.

But then another problem is "SelectedItems" wipes out the stored data when you move from to another page in the grid. So I got to account for that as well.

Just starts to seem a bit complicated to restore it right now, unless I am missing something.

hakenr commented 2 weeks ago

I was trying to store "SelectedItems" in my own state, which works fine, but when I try to reapply those selected items, they don't appear as checked. I think this is because of "GetGridData" - even if it returns the same set of data, they are considered "different" objects than the ones I stored. I believe you're using a "contains" check, and the default Contains method treats them as different objects.

We use item.Equals(SelectedDataItem) to compare an item from DataProvider with SelectedDataItem (single-selection mode) and SelectedDataItems?.Contains(item) to check if an item from DataProvider is selected (multi-selection mode).

Overriding Equals() (and GetHashCode()) should be all you need to match the items correctly.

But another problem is that "SelectedItems" wipes out the stored data when you navigate to another page in the grid. So I need to account for that as well.

See #576.
We will likely make this configurable in the near future.