diff --git a/src/Components/Web/src/PublicAPI.Unshipped.txt b/src/Components/Web/src/PublicAPI.Unshipped.txt index cb3b73f4a142..f32a10122f78 100644 --- a/src/Components/Web/src/PublicAPI.Unshipped.txt +++ b/src/Components/Web/src/PublicAPI.Unshipped.txt @@ -74,3 +74,5 @@ static Microsoft.AspNetCore.Components.Web.RenderMode.Auto.get -> Microsoft.AspN static Microsoft.AspNetCore.Components.Web.RenderMode.Server.get -> Microsoft.AspNetCore.Components.Web.ServerRenderMode! static Microsoft.AspNetCore.Components.Web.RenderMode.WebAssembly.get -> Microsoft.AspNetCore.Components.Web.WebAssemblyRenderMode! virtual Microsoft.AspNetCore.Components.HtmlRendering.Infrastructure.StaticHtmlRenderer.WriteComponentHtml(int componentId, System.IO.TextWriter! output) -> void +Microsoft.AspNetCore.Components.Web.Virtualization.Virtualize.EmptyContent.get -> Microsoft.AspNetCore.Components.RenderFragment? +Microsoft.AspNetCore.Components.Web.Virtualization.Virtualize.EmptyContent.set -> void diff --git a/src/Components/Web/src/Virtualization/Virtualize.cs b/src/Components/Web/src/Virtualization/Virtualize.cs index b2418c29d637..61f22da4a73a 100644 --- a/src/Components/Web/src/Virtualization/Virtualize.cs +++ b/src/Components/Web/src/Virtualization/Virtualize.cs @@ -47,6 +47,10 @@ public sealed class Virtualize : ComponentBase, IVirtualizeJsCallbacks, I private RenderFragment? _placeholder; + private RenderFragment? _emptyContent; + + private bool _loading; + [Inject] private IJSRuntime JSRuntime { get; set; } = default!; @@ -68,6 +72,13 @@ public sealed class Virtualize : ComponentBase, IVirtualizeJsCallbacks, I [Parameter] public RenderFragment? Placeholder { get; set; } + /// + /// Gets or sets the content to show when is empty + /// or when the is zero. + /// + [Parameter] + public RenderFragment? EmptyContent { get; set; } + /// /// Gets the size of each item in pixels. Defaults to 50px. /// @@ -167,6 +178,7 @@ protected override void OnParametersSet() _itemTemplate = ItemContent ?? ChildContent; _placeholder = Placeholder ?? DefaultPlaceholder; + _emptyContent = EmptyContent; } /// @@ -213,15 +225,19 @@ protected override void BuildRenderTree(RenderTreeBuilder builder) _lastRenderedItemCount = 0; - // Render the loaded items. - if (_loadedItems != null && _itemTemplate != null) + if (_loadedItems != null && !_loading && _itemCount == 0 && _emptyContent != null) + { + builder.AddContent(4, _emptyContent); + } + else if (_loadedItems != null && _itemTemplate != null) { var itemsToShow = _loadedItems .Skip(_itemsBefore - _loadedItemsStartIndex) .Take(lastItemIndex - _loadedItemsStartIndex); - builder.OpenRegion(4); + builder.OpenRegion(5); + // Render the loaded items. foreach (var item in itemsToShow) { _itemTemplate(item)(builder); @@ -235,7 +251,7 @@ protected override void BuildRenderTree(RenderTreeBuilder builder) _lastRenderedPlaceholderCount = Math.Max(0, lastItemIndex - _itemsBefore - _lastRenderedItemCount); - builder.OpenRegion(5); + builder.OpenRegion(6); // Render the placeholders after the loaded items. for (; renderIndex < lastItemIndex; renderIndex++) @@ -247,9 +263,9 @@ protected override void BuildRenderTree(RenderTreeBuilder builder) var itemsAfter = Math.Max(0, _itemCount - _visibleItemCapacity - _itemsBefore); - builder.OpenElement(6, SpacerElement); - builder.AddAttribute(7, "style", GetSpacerStyle(itemsAfter)); - builder.AddElementReferenceCapture(8, elementReference => _spacerAfter = elementReference); + builder.OpenElement(7, SpacerElement); + builder.AddAttribute(8, "style", GetSpacerStyle(itemsAfter)); + builder.AddElementReferenceCapture(9, elementReference => _spacerAfter = elementReference); builder.CloseElement(); } @@ -354,6 +370,7 @@ private async ValueTask RefreshDataCoreAsync(bool renderOnSuccess) { _refreshCts = new CancellationTokenSource(); cancellationToken = _refreshCts.Token; + _loading = true; } var request = new ItemsProviderRequest(_itemsBefore, _visibleItemCapacity, cancellationToken); @@ -368,6 +385,7 @@ private async ValueTask RefreshDataCoreAsync(bool renderOnSuccess) _itemCount = result.TotalItemCount; _loadedItems = result.Items; _loadedItemsStartIndex = request.StartIndex; + _loading = false; if (renderOnSuccess) { diff --git a/src/Components/test/E2ETest/Tests/VirtualizationTest.cs b/src/Components/test/E2ETest/Tests/VirtualizationTest.cs index b954498b481e..46e55d6318a8 100644 --- a/src/Components/test/E2ETest/Tests/VirtualizationTest.cs +++ b/src/Components/test/E2ETest/Tests/VirtualizationTest.cs @@ -489,6 +489,51 @@ public void CanHandleDataSetShrinkingWithExistingOffsetAlreadyBeyondNewListEnd(s Browser.True(() => GetPeopleNames(container).Contains("Person 25")); } + [Fact] + public void EmptyContentRendered_Sync() + { + Browser.MountTestComponent(); + Browser.Exists(By.Id("no-data-sync")); + } + + [Fact] + public void EmptyContentRendered_Async() + { + Browser.MountTestComponent(); + var finishLoadingWithItemsButton = Browser.Exists(By.Id("finish-loading-button")); + var finishLoadingWithoutItemsButton = Browser.Exists(By.Id("finish-loading-button-empty")); + var refreshDataAsync = Browser.Exists(By.Id("refresh-data-async")); + + // Check that no items or placeholders are visible. + // No data fetches have happened so we don't know how many items there are. + Browser.Equal(0, GetItemCount); + Browser.Equal(0, GetPlaceholderCount); + + // Check that is not shown while loading + Browser.DoesNotExist(By.Id("no-data-async")); + + // Load the initial set of items. + finishLoadingWithItemsButton.Click(); + + // Check that is still not shown (because there are items loaded) + Browser.DoesNotExist(By.Id("no-data-async")); + + // Start loading + refreshDataAsync.Click(); + + // Check that is not shown + Browser.DoesNotExist(By.Id("no-data-async")); + + // Simulate 0 items + finishLoadingWithoutItemsButton.Click(); + + // Check that is shown + Browser.Exists(By.Id("no-data-async")); + + int GetItemCount() => Browser.FindElements(By.Id("async-item")).Count; + int GetPlaceholderCount() => Browser.FindElements(By.Id("async-placeholder")).Count; + } + private string[] GetPeopleNames(IWebElement container) { var peopleElements = container.FindElements(By.CssSelector(".person span")); diff --git a/src/Components/test/testassets/BasicTestApp/VirtualizationComponent.razor b/src/Components/test/testassets/BasicTestApp/VirtualizationComponent.razor index 0ce6e49e6ebe..45c1afef3cde 100644 --- a/src/Components/test/testassets/BasicTestApp/VirtualizationComponent.razor +++ b/src/Components/test/testassets/BasicTestApp/VirtualizationComponent.razor @@ -14,20 +14,40 @@

Asynchronous:
-
+
+
+
Cancellation count: @asyncCancellationCount

- +
Item @context
Loading item @context.Index...
+ +

No data to show

+

+

+ Empty Content:
+

+ + +
+ Item @context
+
+ +

No data to show

+
+
+
+

+

Slightly incorrect item size:

@@ -47,12 +67,13 @@ @code { float itemSize = 100; ICollection fixedItems = Enumerable.Range(0, 1000).ToList(); + ICollection emptyCollection = Array.Empty(); int asyncTotalItemCount = 200; int asyncCancellationCount = 0; TaskCompletionSource asyncTcs = new TaskCompletionSource(); - HashSet cachedItems = new HashSet(); + Virtualize asyncComponent; async ValueTask> GetItemsAsync(ItemsProviderRequest request) { @@ -67,8 +88,9 @@ return new ItemsProviderResult(Enumerable.Range(request.StartIndex, request.Count), asyncTotalItemCount); } - void FinishLoadingAsync() + void FinishLoadingAsync(int totalItemCount) { + asyncTotalItemCount = totalItemCount; asyncTcs.SetResult(); asyncTcs = new TaskCompletionSource(); }