This commit is contained in:
parent
285110de91
commit
7f4dd27551
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@
|
|||
// Used by ElementRefComponent
|
||||
function setElementValue(element, newValue) {
|
||||
element.value = newValue;
|
||||
return element.value;
|
||||
}
|
||||
|
||||
(function () {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue