Fix components event dispatch ordering + E2E test fixes (#10112)

This commit is contained in:
Steve Sanderson 2019-05-09 18:32:26 +01:00 committed by GitHub
parent dcf49f2575
commit 4db8260c6c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 116 additions and 86 deletions

View File

@ -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; }

View File

@ -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>();
}
}
}
}

View File

@ -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>();
}
}
}
}

View File

@ -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);

View File

@ -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;

View File

@ -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();

View File

@ -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)
{
}
}
}

View File

@ -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);

View File

@ -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