From 978654e8d36a78cdacb50ae6509785ed270c9fd5 Mon Sep 17 00:00:00 2001 From: Robert Haken Date: Mon, 25 Nov 2024 15:47:39 +0100 Subject: [PATCH] #950 [HxGrid] Allowing MultiSelectionEnabled with InfiniteScroll navigation (virtualized) - basic support (hidden select/deselect all checkbox) --- .../Grids/HxGrid.razor | 1 + .../Grids/HxGrid.razor.cs | 8 ++-- .../HxMultiSelectGridColumnInternal.cs | 42 ++++++++++------- .../HxGridDoc/HxGrid_Demo_Multiselect.razor | 4 +- .../HxGridDoc/HxGrid_Documentation.razor | 9 ++-- ...iteScroll_MultiSelectionEnabled_Test.razor | 45 +++++++++++++++++++ 6 files changed, 82 insertions(+), 27 deletions(-) create mode 100644 Havit.Blazor.TestApp/Havit.Blazor.TestApp.Client/HxGridTests/HxGrid_InfiniteScroll_MultiSelectionEnabled_Test.razor diff --git a/Havit.Blazor.Components.Web.Bootstrap/Grids/HxGrid.razor b/Havit.Blazor.Components.Web.Bootstrap/Grids/HxGrid.razor index e40797ea6..3c94a3584 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/Grids/HxGrid.razor +++ b/Havit.Blazor.Components.Web.Bootstrap/Grids/HxGrid.razor @@ -14,6 +14,7 @@ SelectedDataItems="@SelectedDataItems" OnSelectDataItemClicked="HandleMultiSelectSelectDataItemClicked" OnUnselectDataItemClicked="HandleMultiSelectUnselectDataItemClicked" + SelectDeselectAllHeaderVisible="ContentNavigationModeEffective != GridContentNavigationMode.InfiniteScroll " OnSelectAllClicked="HandleMultiSelectSelectAllClicked" OnSelectNoneClicked="HandleMultiSelectSelectNoneClicked" /> } diff --git a/Havit.Blazor.Components.Web.Bootstrap/Grids/HxGrid.razor.cs b/Havit.Blazor.Components.Web.Bootstrap/Grids/HxGrid.razor.cs index cd0a519e9..9053f6e16 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/Grids/HxGrid.razor.cs +++ b/Havit.Blazor.Components.Web.Bootstrap/Grids/HxGrid.razor.cs @@ -397,7 +397,10 @@ protected override async Task OnParametersSetAsync() Contract.Requires(DataProvider != null, $"Property {nameof(DataProvider)} on {GetType()} must have a value."); Contract.Requires(CurrentUserState != null, $"Property {nameof(CurrentUserState)} on {GetType()} must have a value."); - Contract.Requires(!MultiSelectionEnabled || (ContentNavigationModeEffective != GridContentNavigationMode.InfiniteScroll), $"Cannot use multi selection with infinite scroll on {GetType()}."); + if ((ContentNavigationModeEffective == GridContentNavigationMode.InfiniteScroll) && MultiSelectionEnabled) + { + Contract.Requires(PreserveSelectionEffective, $"{nameof(PreserveSelection)} must be enabled on {nameof(HxGrid)} when using {nameof(GridContentNavigationMode.InfiniteScroll)} with {nameof(MultiSelectionEnabled)}."); + } if (_previousUserState != CurrentUserState) { @@ -931,7 +934,6 @@ private async Task HandleMultiSelectSelectDataItemClicked(TItem selectedDataItem private async Task HandleMultiSelectUnselectDataItemClicked(TItem selectedDataItem) { Contract.Requires(MultiSelectionEnabled); - Contract.Requires((ContentNavigationModeEffective == GridContentNavigationMode.Pagination) || (ContentNavigationModeEffective == GridContentNavigationMode.LoadMore) || (ContentNavigationModeEffective == GridContentNavigationMode.PaginationAndLoadMore)); var selectedDataItems = SelectedDataItems?.ToHashSet() ?? []; if (selectedDataItems.Remove(selectedDataItem)) @@ -943,7 +945,6 @@ private async Task HandleMultiSelectUnselectDataItemClicked(TItem selectedDataIt private async Task HandleMultiSelectSelectAllClicked() { Contract.Requires(MultiSelectionEnabled, nameof(MultiSelectionEnabled)); - Contract.Requires((ContentNavigationModeEffective == GridContentNavigationMode.Pagination) || (ContentNavigationModeEffective == GridContentNavigationMode.LoadMore) || (ContentNavigationModeEffective == GridContentNavigationMode.PaginationAndLoadMore)); if (_paginationDataItemsToRender is null) { @@ -971,7 +972,6 @@ private async Task HandleMultiSelectSelectAllClicked() private async Task HandleMultiSelectSelectNoneClicked() { Contract.Requires(MultiSelectionEnabled); - Contract.Requires((ContentNavigationModeEffective == GridContentNavigationMode.Pagination) || (ContentNavigationModeEffective == GridContentNavigationMode.LoadMore) || (ContentNavigationModeEffective == GridContentNavigationMode.PaginationAndLoadMore)); if (PreserveSelectionEffective) { diff --git a/Havit.Blazor.Components.Web.Bootstrap/Grids/Internal/HxMultiSelectGridColumnInternal.cs b/Havit.Blazor.Components.Web.Bootstrap/Grids/Internal/HxMultiSelectGridColumnInternal.cs index ec24dda32..119528651 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/Grids/Internal/HxMultiSelectGridColumnInternal.cs +++ b/Havit.Blazor.Components.Web.Bootstrap/Grids/Internal/HxMultiSelectGridColumnInternal.cs @@ -2,6 +2,7 @@ public class HxMultiSelectGridColumnInternal : HxGridColumnBase { + [Parameter, EditorRequired] public bool SelectDeselectAllHeaderVisible { get; set; } [Parameter] public HashSet SelectedDataItems { get; set; } [Parameter] public bool AllDataItemsSelected { get; set; } [Parameter] public EventCallback OnSelectAllClicked { get; set; } @@ -18,28 +19,35 @@ public class HxMultiSelectGridColumnInternal : HxGridColumnBase /// protected override GridCellTemplate GetHeaderCellTemplate(GridHeaderCellContext context) { - return new GridCellTemplate + if (SelectDeselectAllHeaderVisible) { - CssClass = "text-center", - Template = (RenderTreeBuilder builder) => + return new GridCellTemplate { - builder.OpenElement(100, "input"); - builder.AddAttribute(101, "type", "checkbox"); - builder.AddAttribute(102, "class", "form-check-input"); + CssClass = "text-center", + Template = (RenderTreeBuilder builder) => + { + builder.OpenElement(100, "input"); + builder.AddAttribute(101, "type", "checkbox"); + builder.AddAttribute(102, "class", "form-check-input"); - builder.AddAttribute(103, "checked", AllDataItemsSelected); - builder.AddAttribute(104, "onchange", EventCallback.Factory.Create(this, HandleSelectAllOrNoneClick)); - builder.SetUpdatesAttributeName("checked"); - builder.AddEventStopPropagationAttribute(105, "onclick", true); + builder.AddAttribute(103, "checked", AllDataItemsSelected); + builder.AddAttribute(104, "onchange", EventCallback.Factory.Create(this, HandleSelectAllOrNoneClick)); + builder.SetUpdatesAttributeName("checked"); + builder.AddEventStopPropagationAttribute(105, "onclick", true); - if ((context.TotalCount is null) || (context.TotalCount == 0)) - { - builder.AddAttribute(102, "disabled"); - } + if ((context.TotalCount is null) || (context.TotalCount == 0)) + { + builder.AddAttribute(102, "disabled"); + } - builder.CloseElement(); // input - } - }; + builder.CloseElement(); // input + } + }; + } + else + { + return GridCellTemplate.Empty; + } } /// diff --git a/Havit.Blazor.Documentation/Pages/Components/HxGridDoc/HxGrid_Demo_Multiselect.razor b/Havit.Blazor.Documentation/Pages/Components/HxGridDoc/HxGrid_Demo_Multiselect.razor index d6da70ab2..d0edc7490 100644 --- a/Havit.Blazor.Documentation/Pages/Components/HxGridDoc/HxGrid_Demo_Multiselect.razor +++ b/Havit.Blazor.Documentation/Pages/Components/HxGridDoc/HxGrid_Demo_Multiselect.razor @@ -15,12 +15,12 @@ - + +

