Skip to content

Commit 15f0daa

Browse files
authored
Add EmptyContent parameter to Virtualize component (dotnet#49185)
1 parent 2cb1268 commit 15f0daa

File tree

4 files changed

+98
-11
lines changed

4 files changed

+98
-11
lines changed

src/Components/Web/src/PublicAPI.Unshipped.txt

+2
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,5 @@ static Microsoft.AspNetCore.Components.Web.RenderMode.Auto.get -> Microsoft.AspN
9595
static Microsoft.AspNetCore.Components.Web.RenderMode.Server.get -> Microsoft.AspNetCore.Components.Web.ServerRenderMode!
9696
static Microsoft.AspNetCore.Components.Web.RenderMode.WebAssembly.get -> Microsoft.AspNetCore.Components.Web.WebAssemblyRenderMode!
9797
virtual Microsoft.AspNetCore.Components.HtmlRendering.Infrastructure.StaticHtmlRenderer.WriteComponentHtml(int componentId, System.IO.TextWriter! output) -> void
98+
Microsoft.AspNetCore.Components.Web.Virtualization.Virtualize<TItem>.EmptyContent.get -> Microsoft.AspNetCore.Components.RenderFragment?
99+
Microsoft.AspNetCore.Components.Web.Virtualization.Virtualize<TItem>.EmptyContent.set -> void

src/Components/Web/src/Virtualization/Virtualize.cs

+25-7
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ public sealed class Virtualize<TItem> : ComponentBase, IVirtualizeJsCallbacks, I
4747

4848
private RenderFragment<PlaceholderContext>? _placeholder;
4949

50+
private RenderFragment? _emptyContent;
51+
52+
private bool _loading;
53+
5054
[Inject]
5155
private IJSRuntime JSRuntime { get; set; } = default!;
5256

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

75+
/// <summary>
76+
/// Gets or sets the content to show when <see cref="Items"/> is empty
77+
/// or when the <see cref="ItemsProviderResult&lt;TItem&gt;.TotalItemCount"/> is zero.
78+
/// </summary>
79+
[Parameter]
80+
public RenderFragment? EmptyContent { get; set; }
81+
7182
/// <summary>
7283
/// Gets the size of each item in pixels. Defaults to 50px.
7384
/// </summary>
@@ -167,6 +178,7 @@ protected override void OnParametersSet()
167178

168179
_itemTemplate = ItemContent ?? ChildContent;
169180
_placeholder = Placeholder ?? DefaultPlaceholder;
181+
_emptyContent = EmptyContent;
170182
}
171183

172184
/// <inheritdoc />
@@ -213,15 +225,19 @@ protected override void BuildRenderTree(RenderTreeBuilder builder)
213225

214226
_lastRenderedItemCount = 0;
215227

