Virtualize component: method to trigger data refresh (#26177)
* E2E test cases for Virtualize data refresh * Expose public RefreshDataAsync API * Optimize: don't instantiate CancellationTokenSource when it won't be used * For in-memory data, refresh automatically on each render cycle * Fix typo
This commit is contained in:
parent
c0bd50075a
commit
7140f7cae7
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
|
@ -97,6 +98,20 @@ namespace Microsoft.AspNetCore.Components.Web.Virtualization
|
|||
[Parameter]
|
||||
public int OverscanCount { get; set; } = 3;
|
||||
|
||||
/// <summary>
|
||||
/// Instructs the component to re-request data from its <see cref="ItemsProvider"/>.
|
||||
/// This is useful if external data may have changed. There is no need to call this
|
||||
/// when using <see cref="Items"/>.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="Task"/> representing the completion of the operation.</returns>
|
||||
public async Task RefreshDataAsync()
|
||||
{
|
||||
// We don't auto-render after this operation because in the typical use case, the
|
||||
// host component calls this from one of its lifecycle methods, and will naturally
|
||||
// re-render afterwards anyway. It's not desirable to re-render twice.
|
||||
await RefreshDataCoreAsync(renderOnSuccess: false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
|
|
@ -125,6 +140,14 @@ namespace Microsoft.AspNetCore.Components.Web.Virtualization
|
|||
else if (Items != null)
|
||||
{
|
||||
_itemsProvider = DefaultItemsProvider;
|
||||
|
||||
// When we have a fixed set of in-memory data, it doesn't cost anything to
|
||||
// re-query it on each cycle, so do that. This means the developer can add/remove
|
||||
// items in the collection and see the UI update without having to call RefreshDataAsync.
|
||||
var refreshTask = RefreshDataCoreAsync(renderOnSuccess: false);
|
||||
|
||||
// We know it's synchronous and has its own error handling
|
||||
Debug.Assert(refreshTask.IsCompletedSuccessfully);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -270,7 +293,7 @@ namespace Microsoft.AspNetCore.Components.Web.Virtualization
|
|||
{
|
||||
_itemsBefore = itemsBefore;
|
||||
_visibleItemCapacity = visibleItemCapacity;
|
||||
var refreshTask = RefreshDataAsync();
|
||||
var refreshTask = RefreshDataCoreAsync(renderOnSuccess: true);
|
||||
|
||||
if (!refreshTask.IsCompleted)
|
||||
{
|
||||
|
|
@ -279,12 +302,25 @@ namespace Microsoft.AspNetCore.Components.Web.Virtualization
|
|||
}
|
||||
}
|
||||
|
||||
private async Task RefreshDataAsync()
|
||||
private async ValueTask RefreshDataCoreAsync(bool renderOnSuccess)
|
||||
{
|
||||
_refreshCts?.Cancel();
|
||||
_refreshCts = new CancellationTokenSource();
|
||||
CancellationToken cancellationToken;
|
||||
|
||||
if (_itemsProvider == DefaultItemsProvider)
|
||||
{
|
||||
// If we're using the DefaultItemsProvider (because the developer supplied a fixed
|
||||
// Items collection) we know it will complete synchronously, and there's no point
|
||||
// instantiating a new CancellationTokenSource
|
||||
_refreshCts = null;
|
||||
cancellationToken = CancellationToken.None;
|
||||
}
|
||||
else
|
||||
{
|
||||
_refreshCts = new CancellationTokenSource();
|
||||
cancellationToken = _refreshCts.Token;
|
||||
}
|
||||
|
||||
var cancellationToken = _refreshCts.Token;
|
||||
var request = new ItemsProviderRequest(_itemsBefore, _visibleItemCapacity, cancellationToken);
|
||||
|
||||
try
|
||||
|
|
@ -298,7 +334,10 @@ namespace Microsoft.AspNetCore.Components.Web.Virtualization
|
|||
_loadedItems = result.Items;
|
||||
_loadedItemsStartIndex = request.StartIndex;
|
||||
|
||||
StateHasChanged();
|
||||
if (renderOnSuccess)
|
||||
{
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Linq;
|
||||
using BasicTestApp;
|
||||
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
|
||||
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
|
||||
|
|
@ -13,7 +14,7 @@ using Xunit.Abstractions;
|
|||
|
||||
namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
||||
{
|
||||
public class VirtualizationTest : E2ETest.Infrastructure.ServerTestBase<ToggleExecutionModeServerFixture<Program>>
|
||||
public class VirtualizationTest : ServerTestBase<ToggleExecutionModeServerFixture<Program>>
|
||||
{
|
||||
public VirtualizationTest(
|
||||
BrowserFixture browserFixture,
|
||||
|
|
@ -26,12 +27,12 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
protected override void InitializeAsyncCore()
|
||||
{
|
||||
Navigate(ServerPathBase, noReload: _serverFixture.ExecutionMode == ExecutionMode.Client);
|
||||
Browser.MountTestComponent<VirtualizationComponent>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AlwaysFillsVisibleCapacity_Sync()
|
||||
{
|
||||
Browser.MountTestComponent<VirtualizationComponent>();
|
||||
var topSpacer = Browser.FindElement(By.Id("sync-container")).FindElement(By.TagName("div"));
|
||||
var expectedInitialSpacerStyle = "height: 0px;";
|
||||
|
||||
|
|
@ -61,6 +62,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
[Fact]
|
||||
public void AlwaysFillsVisibleCapacity_Async()
|
||||
{
|
||||
Browser.MountTestComponent<VirtualizationComponent>();
|
||||
var finishLoadingButton = Browser.FindElement(By.Id("finish-loading-button"));
|
||||
|
||||
// Check that no items or placeholders are visible.
|
||||
|
|
@ -112,6 +114,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
[Fact]
|
||||
public void RerendersWhenItemSizeShrinks_Sync()
|
||||
{
|
||||
Browser.MountTestComponent<VirtualizationComponent>();
|
||||
int initialItemCount = 0;
|
||||
|
||||
// Wait until items have been rendered.
|
||||
|
|
@ -131,6 +134,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
[Fact]
|
||||
public void RerendersWhenItemSizeShrinks_Async()
|
||||
{
|
||||
Browser.MountTestComponent<VirtualizationComponent>();
|
||||
var finishLoadingButton = Browser.FindElement(By.Id("finish-loading-button"));
|
||||
|
||||
// Load the initial set of items.
|
||||
|
|
@ -165,6 +169,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
[Fact]
|
||||
public void CancelsOutdatedRefreshes_Async()
|
||||
{
|
||||
Browser.MountTestComponent<VirtualizationComponent>();
|
||||
var cancellationCount = Browser.FindElement(By.Id("cancellation-count"));
|
||||
var finishLoadingButton = Browser.FindElement(By.Id("finish-loading-button"));
|
||||
|
||||
|
|
@ -191,6 +196,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
[QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/24922")]
|
||||
public void CanUseViewportAsContainer()
|
||||
{
|
||||
Browser.MountTestComponent<VirtualizationComponent>();
|
||||
var expectedInitialSpacerStyle = "height: 0px;";
|
||||
var topSpacer = Browser.FindElement(By.Id("viewport-as-root")).FindElement(By.TagName("div"));
|
||||
|
||||
|
|
@ -204,5 +210,136 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
// Validate that the top spacer has expanded.
|
||||
Browser.NotEqual(expectedInitialSpacerStyle, () => topSpacer.GetAttribute("style"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanMutateDataInPlace_Sync()
|
||||
{
|
||||
Browser.MountTestComponent<VirtualizationDataChanges>();
|
||||
|
||||
// Initial data
|
||||
var container = Browser.FindElement(By.Id("using-items"));
|
||||
Browser.Collection(() => GetPeopleNames(container),
|
||||
name => Assert.Equal("Person 1", name),
|
||||
name => Assert.Equal("Person 2", name),
|
||||
name => Assert.Equal("Person 3", name));
|
||||
|
||||
// Mutate one of them
|
||||
var itemToMutate = container.FindElements(By.ClassName("person"))[1];
|
||||
itemToMutate.FindElement(By.TagName("button")).Click();
|
||||
|
||||
// See changes
|
||||
Browser.Collection(() => GetPeopleNames(container),
|
||||
name => Assert.Equal("Person 1", name),
|
||||
name => Assert.Equal("Person 2 MUTATED", name),
|
||||
name => Assert.Equal("Person 3", name));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanMutateDataInPlace_Async()
|
||||
{
|
||||
Browser.MountTestComponent<VirtualizationDataChanges>();
|
||||
|
||||
// Initial data
|
||||
var container = Browser.FindElement(By.Id("using-itemsprovider"));
|
||||
Browser.Collection(() => GetPeopleNames(container),
|
||||
name => Assert.Equal("Person 1", name),
|
||||
name => Assert.Equal("Person 2", name),
|
||||
name => Assert.Equal("Person 3", name));
|
||||
|
||||
// Mutate one of them
|
||||
var itemToMutate = container.FindElements(By.ClassName("person"))[1];
|
||||
itemToMutate.FindElement(By.TagName("button")).Click();
|
||||
|
||||
// See changes
|
||||
Browser.Collection(() => GetPeopleNames(container),
|
||||
name => Assert.Equal("Person 1", name),
|
||||
name => Assert.Equal("Person 2 MUTATED", name),
|
||||
name => Assert.Equal("Person 3", name));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanChangeDataCount_Sync()
|
||||
{
|
||||
Browser.MountTestComponent<VirtualizationDataChanges>();
|
||||
|
||||
// Initial data
|
||||
var container = Browser.FindElement(By.Id("using-items"));
|
||||
Browser.Collection(() => GetPeopleNames(container),
|
||||
name => Assert.Equal("Person 1", name),
|
||||
name => Assert.Equal("Person 2", name),
|
||||
name => Assert.Equal("Person 3", name));
|
||||
|
||||
// Add another item
|
||||
Browser.FindElement(By.Id("add-person-to-fixed-list")).Click();
|
||||
|
||||
// See changes
|
||||
Browser.Collection(() => GetPeopleNames(container),
|
||||
name => Assert.Equal("Person 1", name),
|
||||
name => Assert.Equal("Person 2", name),
|
||||
name => Assert.Equal("Person 3", name),
|
||||
name => Assert.Equal("Person 4", name));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanChangeDataCount_Async()
|
||||
{
|
||||
Browser.MountTestComponent<VirtualizationDataChanges>();
|
||||
|
||||
// Initial data
|
||||
var container = Browser.FindElement(By.Id("using-itemsprovider"));
|
||||
Browser.Collection(() => GetPeopleNames(container),
|
||||
name => Assert.Equal("Person 1", name),
|
||||
name => Assert.Equal("Person 2", name),
|
||||
name => Assert.Equal("Person 3", name));
|
||||
|
||||
// Add another item
|
||||
Browser.FindElement(By.Id("add-person-to-itemsprovider")).Click();
|
||||
|
||||
// Initially this has no effect because we don't re-query the provider until told to do so
|
||||
Browser.Collection(() => GetPeopleNames(container),
|
||||
name => Assert.Equal("Person 1", name),
|
||||
name => Assert.Equal("Person 2", name),
|
||||
name => Assert.Equal("Person 3", name));
|
||||
|
||||
// Request refresh
|
||||
Browser.FindElement(By.Id("refresh-itemsprovider")).Click();
|
||||
|
||||
// See changes
|
||||
Browser.Collection(() => GetPeopleNames(container),
|
||||
name => Assert.Equal("Person 1", name),
|
||||
name => Assert.Equal("Person 2", name),
|
||||
name => Assert.Equal("Person 3", name),
|
||||
name => Assert.Equal("Person 4", name));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanRefreshItemsProviderResultsInPlace()
|
||||
{
|
||||
Browser.MountTestComponent<VirtualizationDataChanges>();
|
||||
|
||||
// Mutate the data
|
||||
var container = Browser.FindElement(By.Id("using-itemsprovider"));
|
||||
var itemToMutate = container.FindElements(By.ClassName("person"))[1];
|
||||
itemToMutate.FindElement(By.TagName("button")).Click();
|
||||
|
||||
// Verify the mutation was applied
|
||||
Browser.Collection(() => GetPeopleNames(container),
|
||||
name => Assert.Equal("Person 1", name),
|
||||
name => Assert.Equal("Person 2 MUTATED", name),
|
||||
name => Assert.Equal("Person 3", name));
|
||||
|
||||
// Refresh and verify the mutation was reverted
|
||||
Browser.FindElement(By.Id("refresh-itemsprovider")).Click();
|
||||
Browser.Collection(() => GetPeopleNames(container),
|
||||
name => Assert.Equal("Person 1", name),
|
||||
name => Assert.Equal("Person 2", name),
|
||||
name => Assert.Equal("Person 3", name));
|
||||
}
|
||||
|
||||
private string[] GetPeopleNames(IWebElement container)
|
||||
{
|
||||
var peopleElements = container.FindElements(By.CssSelector(".person span"));
|
||||
return peopleElements.Select(element => element.Text).ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
<option value="BasicTestApp.ComponentRefComponent">Component ref component</option>
|
||||
<option value="BasicTestApp.ConcurrentRenderParent">Concurrent rendering</option>
|
||||
<option value="BasicTestApp.ConfigurationComponent">Configuration</option>
|
||||
<option value="BasicTestApp.ContentEditable">Content-editable</option>
|
||||
<option value="BasicTestApp.CounterComponent">Counter</option>
|
||||
<option value="BasicTestApp.CounterComponentUsingChild">Counter using child component</option>
|
||||
<option value="BasicTestApp.CounterComponentWrapper">Counter wrapped in parent</option>
|
||||
|
|
@ -80,16 +81,16 @@
|
|||
<option value="BasicTestApp.RouterTest.TestRouterWithOnNavigate">Router with OnNavigate</option>
|
||||
<option value="BasicTestApp.RouterTest.TestRouterWithLazyAssembly">Router with dynamic assembly</option>
|
||||
<option value="BasicTestApp.RouterTest.TestRouterWithAdditionalAssembly">Router with additional assembly</option>
|
||||
<option value="BasicTestApp.SelectVariantsComponent">Select with component options</option>
|
||||
<option value="BasicTestApp.SignalRClientComponent">SignalR client</option>
|
||||
<option value="BasicTestApp.StringComparisonComponent">StringComparison</option>
|
||||
<option value="BasicTestApp.SvgComponent">SVG</option>
|
||||
<option value="BasicTestApp.SvgWithChildComponent">SVG with child component</option>
|
||||
<option value="BasicTestApp.TextOnlyComponent">Plain text</option>
|
||||
<option value="BasicTestApp.ToggleEventComponent">Toggle Event</option>
|
||||
<option value="BasicTestApp.TouchEventComponent">Touch events</option>
|
||||
<option value="BasicTestApp.VirtualizationComponent">Virtualization</option>
|
||||
<option value="BasicTestApp.SelectVariantsComponent">Select with component options</option>
|
||||
<option value="BasicTestApp.ToggleEventComponent">Toggle Event</option>
|
||||
<option value="BasicTestApp.ContentEditable">Content-editable</option>
|
||||
<option value="BasicTestApp.VirtualizationDataChanges">Virtualization data changes</option>
|
||||
</select>
|
||||
|
||||
<span id="runtime-info"><code><tt>@System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription</tt></code></span>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,84 @@
|
|||
<h3 id="virtualization-data-changes">Virtualization data changes</h3>
|
||||
|
||||
<p>This scenario shows how the data behind a Virtualize component can change.</p>
|
||||
|
||||
<button id="add-person-to-fixed-list" @onclick="AddPersonToFixedList">Add person</button>
|
||||
|
||||
<h4>Using Items parameter</h4>
|
||||
|
||||
<div id="using-items" style="overflow-y: auto; height: 200px; border: 1px dashed gray;">
|
||||
<Virtualize Items="@fixedPeople" Context="person">
|
||||
@*
|
||||
Note that for best performance, you really should use @key on the top-level elements within the <Virtualize>.
|
||||
This test case doesn't do so only as a way of verifying it's not strictly required (though it is recommended).
|
||||
*@
|
||||
<div class="person" style="border-bottom: 1px dashed red; display: flex; justify-content: space-between; padding: 4px;">
|
||||
<span>@person.Name</span>
|
||||
<button @onclick="@person.Mutate">Mutate</button>
|
||||
</div>
|
||||
</Virtualize>
|
||||
</div>
|
||||
|
||||
<h4>Using ItemsProvider parameter</h4>
|
||||
<p>
|
||||
<button id="add-person-to-itemsprovider" @onclick="AddPersonToItemsProvider">Add person</button>
|
||||
<button id="refresh-itemsprovider" @onclick="() => asyncVirtualize.RefreshDataAsync()">Refresh</button>
|
||||
</p>
|
||||
|
||||
<div id="using-itemsprovider" style="overflow-y: auto; height: 200px; border: 1px dashed gray;">
|
||||
<Virtualize @ref="asyncVirtualize" ItemsProvider="GetPeopleAsync" Context="person">
|
||||
@*
|
||||
Note that for best performance, you really should use @key on the top-level elements within the <Virtualize>.
|
||||
This test case doesn't do so only as a way of verifying it's not strictly required (though it is recommended).
|
||||
*@
|
||||
<div class="person" style="border-bottom: 1px dashed red; display: flex; justify-content: space-between; padding: 4px;">
|
||||
<span>@person.Name</span>
|
||||
<button @onclick="@person.Mutate">Mutate</button>
|
||||
</div>
|
||||
</Virtualize>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
Virtualize<Person> asyncVirtualize;
|
||||
List<Person> fixedPeople = Enumerable.Range(1, 3).Select(GeneratePerson).ToList();
|
||||
int numPeopleInItemsProvider = 3;
|
||||
|
||||
void AddPersonToFixedList()
|
||||
{
|
||||
// When using Items (not ItemsProvider), the Virtualize component re-queries
|
||||
// the data on every refresh cycle without requiring you to call RefreshDataAsync.
|
||||
// This is because there's no cost involved in doing so. Thus, the following
|
||||
// line is enough to make the UI change on its own.
|
||||
fixedPeople.Add(GeneratePerson(fixedPeople.Count + 1));
|
||||
}
|
||||
|
||||
void AddPersonToItemsProvider()
|
||||
{
|
||||
// On its own, this isn't going to make the UI change, because it doesn't know
|
||||
// to re-query the underlying items provider until you call RefreshDataAsync.
|
||||
numPeopleInItemsProvider++;
|
||||
}
|
||||
|
||||
async ValueTask<ItemsProviderResult<Person>> GetPeopleAsync(ItemsProviderRequest request)
|
||||
{
|
||||
await Task.Delay(500);
|
||||
|
||||
var lastIndexExcl = Math.Min(request.StartIndex + request.Count, numPeopleInItemsProvider);
|
||||
return new ItemsProviderResult<Person>(
|
||||
Enumerable.Range(1 + request.StartIndex, lastIndexExcl - request.StartIndex).Select(GeneratePerson).ToList(),
|
||||
numPeopleInItemsProvider);
|
||||
}
|
||||
|
||||
class Person
|
||||
{
|
||||
public string Name { get; set; }
|
||||
|
||||
public void Mutate()
|
||||
{
|
||||
Name += " MUTATED";
|
||||
}
|
||||
}
|
||||
|
||||
static Person GeneratePerson(int index)
|
||||
=> new Person { Name = $"Person {index}" };
|
||||
}
|
||||
Loading…
Reference in New Issue