Fix components event dispatch ordering + E2E test fixes (#10112)
This commit is contained in:
parent
dcf49f2575
commit
4db8260c6c
|
|
@ -61,6 +61,7 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
|
|||
public WebAssemblyRenderer(System.IServiceProvider serviceProvider) : base (default(System.IServiceProvider)) { }
|
||||
public System.Threading.Tasks.Task AddComponentAsync(System.Type componentType, string domElementSelector) { throw null; }
|
||||
public System.Threading.Tasks.Task AddComponentAsync<TComponent>(string domElementSelector) where TComponent : Microsoft.AspNetCore.Components.IComponent { throw null; }
|
||||
public override System.Threading.Tasks.Task DispatchEventAsync(int eventHandlerId, Microsoft.AspNetCore.Components.UIEventArgs eventArgs) { throw null; }
|
||||
protected override void Dispose(bool disposing) { }
|
||||
protected override void HandleException(System.Exception exception) { }
|
||||
protected override System.Threading.Tasks.Task UpdateDisplayAsync(in Microsoft.AspNetCore.Components.Rendering.RenderBatch batch) { throw null; }
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Blazor.Services;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
|
@ -18,6 +19,9 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
|
|||
{
|
||||
private readonly int _webAssemblyRendererId;
|
||||
|
||||
private bool isDispatchingEvent;
|
||||
private Queue<IncomingEventInfo> deferredIncomingEvents = new Queue<IncomingEventInfo>();
|
||||
|
||||
/// <summary>
|
||||
/// Constructs an instance of <see cref="WebAssemblyRenderer"/>.
|
||||
/// </summary>
|
||||
|
|
@ -110,5 +114,78 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
|
|||
Console.Error.WriteLine(exception);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task DispatchEventAsync(int eventHandlerId, UIEventArgs eventArgs)
|
||||
{
|
||||
// Be sure we only run one event handler at once. Although they couldn't run
|
||||
// simultaneously anyway (there's only one thread), they could run nested on
|
||||
// the stack if somehow one event handler triggers another event synchronously.
|
||||
// We need event handlers not to overlap because (a) that's consistent with
|
||||
// server-side Blazor which uses a sync context, and (b) the rendering logic
|
||||
// relies completely on the idea that within a given scope it's only building
|
||||
// or processing one batch at a time.
|
||||
//
|
||||
// The only currently known case where this makes a difference is in the E2E
|
||||
// tests in ReorderingFocusComponent, where we hit what seems like a Chrome bug
|
||||
// where mutating the DOM cause an element's "change" to fire while its "input"
|
||||
// handler is still running (i.e., nested on the stack) -- this doesn't happen
|
||||
// in Firefox. Possibly a future version of Chrome may fix this, but even then,
|
||||
// it's conceivable that DOM mutation events could trigger this too.
|
||||
|
||||
if (isDispatchingEvent)
|
||||
{
|
||||
var info = new IncomingEventInfo(eventHandlerId, eventArgs);
|
||||
deferredIncomingEvents.Enqueue(info);
|
||||
return info.TaskCompletionSource.Task;
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
isDispatchingEvent = true;
|
||||
return base.DispatchEventAsync(eventHandlerId, eventArgs);
|
||||
}
|
||||
finally
|
||||
{
|
||||
isDispatchingEvent = false;
|
||||
|
||||
if (deferredIncomingEvents.Count > 0)
|
||||
{
|
||||
ProcessNextDeferredEvent();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async void ProcessNextDeferredEvent()
|
||||
{
|
||||
var info = deferredIncomingEvents.Dequeue();
|
||||
var taskCompletionSource = info.TaskCompletionSource;
|
||||
|
||||
try
|
||||
{
|
||||
await DispatchEventAsync(info.EventHandlerId, info.EventArgs);
|
||||
taskCompletionSource.SetResult(null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
taskCompletionSource.SetException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
readonly struct IncomingEventInfo
|
||||
{
|
||||
public readonly int EventHandlerId;
|
||||
public readonly UIEventArgs EventArgs;
|
||||
public readonly TaskCompletionSource<object> TaskCompletionSource;
|
||||
|
||||
public IncomingEventInfo(int eventHandlerId, UIEventArgs eventArgs)
|
||||
{
|
||||
EventHandlerId = eventHandlerId;
|
||||
EventArgs = eventArgs;
|
||||
TaskCompletionSource = new TaskCompletionSource<object>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components.Rendering;
|
||||
using Microsoft.JSInterop;
|
||||
|
|
@ -14,10 +13,6 @@ namespace Microsoft.AspNetCore.Components.Browser
|
|||
/// </summary>
|
||||
public static class RendererRegistryEventDispatcher
|
||||
{
|
||||
private static bool isDispatchingEvent;
|
||||
private static Queue<IncomingEventInfo> deferredIncomingEvents
|
||||
= new Queue<IncomingEventInfo>();
|
||||
|
||||
/// <summary>
|
||||
/// For framework use only.
|
||||
/// </summary>
|
||||
|
|
@ -25,62 +20,9 @@ namespace Microsoft.AspNetCore.Components.Browser
|
|||
public static Task DispatchEvent(
|
||||
BrowserEventDescriptor eventDescriptor, string eventArgsJson)
|
||||
{
|
||||
// Be sure we only run one event handler at once. Although they couldn't run
|
||||
// simultaneously anyway (there's only one thread), they could run nested on
|
||||
// the stack if somehow one event handler triggers another event synchronously.
|
||||
// We need event handlers not to overlap because (a) that's consistent with
|
||||
// server-side Blazor which uses a sync context, and (b) the rendering logic
|
||||
// relies completely on the idea that within a given scope it's only building
|
||||
// or processing one batch at a time.
|
||||
//
|
||||
// The only currently known case where this makes a difference is in the E2E
|
||||
// tests in ReorderingFocusComponent, where we hit what seems like a Chrome bug
|
||||
// where mutating the DOM cause an element's "change" to fire while its "input"
|
||||
// handler is still running (i.e., nested on the stack) -- this doesn't happen
|
||||
// in Firefox. Possibly a future version of Chrome may fix this, but even then,
|
||||
// it's conceivable that DOM mutation events could trigger this too.
|
||||
|
||||
if (isDispatchingEvent)
|
||||
{
|
||||
var info = new IncomingEventInfo(eventDescriptor, eventArgsJson);
|
||||
deferredIncomingEvents.Enqueue(info);
|
||||
return info.TaskCompletionSource.Task;
|
||||
}
|
||||
else
|
||||
{
|
||||
isDispatchingEvent = true;
|
||||
try
|
||||
{
|
||||
var eventArgs = ParseEventArgsJson(eventDescriptor.EventArgsType, eventArgsJson);
|
||||
var renderer = RendererRegistry.Current.Find(eventDescriptor.BrowserRendererId);
|
||||
return renderer.DispatchEventAsync(eventDescriptor.EventHandlerId, eventArgs);
|
||||
}
|
||||
finally
|
||||
{
|
||||
isDispatchingEvent = false;
|
||||
if (deferredIncomingEvents.Count > 0)
|
||||
{
|
||||
ProcessNextDeferredEvent();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ProcessNextDeferredEvent()
|
||||
{
|
||||
var info = deferredIncomingEvents.Dequeue();
|
||||
var task = DispatchEvent(info.EventDescriptor, info.EventArgsJson);
|
||||
task.ContinueWith(_ =>
|
||||
{
|
||||
if (task.Exception != null)
|
||||
{
|
||||
info.TaskCompletionSource.SetException(task.Exception);
|
||||
}
|
||||
else
|
||||
{
|
||||
info.TaskCompletionSource.SetResult(null);
|
||||
}
|
||||
});
|
||||
var eventArgs = ParseEventArgsJson(eventDescriptor.EventArgsType, eventArgsJson);
|
||||
var renderer = RendererRegistry.Current.Find(eventDescriptor.BrowserRendererId);
|
||||
return renderer.DispatchEventAsync(eventDescriptor.EventHandlerId, eventArgs);
|
||||
}
|
||||
|
||||
private static UIEventArgs ParseEventArgsJson(string eventArgsType, string eventArgsJson)
|
||||
|
|
@ -136,19 +78,5 @@ namespace Microsoft.AspNetCore.Components.Browser
|
|||
/// </summary>
|
||||
public string EventArgsType { get; set; }
|
||||
}
|
||||
|
||||
readonly struct IncomingEventInfo
|
||||
{
|
||||
public readonly BrowserEventDescriptor EventDescriptor;
|
||||
public readonly string EventArgsJson;
|
||||
public readonly TaskCompletionSource<object> TaskCompletionSource;
|
||||
|
||||
public IncomingEventInfo(BrowserEventDescriptor eventDescriptor, string eventArgsJson)
|
||||
{
|
||||
EventDescriptor = eventDescriptor;
|
||||
EventArgsJson = eventArgsJson;
|
||||
TaskCompletionSource = new TaskCompletionSource<object>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -710,7 +710,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
protected internal virtual void AddToRenderQueue(int componentId, Microsoft.AspNetCore.Components.RenderFragment renderFragment) { }
|
||||
protected internal int AssignRootComponentId(Microsoft.AspNetCore.Components.IComponent component) { throw null; }
|
||||
public static Microsoft.AspNetCore.Components.Rendering.IDispatcher CreateDefaultDispatcher() { throw null; }
|
||||
public System.Threading.Tasks.Task DispatchEventAsync(int eventHandlerId, Microsoft.AspNetCore.Components.UIEventArgs eventArgs) { throw null; }
|
||||
public virtual System.Threading.Tasks.Task DispatchEventAsync(int eventHandlerId, Microsoft.AspNetCore.Components.UIEventArgs eventArgs) { throw null; }
|
||||
public void Dispose() { }
|
||||
protected virtual void Dispose(bool disposing) { }
|
||||
protected abstract void HandleException(System.Exception exception);
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ namespace Microsoft.AspNetCore.Components.Forms
|
|||
static bool TryParseFloat(string value, out T result)
|
||||
{
|
||||
var success = float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsedValue);
|
||||
if (success)
|
||||
if (success && !float.IsInfinity(parsedValue))
|
||||
{
|
||||
result = (T)(object)parsedValue;
|
||||
return true;
|
||||
|
|
@ -134,7 +134,7 @@ namespace Microsoft.AspNetCore.Components.Forms
|
|||
static bool TryParseDouble(string value, out T result)
|
||||
{
|
||||
var success = double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsedValue);
|
||||
if (success)
|
||||
if (success && !double.IsInfinity(parsedValue))
|
||||
{
|
||||
result = (T)(object)parsedValue;
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -209,7 +209,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
/// A <see cref="Task"/> which will complete once all asynchronous processing related to the event
|
||||
/// has completed.
|
||||
/// </returns>
|
||||
public Task DispatchEventAsync(int eventHandlerId, UIEventArgs eventArgs)
|
||||
public virtual Task DispatchEventAsync(int eventHandlerId, UIEventArgs eventArgs)
|
||||
{
|
||||
EnsureSynchronizationContext();
|
||||
|
||||
|
|
|
|||
|
|
@ -57,4 +57,28 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class ServerEventCallbackTest : EventCallbackTest
|
||||
{
|
||||
public ServerEventCallbackTest(BrowserFixture browserFixture, ToggleExecutionModeServerFixture<Program> serverFixture, ITestOutputHelper output)
|
||||
: base(browserFixture, serverFixture.WithServerExecution(), output)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class ServerFormsTest : FormsTest
|
||||
{
|
||||
public ServerFormsTest(BrowserFixture browserFixture, ToggleExecutionModeServerFixture<Program> serverFixture, ITestOutputHelper output)
|
||||
: base(browserFixture, serverFixture.WithServerExecution(), output)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class ServerKeyTest : KeyTest
|
||||
{
|
||||
public ServerKeyTest(BrowserFixture browserFixture, ToggleExecutionModeServerFixture<Program> serverFixture, ITestOutputHelper output)
|
||||
: base(browserFixture, serverFixture.WithServerExecution(), output)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -210,6 +210,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
Browser.Equal("modified valid", () => expiryDateInput.GetAttribute("class"));
|
||||
|
||||
// Can become invalid
|
||||
expiryDateInput.Clear();
|
||||
expiryDateInput.SendKeys("111111111");
|
||||
Browser.Equal("modified invalid", () => expiryDateInput.GetAttribute("class"));
|
||||
Browser.Equal(new[] { "The OptionalExpiryDate field must be a date." }, messagesAccessor);
|
||||
|
|
|
|||
|
|
@ -211,7 +211,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
{
|
||||
var appElem = MountTestComponent<ReorderingFocusComponent>();
|
||||
Func<IWebElement> textboxFinder = () => appElem.FindElement(By.CssSelector(".incomplete-items .item-1 input[type=text]"));
|
||||
var textToType = "Hello this is a long string that should be typed";
|
||||
var textToType = "Hello there!";
|
||||
var expectedTextTyped = "";
|
||||
|
||||
textboxFinder().Clear();
|
||||
|
|
@ -221,17 +221,16 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
textboxFinder().Click();
|
||||
while (textToType.Length > 0)
|
||||
{
|
||||
var nextBlockLength = Math.Min(5, textToType.Length);
|
||||
var nextBlock = textToType.Substring(0, nextBlockLength);
|
||||
textToType = textToType.Substring(nextBlockLength);
|
||||
expectedTextTyped += nextBlock;
|
||||
var nextChar = textToType.Substring(0, 1);
|
||||
textToType = textToType.Substring(1);
|
||||
expectedTextTyped += nextChar;
|
||||
|
||||
// Send keys to whatever has focus
|
||||
new Actions(Browser).SendKeys(nextBlock).Perform();
|
||||
new Actions(Browser).SendKeys(nextChar).Perform();
|
||||
Browser.Equal(expectedTextTyped, () => textboxFinder().GetAttribute("value"));
|
||||
|
||||
// We delay between typings to ensure the events aren't all collapsed into one.
|
||||
await Task.Delay(100);
|
||||
await Task.Delay(50);
|
||||
}
|
||||
|
||||
// Verify that after all this, we can still move the edited item
|
||||
|
|
|
|||
Loading…
Reference in New Issue