216-
// Render the loaded items.
217-
if (_loadedItems != null && _itemTemplate != null)
228+
if (_loadedItems != null && !_loading && _itemCount == 0 && _emptyContent != null)
229+
{
230+
builder.AddContent(4, _emptyContent);
231+
}
232+
else if (_loadedItems != null && _itemTemplate != null)
218233
{
219234
var itemsToShow = _loadedItems
220235
.Skip(_itemsBefore - _loadedItemsStartIndex)
221236
.Take(lastItemIndex - _loadedItemsStartIndex);
222237

223-
builder.OpenRegion(4);
238+
builder.OpenRegion(5);
224239

240+
// Render the loaded items.
225241
foreach (var item in itemsToShow)
226242
{
227243
_itemTemplate(item)(builder);
@@ -235,7 +251,7 @@ protected override void BuildRenderTree(RenderTreeBuilder builder)
235251

236252
_lastRenderedPlaceholderCount = Math.Max(0, lastItemIndex - _itemsBefore - _lastRenderedItemCount);
237253

238-
builder.OpenRegion(5);
254+
builder.OpenRegion(6);
239255

240256
// Render the placeholders after the loaded items.
241257
for (; renderIndex < lastItemIndex; renderIndex++)
@@ -247,9 +263,9 @@ protected override void BuildRenderTree(RenderTreeBuilder builder)
247263

248264
var itemsAfter = Math.Max(0, _itemCount - _visibleItemCapacity - _itemsBefore);
249265

250-
builder.OpenElement(6, SpacerElement);
251-
builder.AddAttribute(7, "style", GetSpacerStyle(itemsAfter));
252-
builder.AddElementReferenceCapture(8, elementReference => _spacerAfter = elementReference);
266+
builder.OpenElement(7, SpacerElement);
267+
builder.AddAttribute(8, "style", GetSpacerStyle(itemsAfter));
268+
builder.AddElementReferenceCapture(9, elementReference => _spacerAfter = elementReference);
253269

254270
builder.CloseElement();
255271
}
@@ -354,6 +370,7 @@ private async ValueTask RefreshDataCoreAsync(bool renderOnSuccess)
354370
{
355371
_refreshCts = new CancellationTokenSource();
356372
cancellationToken = _refreshCts.Token;
373+
_loading = true;
357374
}
358375

359376
var request = new ItemsProviderRequest(_itemsBefore, _visibleItemCapacity, cancellationToken);
@@ -368,6 +385,7 @@ private async ValueTask RefreshDataCoreAsync(bool renderOnSuccess)
368385
_itemCount = result.TotalItemCount;
369386
_loadedItems = result.Items;
370387
_loadedItemsStartIndex = request.StartIndex;
388+
_loading = false;
371389

372390
if (renderOnSuccess)
373391
{

src/Components/test/E2ETest/Tests/VirtualizationTest.cs

+45
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,51 @@ public void CanHandleDataSetShrinkingWithExistingOffsetAlreadyBeyondNewListEnd(s
489489
Browser.True(() => GetPeopleNames(container).Contains("Person 25"));
490490
}
491491

492+
[Fact]
493+
public void EmptyContentRendered_Sync()
494+
{
495+
Browser.MountTestComponent<VirtualizationComponent>();
496+
Browser.Exists(By.Id("no-data-sync"));
497+
}
498+
499+
[Fact]
500+
public void EmptyContentRendered_Async()
501+
{
502+
Browser.MountTestComponent<VirtualizationComponent>();
503+
var finishLoadingWithItemsButton = Browser.Exists(By.Id("finish-loading-button"));
504+
var finishLoadingWithoutItemsButton = Browser.Exists(By.Id("finish-loading-button-empty"));
505+
var refreshDataAsync = Browser.Exists(By.Id("refresh-data-async"));
506+
507+
// Check that no items or placeholders are visible.
508+
// No data fetches have happened so we don't know how many items there are.
509+
Browser.Equal(0, GetItemCount);
510+
Browser.Equal(0, GetPlaceholderCount);
511+
512+
// Check that <EmptyContent> is not shown while loading
513+
Browser.DoesNotExist(By.Id("no-data-async"));
514+
515+
// Load the initial set of items.
516+
finishLoadingWithItemsButton.Click();
517+
518+
// Check that <EmptyContent> is still not shown (because there are items loaded)
519+
Browser.DoesNotExist(By.Id("no-data-async"));
520+
521+
// Start loading
522+
refreshDataAsync.Click();
523+
524+
// Check that <EmptyContent> is not shown
525+
Browser.DoesNotExist(By.Id("no-data-async"));
526+
527+
// Simulate 0 items
528+
finishLoadingWithoutItemsButton.Click();
529+
530+
// Check that <EmptyContent> is shown
531+
Browser.Exists(By.Id("no-data-async"));
532+
533+
int GetItemCount() => Browser.FindElements(By.Id("async-item")).Count;
534+
int GetPlaceholderCount() => Browser.FindElements(By.Id("async-placeholder")).Count;
535+
}
536+
492537
private string[] GetPeopleNames(IWebElement container)
493538
{
494539
var peopleElements = container.FindElements(By.CssSelector(".person span"));

src/Components/test/testassets/BasicTestApp/VirtualizationComponent.razor

+26-4
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,40 @@
1414

1515
<p>
1616
Asynchronous:<br />
17-
<button id="finish-loading-button" @onclick="FinishLoadingAsync">Finish loading</button><br />
17+
<button id="finish-loading-button-empty" @onclick="() => FinishLoadingAsync(0)">Finish loading with total item count = 0</button><br />
18+
<button id="finish-loading-button" @onclick="() => FinishLoadingAsync(200)">Finish loading with total item count = 200</button><br />
19+
<button id="refresh-data-async" @onclick="() => asyncComponent.RefreshDataAsync()">Call RefreshDataAsync</button><br />
1820
Cancellation count: <span id="cancellation-count">@asyncCancellationCount</span><br />
1921
<div id="async-container" style="background-color: #eee; height: 500px; overflow-y: auto">
20-
<Virtualize ItemsProvider="GetItemsAsync" ItemSize="itemSize">
22+
<Virtualize @ref="asyncComponent" ItemsProvider="GetItemsAsync" ItemSize="itemSize">
2123
<ItemContent>
2224
<div @key="context" id="async-item" style="height: @(itemSize)px; background-color: rgb(@((context % 2) * 255), @((1-(context % 2)) * 255), 255);">Item @context</div>
2325
</ItemContent>
2426
<Placeholder>
2527
<div id="async-placeholder" style="height: @(context.Size)px; background-color: orange;">Loading item @context.Index...</div>
2628
</Placeholder>
29+
<EmptyContent>
30+
<p id="no-data-async">No data to show</p>
31+
</EmptyContent>
2732
</Virtualize>
2833
</div>
2934
</p>
3035

36+
<p>
37+
Empty Content:<br />
38+
<div id="empty-container" style="background-color: #eee; height: 100px; overflow-y: auto">
39+
<Virtualize Items="@emptyCollection" ItemSize="itemSize">
40+
<ItemContent>
41+
<div @key="context" style="height: @(itemSize)px; background-color: rgb(@((context % 2) * 255), @((1-(context % 2)) * 255), 255);">
42+
Item @context</div>
43+
</ItemContent>
44+
<EmptyContent>
45+
<p id="no-data-sync">No data to show</p>
46+
</EmptyContent>
47+
</Virtualize>
48+
</div>
49+
</p>
50+
3151
<p>
3252
Slightly incorrect item size:<br />
3353
<div id="incorrect-size-container" style="background-color: #eee; height: 500px; overflow-y: auto">
@@ -47,12 +67,13 @@
4767
@code {
4868
float itemSize = 100;
4969
ICollection<int> fixedItems = Enumerable.Range(0, 1000).ToList();
70+
ICollection<int> emptyCollection = Array.Empty<int>();
5071

5172
int asyncTotalItemCount = 200;
5273
int asyncCancellationCount = 0;
5374
TaskCompletionSource asyncTcs = new TaskCompletionSource();
5475

55-
HashSet<int> cachedItems = new HashSet<int>();
76+
Virtualize<int> asyncComponent;
5677

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

70-
void FinishLoadingAsync()
91+
void FinishLoadingAsync(int totalItemCount)
7192
{
93+
asyncTotalItemCount = totalItemCount;
7294
asyncTcs.SetResult();
7395
asyncTcs = new TaskCompletionSource();
7496
}

0 commit comments

Comments
 (0)