Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ public partial class WebAssemblyRenderer : Microsoft.AspNetCore.Components.Rende
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; }
Expand Down
77 changes: 77 additions & 0 deletions src/Components/Blazor/Blazor/src/Rendering/WebAssemblyRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -18,6 +19,9 @@ public class WebAssemblyRenderer : Renderer
{
private readonly int _webAssemblyRendererId;

private bool isDispatchingEvent;
private Queue<IncomingEventInfo> deferredIncomingEvents = new Queue<IncomingEventInfo>();

/// <summary>
/// Constructs an instance of <see cref="WebAssemblyRenderer"/>.
/// </summary>
Expand Down Expand Up @@ -110,5 +114,78 @@ protected override void HandleException(Exception exception)
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>();
}
}
}
}
78 changes: 3 additions & 75 deletions src/Components/Browser/src/RendererRegistryEventDispatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -14,73 +13,16 @@ 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>
[JSInvokable(nameof(DispatchEvent))]
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)
Expand Down Expand Up @@ -136,19 +78,5 @@ public class BrowserEventDescriptor
/// </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>();
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -710,7 +710,7 @@ public event System.UnhandledExceptionEventHandler UnhandledSynchronizationExcep
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ static bool TryParseLong(string value, out T result)
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;
Expand All @@ -134,7 +134,7 @@ static bool TryParseFloat(string value, out T result)
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;
Expand Down
2 changes: 1 addition & 1 deletion src/Components/Components/src/Rendering/Renderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ private ComponentState AttachAndInitComponent(IComponent component, int parentCo
/// 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();

Expand Down
24 changes: 24 additions & 0 deletions src/Components/test/E2ETest/ServerExecutionTests/TestSubclasses.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,28 @@ public ServerCascadingValueTest(BrowserFixture browserFixture, ToggleExecutionMo
{
}
}

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)
{
}
}
}
1 change: 1 addition & 0 deletions src/Components/test/E2ETest/Tests/FormsTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ public void InputDateInteractsWithEditContext_NullableDateTimeOffset()
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);
Expand Down
13 changes: 6 additions & 7 deletions src/Components/test/E2ETest/Tests/KeyTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ public async Task CanRetainFocusWhileMovingTextBox()
{
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();
Expand All @@ -221,17 +221,16 @@ public async Task CanRetainFocusWhileMovingTextBox()
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
Expand Down