Skip to content

Commit 8d16b79

Browse files
committed
Started on asynchronous data fetching
1 parent 0d90cbb commit 8d16b79

File tree

4 files changed

+148
-25
lines changed

4 files changed

+148
-25
lines changed

src/Components/Components/src/Virtualization/VirtualizeBase.cs

Lines changed: 88 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,100 @@
11
using System;
2+
using System.Collections.Concurrent;
23
using System.Collections.Generic;
34
using System.Linq;
5+
using System.Threading;
6+
using System.Threading.Tasks;
47
using Microsoft.AspNetCore.Components.Rendering;
58

69
namespace Microsoft.AspNetCore.Components.Virtualization
710
{
811
public abstract class VirtualizeBase<TItem> : ComponentBase
912
{
13+
private readonly ConcurrentQueue<TItem> _loadedItems = new ConcurrentQueue<TItem>();
14+
15+
private readonly SemaphoreSlim _fetchSemaphore = new SemaphoreSlim(1);
16+
1017
private int _itemsAbove;
1118

1219
private int _itemsVisible;
1320

1421
private int _itemsBelow;
1522

23+
private IEnumerable<TItem> LoadedItems => Items ?? (IEnumerable<TItem>)_loadedItems;
24+
25+
private int ItemCount => Items?.Count ?? _loadedItems.Count;
26+
1627
protected ElementReference TopSpacer { get; private set; }
1728

1829
protected ElementReference BottomSpacer { get; private set; }
1930

2031
[Parameter]
2132
public ICollection<TItem> Items { get; set; } = default!;
2233

34+
[Parameter]
35+
public Func<Range, Task<IEnumerable<TItem>>> ItemsProvider { get; set; } = default!;
36+
2337
[Parameter]
2438
public float ItemSize { get; set; }
2539

40+
[Parameter]
41+
public int InitialItemsCount { get; set; }
42+
2643
[Parameter]
2744
public RenderFragment<TItem>? ChildContent { get; set; }
2845

2946
protected override void OnParametersSet()
3047
{
31-
if (Items == null)
48+
if (ItemSize <= 0f)
3249
{
3350
throw new InvalidOperationException(
34-
$"Parameter '{nameof(Items)}' must be specified and non-null.");
51+
$"Parameter '{nameof(ItemSize)}' must be specified and greater than zero.");
3552
}
3653

37-
if (ItemSize <= 0f)
54+
if (Items != null)
55+
{
56+
if (ItemsProvider != null)
57+
{
58+
throw new InvalidOperationException(
59+
$"{GetType()} cannot have both '{nameof(Items)}' and '{nameof(ItemsProvider)}' parameters.");
60+
}
61+
62+
_itemsBelow = Items.Count;
63+
}
64+
else if (ItemsProvider != null)
65+
{
66+
_itemsBelow = 0;
67+
}
68+
else
3869
{
3970
throw new InvalidOperationException(
40-
$"Parameter '{nameof(ItemSize)}' must be specified and greater than zero.");
71+
$"{GetType()} requires either the '{nameof(Items)}' or '{nameof(ItemsProvider)}' parameter to " +
72+
$"be specified and non-null.");
4173
}
42-
43-
_itemsBelow = Items.Count;
4474
}
4575

4676
protected void UpdateTopSpacer(float spacerSize, float containerSize)
47-
=> CalculateSpacerItemDistribution(spacerSize, containerSize, out _itemsAbove, out _itemsBelow);
77+
{
78+
CalculateSpacerItemDistribution(spacerSize, containerSize, out _itemsAbove, out _itemsBelow);
79+
Console.WriteLine($"Above: {_itemsAbove}, Visible: {_itemsVisible}");
80+
}
4881

4982
protected void UpdateBottomSpacer(float spacerSize, float containerSize)
50-
=> CalculateSpacerItemDistribution(spacerSize, containerSize, out _itemsBelow, out _itemsAbove);
83+
{
84+
CalculateSpacerItemDistribution(spacerSize, containerSize, out _itemsBelow, out _itemsAbove);
85+
Console.WriteLine($"Above: {_itemsAbove}, Visible: {_itemsVisible}");
86+
87+
if (ItemsProvider != null && _itemsAbove + _itemsVisible >= _loadedItems.Count)
88+
{
89+
FetchItems(_itemsAbove + _itemsVisible + InitialItemsCount);
90+
}
91+
}
5192

