Support triggering rendering from OnAfterRenderAsync. Fixes #8435 (#8960)

This commit is contained in:
Steve Sanderson 2019-04-01 13:01:33 +01:00 committed by GitHub
parent 285110de91
commit 7f4dd27551
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 131 additions and 5 deletions

View File

@ -79,7 +79,7 @@ namespace Microsoft.AspNetCore.Components.Performance
[Benchmark(Description = "RenderTreeDiffBuilder: Input and validation on a single form field.", Baseline = true)]
public void ComputeDiff_SingleFormField()
{
builder.Clear();
builder.ClearStateForCurrentBatch();
var diff = RenderTreeDiffBuilder.ComputeDiff(renderer, builder, 0, original.GetFrames(), modified.GetFrames());
GC.KeepAlive(diff);
}

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// 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.Collections.Generic;
@ -30,11 +30,17 @@ namespace Microsoft.AspNetCore.Components.Rendering
// Scratch data structure for understanding attribute diffs.
public Dictionary<string, int> AttributeDiffSet { get; } = new Dictionary<string, int>();
public void Clear()
public void ClearStateForCurrentBatch()
{
// This method is used to reset the builder back to a default state so it can
// begin building the next batch. That means clearing all the tracked state, but
// *not* clearing ComponentRenderQueue because that may hold information about
// the next batch we want to build. We shouldn't ever need to clear
// ComponentRenderQueue explicitly, because it gets cleared as an aspect of
// processing the render queue.
EditsBuffer.Clear();
ReferenceFramesBuffer.Clear();
ComponentRenderQueue.Clear();
UpdatedComponentDiffs.Clear();
DisposedComponentIds.Clear();
DisposedEventHandlerIds.Clear();

View File

@ -447,9 +447,18 @@ namespace Microsoft.AspNetCore.Components.Rendering
finally
{
RemoveEventHandlerIds(_batchBuilder.DisposedEventHandlerIds.ToRange(), updateDisplayTask);
_batchBuilder.Clear();
_batchBuilder.ClearStateForCurrentBatch();
_isBatchInProgress = false;
}
// An OnAfterRenderAsync callback might have queued more work synchronously.
// Note: we do *not* re-render implicitly after the OnAfterRenderAsync-returned
// task (that would be an infinite loop). We only render after an explicit render
// request (e.g., StateHasChanged()).
if (_batchBuilder.ComponentRenderQueue.Count > 0)
{
ProcessRenderQueue();
}
}
private Task InvokeRenderCompletedCalls(ArrayRange<RenderTreeDiff> updatedComponents)

View File

