Skip to content

Add EmptyContent parameter to Virtualize component #49185

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jul 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/Components/Web/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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<TItem>.EmptyContent.get -> Microsoft.AspNetCore.Components.RenderFragment?
Microsoft.AspNetCore.Components.Web.Virtualization.Virtualize<TItem>.EmptyContent.set -> void
32 changes: 25 additions & 7 deletions src/Components/Web/src/Virtualization/Virtualize.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ public sealed class Virtualize<TItem> : ComponentBase, IVirtualizeJsCallbacks, I

private RenderFragment<PlaceholderContext>? _placeholder;

private RenderFragment? _emptyContent;

private bool _loading;

[Inject]
private IJSRuntime JSRuntime { get; set; } = default!;

Expand All @@ -68,6 +72,13 @@ public sealed class Virtualize<TItem> : ComponentBase, IVirtualizeJsCallbacks, I
[Parameter]
public RenderFragment<PlaceholderContext>? Placeholder { get; set; }

/// <summary>
/// Gets or sets the content to show when <see cref="Items"/> is empty
/// or when the <see cref="ItemsProviderResult&lt;TItem&gt;.TotalItemCount"/> is zero.
/// </summary>
[Parameter]
public RenderFragment? EmptyContent { get; set; }

/// <summary>
/// Gets the size of each item in pixels. Defaults to 50px.
/// </summary>
Expand Down Expand Up @@ -167,6 +178,7 @@ protected override void OnParametersSet()

_itemTemplate = ItemContent ?? ChildContent;
_placeholder = Placeholder ?? DefaultPlaceholder;
_emptyContent = EmptyContent;
}

/// <inheritdoc />
Expand Down Expand Up @@ -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);
Expand All @@ -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++)
Expand All @@ -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();
}
Expand Down Expand Up @@ -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);
Expand All @@ -368,6 +385,7 @@ private async ValueTask RefreshDataCoreAsync(bool renderOnSuccess)
_itemCount = result.TotalItemCount;
_loadedItems = result.Items;
_loadedItemsStartIndex = request.StartIndex;
_loading = false;

if (renderOnSuccess)
{
Expand Down
45 changes: 45 additions & 0 deletions src/Components/test/E2ETest/Tests/VirtualizationTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,51 @@ public void CanHandleDataSetShrinkingWithExistingOffsetAlreadyBeyondNewListEnd(s
Browser.True(() => GetPeopleNames(container).Contains("Person 25"));
}

[Fact]
public void EmptyContentRendered_Sync()
{
Browser.MountTestComponent<VirtualizationComponent>();
Browser.Exists(By.Id("no-data-sync"));
}

[Fact]
public void EmptyContentRendered_Async()
{
Browser.MountTestComponent<VirtualizationComponent>();
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 <EmptyContent> is not shown while loading
Browser.DoesNotExist(By.Id("no-data-async"));

// Load the initial set of items.
finishLoadingWithItemsButton.Click();

// Check that <EmptyContent> is still not shown (because there are items loaded)
Browser.DoesNotExist(By.Id("no-data-async"));

// Start loading
refreshDataAsync.Click();

// Check that <EmptyContent> is not shown
Browser.DoesNotExist(By.Id("no-data-async"));

// Simulate 0 items
finishLoadingWithoutItemsButton.Click();

// Check that <EmptyContent> 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"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,40 @@

<p>
Asynchronous:<br />
<button id="finish-loading-button" @onclick="FinishLoadingAsync">Finish loading</button><br />
<button id="finish-loading-button-empty" @onclick="() => FinishLoadingAsync(0)">Finish loading with total item count = 0</button><br />
<button id="finish-loading-button" @onclick="() => FinishLoadingAsync(200)">Finish loading with total item count = 200</button><br />
<button id="refresh-data-async" @onclick="() => asyncComponent.RefreshDataAsync()">Call RefreshDataAsync</button><br />
Cancellation count: <span id="cancellation-count">@asyncCancellationCount</span><br />
<div id="async-container" style="background-color: #eee; height: 500px; overflow-y: auto">
<Virtualize ItemsProvider="GetItemsAsync" ItemSize="itemSize">
<Virtualize @ref="asyncComponent" ItemsProvider="GetItemsAsync" ItemSize="itemSize">
<ItemContent>
<div @key="context" id="async-item" style="height: @(itemSize)px; background-color: rgb(@((context % 2) * 255), @((1-(context % 2)) * 255), 255);">Item @context</div>
</ItemContent>
<Placeholder>
<div id="async-placeholder" style="height: @(context.Size)px; background-color: orange;">Loading item @context.Index...</div>
</Placeholder>
<EmptyContent>
<p id="no-data-async">No data to show</p>
</EmptyContent>
</Virtualize>
</div>
</p>

<p>
Empty Content:<br />
<div id="empty-container" style="background-color: #eee; height: 100px; overflow-y: auto">
<Virtualize Items="@emptyCollection" ItemSize="itemSize">
<ItemContent>
<div @key="context" style="height: @(itemSize)px; background-color: rgb(@((context % 2) * 255), @((1-(context % 2)) * 255), 255);">
Item @context</div>
</ItemContent>
<EmptyContent>
<p id="no-data-sync">No data to show</p>
</EmptyContent>
</Virtualize>
</div>
</p>

<p>
Slightly incorrect item size:<br />
<div id="incorrect-size-container" style="background-color: #eee; height: 500px; overflow-y: auto">
Expand All @@ -47,12 +67,13 @@
@code {
float itemSize = 100;
ICollection<int> fixedItems = Enumerable.Range(0, 1000).ToList();
ICollection<int> emptyCollection = Array.Empty<int>();

int asyncTotalItemCount = 200;
int asyncCancellationCount = 0;
TaskCompletionSource asyncTcs = new TaskCompletionSource();

HashSet<int> cachedItems = new HashSet<int>();
Virtualize<int> asyncComponent;

async ValueTask<ItemsProviderResult<int>> GetItemsAsync(ItemsProviderRequest request)
{
Expand All @@ -67,8 +88,9 @@
return new ItemsProviderResult<int>(Enumerable.Range(request.StartIndex, request.Count), asyncTotalItemCount);
}

void FinishLoadingAsync()
void FinishLoadingAsync(int totalItemCount)
{
asyncTotalItemCount = totalItemCount;
asyncTcs.SetResult();
asyncTcs = new TaskCompletionSource();
}
Expand Down