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();
}