5293
private void CalculateSpacerItemDistribution(float spacerSize, float containerSize, out int itemsInThisSpacer, out int itemsInOtherSpacer)
5394
{
54-
_itemsVisible = (int)(containerSize / ItemSize) + 1; // TODO: Custom number of "padding" elements?
55-
itemsInThisSpacer = (int)(spacerSize / ItemSize);
56-
itemsInOtherSpacer = Items.Count - itemsInThisSpacer - _itemsVisible;
95+
_itemsVisible = Math.Max(0, (int)Math.Ceiling(containerSize / ItemSize) + 2);
96+
itemsInThisSpacer = Math.Max(0, (int)Math.Floor(spacerSize / ItemSize) - 1);
97+
itemsInOtherSpacer = Math.Max(0, ItemCount - itemsInThisSpacer - _itemsVisible);
5798

5899
StateHasChanged();
59100
}
@@ -63,30 +104,58 @@ private string GetSpacerStyle(int itemsInSpacer)
63104
return $"height: {itemsInSpacer * ItemSize}px;";
64105
}
65106

107+
private void FetchItems(int newItemCount)
108+
{
109+
var currentScheduler = TaskScheduler.FromCurrentSynchronizationContext();
110+
111+
_fetchSemaphore.WaitAsync().ContinueWith(t =>
112+
{
113+
if (_loadedItems.Count >= newItemCount)
114+
{
115+
_fetchSemaphore.Release();
116+
return;
117+
}
118+
119+
ItemsProvider(_loadedItems.Count..newItemCount).ContinueWith(t =>
120+
{
121+
foreach (var item in t.Result)
122+
{
123+
_loadedItems.Enqueue(item);
124+
}
125+
126+
StateHasChanged();
127+
128+
_fetchSemaphore.Release();
129+
}, currentScheduler);
130+
});
131+
}
132+
66133
protected override void BuildRenderTree(RenderTreeBuilder builder)
67134
{
135+
_itemsBelow = Math.Max(1, ItemCount - (_itemsVisible + _itemsAbove));
136+
68137
builder.OpenElement(0, "div");
69138
builder.AddAttribute(1, "key", "top-spacer");
70139
builder.AddAttribute(2, "style", GetSpacerStyle(_itemsAbove));
71140
builder.AddElementReferenceCapture(3, elementReference => TopSpacer = elementReference);
72141
builder.CloseElement();
73142

143+
builder.OpenRegion(4);
144+
74145
if (ChildContent != null)
75146
{
76-
builder.AddContent(4, new RenderFragment(builder =>
147+
foreach (var item in LoadedItems.Skip(_itemsAbove).Take(_itemsVisible))
77148
{
78-
foreach (var item in Items.Skip(_itemsAbove).Take(_itemsVisible))
79-
{
80-
ChildContent(item)?.Invoke(builder);
81-
}
82-
}));
149+
ChildContent(item)?.Invoke(builder);
150+
}
83151
}
84152

153+
builder.CloseRegion();
154+
85155
builder.OpenElement(5, "div");
86156
builder.AddAttribute(6, "key", "bottom-spacer");
87157
builder.AddAttribute(7, "style", GetSpacerStyle(_itemsBelow));
88-
builder.AddElementReferenceCapture(8, elementReference =>
89-
BottomSpacer = elementReference);
158+
builder.AddElementReferenceCapture(8, elementReference => BottomSpacer = elementReference);
90159
builder.CloseElement();
91160
}
92161
}