Selected employees: @(String.Join(", ", selectedEmployees.Select(e => e.Name)))

- @code { private HashSet selectedEmployees = new(); diff --git a/Havit.Blazor.Documentation/Pages/Components/HxGridDoc/HxGrid_Documentation.razor b/Havit.Blazor.Documentation/Pages/Components/HxGridDoc/HxGrid_Documentation.razor index 4fec2f658..8ac28d5b4 100644 --- a/Havit.Blazor.Documentation/Pages/Components/HxGridDoc/HxGrid_Documentation.razor +++ b/Havit.Blazor.Documentation/Pages/Components/HxGridDoc/HxGrid_Documentation.razor @@ -122,21 +122,22 @@ Enable multi-row selection for users by setting @nameof(HxGrid.MultiSelectionEnabled)="true". The selected items can be accessed via the @nameof(HxGrid.SelectedDataItems) parameter, which is bindable.

+

Note that @nameof(HxGrid.SelectedDataItems) only includes visible items. By default, items are removed from the selection when they become unrendered (for example, after paging, sorting, etc.). However, this behavior can be modified by setting the @nameof(HxGrid.PreserveSelection)="true" parameter, - which ensures that selected items are preserved across data operations such as paging, sorting or manual invocation of RefreshDataAsync. + which ensures that selected items are preserved across data operations such as paging, sorting, or manual invocation of RefreshDataAsync.

The "select/deselect all" checkbox operates only on visible records and adds/removes them from the selection accordingly. Non-visible items (e.g., from other pages) are not affected by this operation.

- Multi-row selection is not compatible with @nameof(GridContentNavigationMode.InfiniteScroll).
- This design decision might change in the future. + When using @nameof(GridContentNavigationMode.InfiniteScroll), @nameof(HxGrid.PreserveSelection)="true" is required for multi-row selection to work. + Attempting to use @nameof(HxGrid.MultiSelectionEnabled)="true" without enabling PreserveSelection will result in an exception. Additionally, the "select/deselect all" checkbox is intentionally hidden in this mode, + as the grid does not have access to all data to reliably perform this operation. For more details, see ticket #950.
- diff --git a/Havit.Blazor.TestApp/Havit.Blazor.TestApp.Client/HxGridTests/HxGrid_InfiniteScroll_MultiSelectionEnabled_Test.razor b/Havit.Blazor.TestApp/Havit.Blazor.TestApp.Client/HxGridTests/HxGrid_InfiniteScroll_MultiSelectionEnabled_Test.razor new file mode 100644 index 000000000..eae7ad5a9 --- /dev/null +++ b/Havit.Blazor.TestApp/Havit.Blazor.TestApp.Client/HxGridTests/HxGrid_InfiniteScroll_MultiSelectionEnabled_Test.razor @@ -0,0 +1,45 @@ +@page "/HxGrid_InfiniteScroll_MultiSelectionEnabled" +@rendermode InteractiveServer +@inject IDemoDataService DemoDataService + + + + + + + + + + + + + +

+ Selected employees: @(String.Join(", ", selectedEmployees.Select(e => e.Name))) +

+ + +@code { + private HashSet selectedEmployees = new(); + + private async Task> GetGridData(GridDataProviderRequest request) + { + var response = await DemoDataService.GetEmployeesDataFragmentAsync(request.StartIndex, request.Count, request.CancellationToken); + return new GridDataProviderResult() + { + Data = response.Data, + TotalCount = response.TotalCount + }; + } +} \ No newline at end of file