@ -2507,6 +2507,31 @@ namespace Microsoft.AspNetCore.Components.Test
Assert.Equal(1, childComponents[2].OnAfterRenderCallCount); // Disposed
}
[Fact]
public void CanTriggerRenderingSynchronouslyFromInsideAfterRenderCallback()
{
// Arrange
AfterRenderCaptureComponent component = null;
component = new AfterRenderCaptureComponent
{
OnAfterRenderLogic = () =>
{
if (component.OnAfterRenderCallCount < 10)
{
component.TriggerRender();
}
}
};
var renderer = new TestRenderer();
renderer.AssignRootComponentId(component);
// Act
component.TriggerRender();
// Assert
Assert.Equal(10, component.OnAfterRenderCallCount);
}
[ConditionalFact]
[SkipOnHelix] // https://github.com/aspnet/AspNetCore/issues/7487
public async Task CanTriggerEventHandlerDisposedInEarlierPendingBatchAsync()
@ -3414,11 +3439,14 @@ namespace Microsoft.AspNetCore.Components.Test
private class AfterRenderCaptureComponent : AutoRenderComponent, IComponent, IHandleAfterRender
{
public Action OnAfterRenderLogic { get; set; }
public int OnAfterRenderCallCount { get; private set; }
public Task OnAfterRenderAsync()
{
OnAfterRenderCallCount++;
OnAfterRenderLogic?.Invoke();
return Task.CompletedTask;
}

View File

@ -39,6 +39,21 @@ namespace Microsoft.AspNetCore.Components.E2ETests.ServerExecutionTests
Browser.Equal("1", () => Browser.FindElement(By.Id("count")).Text);
}
[Fact]
public void CanUseJSInteropFromOnAfterRenderAsync()
{
Navigate("/prerendered/prerendered-interop");
// Prerendered output can't use JSInterop
Browser.Equal("No value yet", () => Browser.FindElement(By.Id("val-get-by-interop")).Text);
Browser.Equal(string.Empty, () => Browser.FindElement(By.Id("val-set-by-interop")).GetAttribute("value"));
// Once connected, we can
BeginInteractivity();
Browser.Equal("Hello from interop call", () => Browser.FindElement(By.Id("val-get-by-interop")).Text);
Browser.Equal("Hello from interop call", () => Browser.FindElement(By.Id("val-set-by-interop")).GetAttribute("value"));
}
private void BeginInteractivity()
{
Browser.FindElement(By.Id("load-boot-script")).Click();

View File

@ -590,6 +590,14 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
Browser.Equal("First Second Third Fourth Fifth", () => result.Text);
}
[Fact]
public void CanPerformInteropImmediatelyOnComponentInsertion()
{
var appElement = MountTestComponent<InteropOnInitializationComponent>();
Browser.Equal("Hello from interop call", () => appElement.FindElement(By.Id("val-get-by-interop")).Text);
Browser.Equal("Hello from interop call", () => appElement.FindElement(By.Id("val-set-by-interop")).GetAttribute("value"));
}
static IAlert SwitchToAlert(IWebDriver driver)
{
try

View File

@ -49,6 +49,7 @@
<option value="BasicTestApp.FormsTest.SimpleValidationComponent">Simple validation</option>
<option value="BasicTestApp.FormsTest.TypicalValidationComponent">Typical validation</option>
<option value="BasicTestApp.FormsTest.NotifyPropertyChangedValidationComponent">INotifyPropertyChanged validation</option>
<option value="BasicTestApp.InteropOnInitializationComponent">Interop on initialization</option>
</select>
@if (SelectedComponentType != null)

View File

@ -0,0 +1,52 @@
@page "/prerendered-interop"
@using Microsoft.AspNetCore.Components.Services
@using Microsoft.JSInterop
@inject IComponentContext ComponentContext
@inject IJSRuntime JSRuntime
<p>
This component shows it's possible to use JSInterop as part of the initialization
logic of a component, and have that be compatible with prerendering. It also shows
that it's possible to trigger a rendering update from inside OnAfterRenderAsync,
though it's the developer's own responsibility to avoid an infinite loop when
doing that.
</p>
<p>
Value get via JS interop call:
<strong id="val-get-by-interop">@(infoFromJs ?? "No value yet")</strong>
</p>
<p>
Value set via JS interop call:
<input id="val-set-by-interop" ref="@myElem" />
</p>
@functions {
string infoFromJs;
ElementRef myElem;
protected override async Task OnAfterRenderAsync()
{
// TEMPORARY: Currently we need this guard to avoid making the interop
// call during prerendering. Soon this will be unnecessary because we
// will change OnAfterRenderAsync not to run during the prerendering phase.
if (!ComponentContext.IsConnected)
{
return;
}
if (infoFromJs == null)
{
// We can only use the ElementRef in OnAfterRenderAsync (and not any
// earlier lifecycle method), because there is no JS element until
// the component has been rendered.
infoFromJs = await JSRuntime.InvokeAsync<string>(
"setElementValue", myElem, "Hello from interop call");
// Now we can re-render with the new state obtained from the interop call.
// Not an infinite loop, because we only call this when "infoFromJs == null"
StateHasChanged();
}
}
}

View File

@ -16,6 +16,7 @@
// Used by ElementRefComponent
function setElementValue(element, newValue) {
element.value = newValue;
return element.value;
}
(function () {

View File

@ -22,6 +22,12 @@
scriptElem.src = '_framework/components.server.js';
document.body.appendChild(scriptElem);
}
// Used by InteropOnInitializationComponent
function setElementValue(element, newValue) {
element.value = newValue;
return element.value;
}
</script>
</body>
</html>