src/Components/Web.JS/src/Virtualize.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
const observersByDotNetId = {};
2+
13
function findClosestScrollContainer(element: Element | null): Element | null {
24
if (!element) {
35
return null;
@@ -30,6 +32,11 @@ function init(component: any, topSpacer: Element, bottomSpacer: Element, rootMar
3032

3133
mutationObserver.observe(topSpacer, { attributes: true });
3234

35+
observersByDotNetId[component._id] = {
36+
intersection: intersectionObserver,
37+
mutation: mutationObserver,
38+
};
39+
3340
function intersectionCallback(entries: IntersectionObserverEntry[]): void {
3441
entries.forEach((entry): void => {
3542
if (!entry.isIntersecting) {
@@ -49,6 +56,20 @@ function init(component: any, topSpacer: Element, bottomSpacer: Element, rootMar
4956
}
5057
}
5158

59+
function dispose(component: any): void {
60+
const observers = observersByDotNetId[component._id];
61+
62+
if (observers) {
63+
observers.intersection.disconnect();
64+
observers.mutation.disconnect();
65+
66+
component.dispose();
67+
68+
delete observersByDotNetId[component.id];
69+
}
70+
}
71+
5272
export const Virtualize = {
5373
init,
74+
dispose,
5475
};

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,10 @@ public void OnBottomSpacerVisible(float spacerSize, float containerSize)
3434

3535
public void Dispose()
3636
{
37-
_selfReference?.Dispose();
37+
if (_selfReference != null)
38+
{
39+
_ = JSRuntime.InvokeVoidAsync("Blazor._internal.Virtualize.dispose", _selfReference);
40+
}
3841
}
3942
}
4043
}
Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,49 @@
11
@using Microsoft.AspNetCore.Components.Virtualization
22

3-
<input type="number" @bind-value="itemSize" />
3+
<p>
4+
Item size:<br />
5+
<input type="number" @bind-value="itemSize" />
6+
</p>
47

5-
<div style="background-color: #eee; height: 500px; overflow-y: auto">
8+
<p>
9+
Scrollable div:<br />
10+
<div style="background-color: #eee; height: 500px; overflow-y: auto">
11+
<Virtualize Items="@Items" ItemSize="itemSize">
12+
<div @key="@context" style="height: @(itemSize)px; background-color: rgb(@((context % 2) * 255), @((1-(context % 2)) * 255), 255);">Item @context</div>
13+
</Virtualize>
14+
<Virtualize Items="@Items" ItemSize="itemSize">
15+
<div @key="@context" style="height: @(itemSize)px; background-color: rgb(0, @((context % 2) * 255), @((1-(context % 2)) * 255));">Item @context</div>
16+
</Virtualize>
17+
</div>
18+
</p>
19+
20+
<p>
21+
Async items:<br />
22+
<div style="background-color: #eee; height: 500px; overflow-y: auto">
23+
<Virtualize ItemsProvider="GetItems" InitialItemsCount="100" ItemSize="itemSize">
24+
<div @key="@context" style="height: @(itemSize)px; background-color: rgb(@((context % 2) * 255), @((1-(context % 2)) * 255), 255);">Item @context</div>
25+
</Virtualize>
26+
</div>
27+
</p>
28+
29+
<p>
30+
Viewport as root:<br />
631
<Virtualize Items="@Items" ItemSize="itemSize">
732
<div @key="@context" style="height: @(itemSize)px; background-color: rgb(@((context % 2) * 255), @((1-(context % 2)) * 255), 255);">Item @context</div>
833
</Virtualize>
934
<Virtualize Items="@Items" ItemSize="itemSize">
1035
<div @key="@context" style="height: @(itemSize)px; background-color: rgb(0, @((context % 2) * 255), @((1-(context % 2)) * 255));">Item @context</div>
1136
</Virtualize>
12-
</div>
13-
37+
</p>
1438

1539
@code {
16-
int itemSize = 100;
40+
float itemSize = 100;
1741
Random rng = new Random();
1842
ICollection<int> Items = Enumerable.Range(1, 1000).ToList();
43+
44+
Task<IEnumerable<int>> GetItems(Range range)
45+
{
46+
return Task.Delay(500).ContinueWith<IEnumerable<int>>(_ =>
47+
Enumerable.Range(range.Start.Value, range.End.Value - range.Start.Value));
48+
}
1949
}

0 commit comments

Comments
 (0)