diff --git a/src/Components/Blazor/Build/test/RazorIntegrationTestBase.cs b/src/Components/Blazor/Build/test/RazorIntegrationTestBase.cs index c05ca16c7b..33d0c93f7c 100644 --- a/src/Components/Blazor/Build/test/RazorIntegrationTestBase.cs +++ b/src/Components/Blazor/Build/test/RazorIntegrationTestBase.cs @@ -376,6 +376,11 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test protected RenderTreeFrame[] GetRenderTree(IComponent component) { var renderer = new TestRenderer(); + return GetRenderTree(renderer, component); + } + + protected private RenderTreeFrame[] GetRenderTree(TestRenderer renderer, IComponent component) + { renderer.AttachComponent(component); var task = renderer.InvokeAsync(() => component.SetParametersAsync(ParameterCollection.Empty)); // we will have to change this method if we add a test that does actual async work. @@ -432,7 +437,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test public IEnumerable Diagnostics { get; set; } } - private class TestRenderer : Renderer + protected class TestRenderer : Renderer { public TestRenderer() : base(new TestServiceProvider(), CreateDefaultDispatcher()) { diff --git a/src/Components/Blazor/Build/test/RenderingRazorIntegrationTest.cs b/src/Components/Blazor/Build/test/RenderingRazorIntegrationTest.cs index 4bb88a2eeb..7f48119dc1 100644 --- a/src/Components/Blazor/Build/test/RenderingRazorIntegrationTest.cs +++ b/src/Components/Blazor/Build/test/RenderingRazorIntegrationTest.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.RenderTree; using Microsoft.AspNetCore.Components.Test.Helpers; @@ -351,7 +352,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test } [Fact] - public void SupportsTwoWayBindingForTextboxes() + public async Task SupportsTwoWayBindingForTextboxes() { // Arrange/Act var component = CompileToComponent( @@ -361,26 +362,27 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test }"); var myValueProperty = component.GetType().GetProperty("MyValue"); + var renderer = new TestRenderer(); + // Assert - var frames = GetRenderTree(component); + Action setter = null; + var frames = GetRenderTree(renderer, component); Assert.Collection(frames, frame => AssertFrame.Element(frame, "input", 3, 0), frame => AssertFrame.Attribute(frame, "value", "Initial value", 1), frame => { AssertFrame.Attribute(frame, "onchange", 2); - - // Trigger the change event to show it updates the property - ((Action)frame.AttributeValue)(new UIChangeEventArgs - { - Value = "Modified value" - }); - Assert.Equal("Modified value", myValueProperty.GetValue(component)); + setter = Assert.IsType>(frame.AttributeValue); }); + + // Trigger the change event to show it updates the property + await renderer.Invoke(() => setter(new UIChangeEventArgs { Value = "Modified value", })); + Assert.Equal("Modified value", myValueProperty.GetValue(component)); } [Fact] - public void SupportsTwoWayBindingForTextareas() + public async Task SupportsTwoWayBindingForTextareas() { // Arrange/Act var component = CompileToComponent( @@ -390,26 +392,27 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test }"); var myValueProperty = component.GetType().GetProperty("MyValue"); + var renderer = new TestRenderer(); + // Assert - var frames = GetRenderTree(component); + Action setter = null; + var frames = GetRenderTree(renderer, component); Assert.Collection(frames, frame => AssertFrame.Element(frame, "textarea", 3, 0), frame => AssertFrame.Attribute(frame, "value", "Initial value", 1), frame => { AssertFrame.Attribute(frame, "onchange", 2); - - // Trigger the change event to show it updates the property - ((Action)frame.AttributeValue)(new UIChangeEventArgs - { - Value = "Modified value" - }); - Assert.Equal("Modified value", myValueProperty.GetValue(component)); + setter = Assert.IsType>(frame.AttributeValue); }); + + // Trigger the change event to show it updates the property + await renderer.Invoke(() => setter(new UIChangeEventArgs() { Value = "Modified value", })); + Assert.Equal("Modified value", myValueProperty.GetValue(component)); } [Fact] - public void SupportsTwoWayBindingForDateValues() + public async Task SupportsTwoWayBindingForDateValues() { // Arrange/Act var component = CompileToComponent( @@ -419,27 +422,28 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test }"); var myDateProperty = component.GetType().GetProperty("MyDate"); + var renderer = new TestRenderer(); + // Assert - var frames = GetRenderTree(component); + Action setter = null; + var frames = GetRenderTree(renderer, component); Assert.Collection(frames, frame => AssertFrame.Element(frame, "input", 3, 0), frame => AssertFrame.Attribute(frame, "value", new DateTime(2018, 3, 4, 1, 2, 3).ToString(), 1), frame => { AssertFrame.Attribute(frame, "onchange", 2); - - // Trigger the change event to show it updates the property - var newDateValue = new DateTime(2018, 3, 5, 4, 5, 6); - ((Action)frame.AttributeValue)(new UIChangeEventArgs - { - Value = newDateValue.ToString() - }); - Assert.Equal(newDateValue, myDateProperty.GetValue(component)); + setter = Assert.IsType>(frame.AttributeValue); }); + + // Trigger the change event to show it updates the property + var newDateValue = new DateTime(2018, 3, 5, 4, 5, 6); + await renderer.Invoke(() => setter(new UIChangeEventArgs() { Value = newDateValue.ToString(), })); + Assert.Equal(newDateValue, myDateProperty.GetValue(component)); } [Fact] - public void SupportsTwoWayBindingForDateValuesWithFormatString() + public async Task SupportsTwoWayBindingForDateValuesWithFormatString() { // Arrange/Act var testDateFormat = "ddd yyyy-MM-dd"; @@ -450,22 +454,23 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test }}"); var myDateProperty = component.GetType().GetProperty("MyDate"); + var renderer = new TestRenderer(); + // Assert - var frames = GetRenderTree(component); + Action setter = null; + var frames = GetRenderTree(renderer, component); Assert.Collection(frames, frame => AssertFrame.Element(frame, "input", 3, 0), frame => AssertFrame.Attribute(frame, "value", new DateTime(2018, 3, 4).ToString(testDateFormat), 1), frame => { AssertFrame.Attribute(frame, "onchange", 2); - - // Trigger the change event to show it updates the property - ((Action)frame.AttributeValue)(new UIChangeEventArgs - { - Value = new DateTime(2018, 3, 5).ToString(testDateFormat) - }); - Assert.Equal(new DateTime(2018, 3, 5), myDateProperty.GetValue(component)); + setter = Assert.IsType>(frame.AttributeValue); }); + + // Trigger the change event to show it updates the property + await renderer.Invoke(() => setter(new UIChangeEventArgs() { Value = new DateTime(2018, 3, 5).ToString(testDateFormat), })); + Assert.Equal(new DateTime(2018, 3, 5), myDateProperty.GetValue(component)); } [Fact] // In this case, onclick is just a normal HTML attribute @@ -496,8 +501,10 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test var clicked = component.GetType().GetProperty("Clicked"); + var renderer = new TestRenderer(); + // Act - var frames = GetRenderTree(component); + var frames = GetRenderTree(renderer, component); // Assert Assert.Collection(frames, @@ -527,8 +534,10 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test var clicked = component.GetType().GetProperty("Clicked"); + var renderer = new TestRenderer(); + // Act - var frames = GetRenderTree(component); + var frames = GetRenderTree(renderer, component); // Assert Assert.Collection(frames, @@ -546,7 +555,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test } [Fact] - public void SupportsTwoWayBindingForBoolValues() + public async Task SupportsTwoWayBindingForBoolValues() { // Arrange/Act var component = CompileToComponent( @@ -556,26 +565,27 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test }"); var myValueProperty = component.GetType().GetProperty("MyValue"); + var renderer = new TestRenderer(); + // Assert - var frames = GetRenderTree(component); + Action setter = null; + var frames = GetRenderTree(renderer, component); Assert.Collection(frames, frame => AssertFrame.Element(frame, "input", 3, 0), frame => AssertFrame.Attribute(frame, "value", true, 1), frame => { AssertFrame.Attribute(frame, "onchange", 2); - - // Trigger the change event to show it updates the property - ((Action)frame.AttributeValue)(new UIChangeEventArgs - { - Value = false - }); - Assert.False((bool)myValueProperty.GetValue(component)); + setter = Assert.IsType>(frame.AttributeValue); }); + + // Trigger the change event to show it updates the property + await renderer.Invoke(() => setter(new UIChangeEventArgs() { Value = false, })); + Assert.False((bool)myValueProperty.GetValue(component)); } [Fact] - public void SupportsTwoWayBindingForEnumValues() + public async Task SupportsTwoWayBindingForEnumValues() { // Arrange/Act var myEnumType = FullTypeName(); @@ -586,22 +596,23 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test }}"); var myValueProperty = component.GetType().GetProperty("MyValue"); + var renderer = new TestRenderer(); + // Assert - var frames = GetRenderTree(component); + Action setter = null; + var frames = GetRenderTree(renderer, component); Assert.Collection(frames, frame => AssertFrame.Element(frame, "input", 3, 0), frame => AssertFrame.Attribute(frame, "value", MyEnum.FirstValue.ToString(), 1), frame => { AssertFrame.Attribute(frame, "onchange", 2); - - // Trigger the change event to show it updates the property - ((Action)frame.AttributeValue)(new UIChangeEventArgs - { - Value = MyEnum.SecondValue.ToString() - }); - Assert.Equal(MyEnum.SecondValue, (MyEnum)myValueProperty.GetValue(component)); + setter = Assert.IsType>(frame.AttributeValue); }); + + // Trigger the change event to show it updates the property + await renderer.Invoke(() => setter(new UIChangeEventArgs() { Value = MyEnum.SecondValue.ToString(), })); + Assert.Equal(MyEnum.SecondValue, (MyEnum)myValueProperty.GetValue(component)); } public enum MyEnum { FirstValue, SecondValue } diff --git a/src/Components/Browser.JS/src/Rendering/BrowserRenderer.ts b/src/Components/Browser.JS/src/Rendering/BrowserRenderer.ts index 3744323880..0499a45e60 100644 --- a/src/Components/Browser.JS/src/Rendering/BrowserRenderer.ts +++ b/src/Components/Browser.JS/src/Rendering/BrowserRenderer.ts @@ -1,6 +1,4 @@ -import { System_Array, MethodHandle } from '../Platform/Platform'; -import { RenderBatch, ArraySegment, ArrayRange, RenderTreeEdit, RenderTreeFrame, EditType, FrameType, ArrayValues } from './RenderBatch/RenderBatch'; -import { platform } from '../Environment'; +import { RenderBatch, ArraySegment, RenderTreeEdit, RenderTreeFrame, EditType, FrameType, ArrayValues } from './RenderBatch/RenderBatch'; import { EventDelegator } from './EventDelegator'; import { EventForDotNet, UIEventArgs } from './EventForDotNet'; import { LogicalElement, toLogicalElement, insertLogicalChild, removeLogicalChild, getLogicalParent, getLogicalChild, createAndInsertLogicalContainer, isSvgElement } from './LogicalElements'; @@ -9,16 +7,14 @@ const selectValuePropname = '_blazorSelectValue'; const sharedTemplateElemForParsing = document.createElement('template'); const sharedSvgElemForParsing = document.createElementNS('http://www.w3.org/2000/svg', 'g'); const preventDefaultEvents: { [eventType: string]: boolean } = { submit: true }; -let raiseEventMethod: MethodHandle; -let renderComponentMethod: MethodHandle; export class BrowserRenderer { private eventDelegator: EventDelegator; private childComponentLocations: { [componentId: number]: LogicalElement } = {}; constructor(private browserRendererId: number) { - this.eventDelegator = new EventDelegator((event, componentId, eventHandlerId, eventArgs) => { - raiseEvent(event, this.browserRendererId, componentId, eventHandlerId, eventArgs); + this.eventDelegator = new EventDelegator((event, eventHandlerId, eventArgs) => { + raiseEvent(event, this.browserRendererId, eventHandlerId, eventArgs); }); } @@ -32,7 +28,7 @@ export class BrowserRenderer { throw new Error(`No element is currently associated with component ${componentId}`); } - this.applyEdits(batch, componentId, element, 0, edits, referenceFrames); + this.applyEdits(batch, element, 0, edits, referenceFrames); } public disposeComponent(componentId: number) { @@ -47,7 +43,7 @@ export class BrowserRenderer { this.childComponentLocations[componentId] = element; } - private applyEdits(batch: RenderBatch, componentId: number, parent: LogicalElement, childIndex: number, edits: ArraySegment, referenceFrames: ArrayValues) { + private applyEdits(batch: RenderBatch, parent: LogicalElement, childIndex: number, edits: ArraySegment, referenceFrames: ArrayValues) { let currentDepth = 0; let childIndexAtCurrentDepth = childIndex; @@ -67,7 +63,7 @@ export class BrowserRenderer { const frameIndex = editReader.newTreeIndex(edit); const frame = batch.referenceFramesEntry(referenceFrames, frameIndex); const siblingIndex = editReader.siblingIndex(edit); - this.insertFrame(batch, componentId, parent, childIndexAtCurrentDepth + siblingIndex, referenceFrames, frame, frameIndex); + this.insertFrame(batch, parent, childIndexAtCurrentDepth + siblingIndex, referenceFrames, frame, frameIndex); break; } case EditType.removeFrame: { @@ -81,7 +77,7 @@ export class BrowserRenderer { const siblingIndex = editReader.siblingIndex(edit); const element = getLogicalChild(parent, childIndexAtCurrentDepth + siblingIndex); if (element instanceof Element) { - this.applyAttribute(batch, componentId, element, frame); + this.applyAttribute(batch, element, frame); } else { throw new Error(`Cannot set attribute on non-element child`); } @@ -145,12 +141,12 @@ export class BrowserRenderer { } } - private insertFrame(batch: RenderBatch, componentId: number, parent: LogicalElement, childIndex: number, frames: ArrayValues, frame: RenderTreeFrame, frameIndex: number): number { + private insertFrame(batch: RenderBatch, parent: LogicalElement, childIndex: number, frames: ArrayValues, frame: RenderTreeFrame, frameIndex: number): number { const frameReader = batch.frameReader; const frameType = frameReader.frameType(frame); switch (frameType) { case FrameType.element: - this.insertElement(batch, componentId, parent, childIndex, frames, frame, frameIndex); + this.insertElement(batch, parent, childIndex, frames, frame, frameIndex); return 1; case FrameType.text: this.insertText(batch, parent, childIndex, frame); @@ -161,7 +157,7 @@ export class BrowserRenderer { this.insertComponent(batch, parent, childIndex, frame); return 1; case FrameType.region: - return this.insertFrameRange(batch, componentId, parent, childIndex, frames, frameIndex + 1, frameIndex + frameReader.subtreeLength(frame)); + return this.insertFrameRange(batch, parent, childIndex, frames, frameIndex + 1, frameIndex + frameReader.subtreeLength(frame)); case FrameType.elementReferenceCapture: if (parent instanceof Element) { applyCaptureIdToElement(parent, frameReader.elementReferenceCaptureId(frame)!); @@ -178,7 +174,7 @@ export class BrowserRenderer { } } - private insertElement(batch: RenderBatch, componentId: number, parent: LogicalElement, childIndex: number, frames: ArrayValues, frame: RenderTreeFrame, frameIndex: number) { + private insertElement(batch: RenderBatch, parent: LogicalElement, childIndex: number, frames: ArrayValues, frame: RenderTreeFrame, frameIndex: number) { const frameReader = batch.frameReader; const tagName = frameReader.elementName(frame)!; const newDomElementRaw = tagName === 'svg' || isSvgElement(parent) ? @@ -192,11 +188,11 @@ export class BrowserRenderer { for (let descendantIndex = frameIndex + 1; descendantIndex < descendantsEndIndexExcl; descendantIndex++) { const descendantFrame = batch.referenceFramesEntry(frames, descendantIndex); if (frameReader.frameType(descendantFrame) === FrameType.attribute) { - this.applyAttribute(batch, componentId, newDomElementRaw, descendantFrame); + this.applyAttribute(batch, newDomElementRaw, descendantFrame); } else { // As soon as we see a non-attribute child, all the subsequent child frames are // not attributes, so bail out and insert the remnants recursively - this.insertFrameRange(batch, componentId, newElement, 0, frames, descendantIndex, descendantsEndIndexExcl); + this.insertFrameRange(batch, newElement, 0, frames, descendantIndex, descendantsEndIndexExcl); break; } } @@ -228,7 +224,7 @@ export class BrowserRenderer { } } - private applyAttribute(batch: RenderBatch, componentId: number, toDomElement: Element, attributeFrame: RenderTreeFrame) { + private applyAttribute(batch: RenderBatch, toDomElement: Element, attributeFrame: RenderTreeFrame) { const frameReader = batch.frameReader; const attributeName = frameReader.attributeName(attributeFrame)!; const browserRendererId = this.browserRendererId; @@ -240,7 +236,7 @@ export class BrowserRenderer { if (firstTwoChars !== 'on' || !eventName) { throw new Error(`Attribute has nonzero event handler ID, but attribute name '${attributeName}' does not start with 'on'.`); } - this.eventDelegator.setListener(toDomElement, eventName, componentId, eventHandlerId); + this.eventDelegator.setListener(toDomElement, eventName, eventHandlerId); return; } @@ -315,11 +311,11 @@ export class BrowserRenderer { } } - private insertFrameRange(batch: RenderBatch, componentId: number, parent: LogicalElement, childIndex: number, frames: ArrayValues, startIndex: number, endIndexExcl: number): number { + private insertFrameRange(batch: RenderBatch, parent: LogicalElement, childIndex: number, frames: ArrayValues, startIndex: number, endIndexExcl: number): number { const origChildIndex = childIndex; for (let index = startIndex; index < endIndexExcl; index++) { const frame = batch.referenceFramesEntry(frames, index); - const numChildrenInserted = this.insertFrame(batch, componentId, parent, childIndex, frames, frame, index); + const numChildrenInserted = this.insertFrame(batch, parent, childIndex, frames, frame, index); childIndex += numChildrenInserted; // Skip over any descendants, since they are already dealt with recursively @@ -355,14 +351,13 @@ function countDescendantFrames(batch: RenderBatch, frame: RenderTreeFrame): numb } } -function raiseEvent(event: Event, browserRendererId: number, componentId: number, eventHandlerId: number, eventArgs: EventForDotNet) { +function raiseEvent(event: Event, browserRendererId: number, eventHandlerId: number, eventArgs: EventForDotNet) { if (preventDefaultEvents[event.type]) { event.preventDefault(); } const eventDescriptor = { browserRendererId, - componentId, eventHandlerId, eventArgsType: eventArgs.type }; diff --git a/src/Components/Browser.JS/src/Rendering/EventDelegator.ts b/src/Components/Browser.JS/src/Rendering/EventDelegator.ts index af101f506c..e17ac99ef0 100644 --- a/src/Components/Browser.JS/src/Rendering/EventDelegator.ts +++ b/src/Components/Browser.JS/src/Rendering/EventDelegator.ts @@ -6,7 +6,7 @@ const nonBubblingEvents = toLookup([ ]); export interface OnEventCallback { - (event: Event, componentId: number, eventHandlerId: number, eventArgs: EventForDotNet): void; + (event: Event, eventHandlerId: number, eventArgs: EventForDotNet): void; } // Responsible for adding/removing the eventInfo on an expando property on DOM elements, and @@ -23,7 +23,7 @@ export class EventDelegator { this.eventInfoStore = new EventInfoStore(this.onGlobalEvent.bind(this)); } - public setListener(element: Element, eventName: string, componentId: number, eventHandlerId: number) { + public setListener(element: Element, eventName: string, eventHandlerId: number) { // Ensure we have a place to store event info for this element let infoForElement: EventHandlerInfosForElement = element[this.eventsCollectionKey]; if (!infoForElement) { @@ -36,7 +36,7 @@ export class EventDelegator { this.eventInfoStore.update(oldInfo.eventHandlerId, eventHandlerId); } else { // Go through the whole flow which might involve registering a new global handler - const newInfo = { element, eventName, componentId, eventHandlerId }; + const newInfo = { element, eventName, eventHandlerId }; this.eventInfoStore.add(newInfo); infoForElement[eventName] = newInfo; } @@ -80,7 +80,7 @@ export class EventDelegator { } const handlerInfo = handlerInfos[evt.type]; - this.onEvent(evt, handlerInfo.componentId, handlerInfo.eventHandlerId, eventArgs); + this.onEvent(evt, handlerInfo.eventHandlerId, eventArgs); } } @@ -161,7 +161,6 @@ interface EventHandlerInfosForElement { interface EventHandlerInfo { element: Element; eventName: string; - componentId: number; eventHandlerId: number; } diff --git a/src/Components/Browser/src/RendererRegistryEventDispatcher.cs b/src/Components/Browser/src/RendererRegistryEventDispatcher.cs index 1004b1e1d4..469a0dcae0 100644 --- a/src/Components/Browser/src/RendererRegistryEventDispatcher.cs +++ b/src/Components/Browser/src/RendererRegistryEventDispatcher.cs @@ -22,11 +22,7 @@ namespace Microsoft.AspNetCore.Components.Browser { var eventArgs = ParseEventArgsJson(eventDescriptor.EventArgsType, eventArgsJson); var renderer = RendererRegistry.Current.Find(eventDescriptor.BrowserRendererId); - - return renderer.DispatchEventAsync( - eventDescriptor.ComponentId, - eventDescriptor.EventHandlerId, - eventArgs); + return renderer.DispatchEventAsync(eventDescriptor.EventHandlerId, eventArgs); } private static UIEventArgs ParseEventArgsJson(string eventArgsType, string eventArgsJson) @@ -72,11 +68,6 @@ namespace Microsoft.AspNetCore.Components.Browser /// public int BrowserRendererId { get; set; } - /// - /// For framework use only. - /// - public int ComponentId { get; set; } - /// /// For framework use only. /// diff --git a/src/Components/Components/src/BindMethods.cs b/src/Components/Components/src/BindMethods.cs index 52affaa6a1..4c0962ed01 100644 --- a/src/Components/Components/src/BindMethods.cs +++ b/src/Components/Components/src/BindMethods.cs @@ -69,12 +69,34 @@ namespace Microsoft.AspNetCore.Components return value; } + /// + /// Not intended to be used directly. + /// + public static EventCallback GetEventHandlerValue(EventCallback value) + where T : UIEventArgs + { + return value; + } + + /// + /// Not intended to be used directly. + /// + public static EventCallback GetEventHandlerValue(EventCallback value) + where T : UIEventArgs + { + return value; + } + /// /// Not intended to be used directly. /// public static Action SetValueHandler(Action setter, string existingValue) { - return _ => setter((string)((UIChangeEventArgs)_).Value); + return eventArgs => + { + setter((string)((UIChangeEventArgs)eventArgs).Value); + _ = DispatchEventAsync(setter.Target, EventCallbackWorkItem.Empty, UIEventArgs.Empty); + }; } /// @@ -82,7 +104,11 @@ namespace Microsoft.AspNetCore.Components /// public static Action SetValueHandler(Action setter, bool existingValue) { - return _ => setter((bool)((UIChangeEventArgs)_).Value); + return eventArgs => + { + setter((bool)((UIChangeEventArgs)eventArgs).Value); + _ = DispatchEventAsync(setter.Target, EventCallbackWorkItem.Empty, UIEventArgs.Empty); + }; } /// @@ -90,7 +116,11 @@ namespace Microsoft.AspNetCore.Components /// public static Action SetValueHandler(Action setter, bool? existingValue) { - return _ => setter((bool?)((UIChangeEventArgs)_).Value); + return eventArgs => + { + setter((bool?)((UIChangeEventArgs)eventArgs).Value); + _ = DispatchEventAsync(setter.Target, EventCallbackWorkItem.Empty, UIEventArgs.Empty); + }; } /// @@ -98,7 +128,11 @@ namespace Microsoft.AspNetCore.Components /// public static Action SetValueHandler(Action setter, int existingValue) { - return _ => setter(int.Parse((string)((UIChangeEventArgs)_).Value)); + return eventArgs => + { + setter(int.Parse((string)((UIChangeEventArgs)eventArgs).Value)); + _ = DispatchEventAsync(setter.Target, EventCallbackWorkItem.Empty, UIEventArgs.Empty); + }; } /// @@ -106,9 +140,11 @@ namespace Microsoft.AspNetCore.Components /// public static Action SetValueHandler(Action setter, int? existingValue) { - return _ => setter(int.TryParse((string)((UIChangeEventArgs)_).Value, out var tmpvalue) - ? tmpvalue - : (int?)null); + return eventArgs => + { + setter(int.TryParse((string)((UIChangeEventArgs)eventArgs).Value, out var value) ? value : (int?)null); + _ = DispatchEventAsync(setter.Target, EventCallbackWorkItem.Empty, UIEventArgs.Empty); + }; } /// @@ -116,7 +152,11 @@ namespace Microsoft.AspNetCore.Components /// public static Action SetValueHandler(Action setter, long existingValue) { - return _ => setter(long.Parse((string)((UIChangeEventArgs)_).Value)); + return eventArgs => + { + setter(long.Parse((string)((UIChangeEventArgs)eventArgs).Value)); + _ = DispatchEventAsync(setter.Target, EventCallbackWorkItem.Empty, UIEventArgs.Empty); + }; } /// @@ -124,9 +164,11 @@ namespace Microsoft.AspNetCore.Components /// public static Action SetValueHandler(Action setter, long? existingValue) { - return _ => setter(long.TryParse((string)((UIChangeEventArgs)_).Value, out var tmpvalue) - ? tmpvalue - : (long?)null); + return eventArgs => + { + setter(long.TryParse((string)((UIChangeEventArgs)eventArgs).Value, out var value) ? value : (long?)null); + _ = DispatchEventAsync(setter.Target, EventCallbackWorkItem.Empty, UIEventArgs.Empty); + }; } /// @@ -134,7 +176,11 @@ namespace Microsoft.AspNetCore.Components /// public static Action SetValueHandler(Action setter, float existingValue) { - return _ => setter(float.Parse((string)((UIChangeEventArgs)_).Value)); + return eventArgs => + { + setter(float.Parse((string)((UIChangeEventArgs)eventArgs).Value)); + _ = DispatchEventAsync(setter.Target, EventCallbackWorkItem.Empty, UIEventArgs.Empty); + }; } /// @@ -142,9 +188,11 @@ namespace Microsoft.AspNetCore.Components /// public static Action SetValueHandler(Action setter, float? existingValue) { - return _ => setter(float.TryParse((string)((UIChangeEventArgs)_).Value, out var tmpvalue) - ? tmpvalue - : (float?)null); + return eventArgs => + { + setter(float.TryParse((string)((UIChangeEventArgs)eventArgs).Value, out var value) ? value : (float?)null); + _ = DispatchEventAsync(setter.Target, EventCallbackWorkItem.Empty, UIEventArgs.Empty); + }; } /// @@ -152,7 +200,11 @@ namespace Microsoft.AspNetCore.Components /// public static Action SetValueHandler(Action setter, double existingValue) { - return _ => setter(double.Parse((string)((UIChangeEventArgs)_).Value)); + return eventArgs => + { + setter(double.Parse((string)((UIChangeEventArgs)eventArgs).Value)); + _ = DispatchEventAsync(setter.Target, EventCallbackWorkItem.Empty, UIEventArgs.Empty); + }; } /// @@ -160,9 +212,11 @@ namespace Microsoft.AspNetCore.Components /// public static Action SetValueHandler(Action setter, double? existingValue) { - return _ => setter(double.TryParse((string)((UIChangeEventArgs)_).Value, out var tmpvalue) - ? tmpvalue - : (double?)null); + return eventArgs => + { + setter(double.TryParse((string)((UIChangeEventArgs)eventArgs).Value, out var value) ? value : (double?)null); + _ = DispatchEventAsync(setter.Target, EventCallbackWorkItem.Empty, UIEventArgs.Empty); + }; } /// @@ -170,7 +224,11 @@ namespace Microsoft.AspNetCore.Components /// public static Action SetValueHandler(Action setter, decimal existingValue) { - return _ => setter(decimal.Parse((string)((UIChangeEventArgs)_).Value)); + return eventArgs => + { + setter(decimal.Parse((string)((UIChangeEventArgs)eventArgs).Value)); + _ = DispatchEventAsync(setter.Target, EventCallbackWorkItem.Empty, UIEventArgs.Empty); + }; } /// @@ -178,9 +236,11 @@ namespace Microsoft.AspNetCore.Components /// public static Action SetValueHandler(Action setter, decimal? existingValue) { - return _ => setter(decimal.TryParse((string)((UIChangeEventArgs)_).Value, out var tmpvalue) - ? tmpvalue - : (decimal?)null); + return eventArgs => + { + setter(decimal.TryParse((string)((UIChangeEventArgs)eventArgs).Value, out var tmpvalue) ? tmpvalue : (decimal?)null); + _ = DispatchEventAsync(setter.Target, EventCallbackWorkItem.Empty, UIEventArgs.Empty); + }; } /// @@ -188,7 +248,10 @@ namespace Microsoft.AspNetCore.Components /// public static Action SetValueHandler(Action setter, DateTime existingValue) { - return _ => SetDateTimeValue(setter, ((UIChangeEventArgs)_).Value, null); + return eventArgs => + { + SetDateTimeValue(setter, ((UIChangeEventArgs)eventArgs).Value, null); + }; } /// @@ -196,7 +259,10 @@ namespace Microsoft.AspNetCore.Components /// public static Action SetValueHandler(Action setter, DateTime existingValue, string format) { - return _ => SetDateTimeValue(setter, ((UIChangeEventArgs)_).Value, format); + return eventArgs => + { + SetDateTimeValue(setter, ((UIChangeEventArgs)eventArgs).Value, format); + }; } /// @@ -209,11 +275,12 @@ namespace Microsoft.AspNetCore.Components throw new ArgumentException($"'bind' does not accept values of type {typeof(T).FullName}. To read and write this value type, wrap it in a property of type string with suitable getters and setters."); } - return _ => + return eventArgs => { - var value = (string)((UIChangeEventArgs)_).Value; + var value = (string)((UIChangeEventArgs)eventArgs).Value; var parsed = (T)Enum.Parse(typeof(T), value); setter(parsed); + _ = DispatchEventAsync(setter.Target, EventCallbackWorkItem.Empty, UIEventArgs.Empty); }; } @@ -224,6 +291,24 @@ namespace Microsoft.AspNetCore.Components : format != null && DateTime.TryParseExact(stringValue, format, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var parsedExact) ? parsedExact : DateTime.Parse(stringValue); setter(parsedValue); + _ = DispatchEventAsync(setter.Target, EventCallbackWorkItem.Empty, UIEventArgs.Empty); + } + + // This is a temporary polyfill for these old-style bind methods until they can be removed. + // This doesn't do proper error handling (usage is fire-and-forget). + private static Task DispatchEventAsync(object component, EventCallbackWorkItem callback, object arg) + { + if (component == null) + { + throw new ArgumentNullException(nameof(component)); + } + + if (component is IHandleEvent handler) + { + return handler.HandleEventAsync(callback, arg); + } + + return callback.InvokeAsync(arg); } } } diff --git a/src/Components/Components/src/ComponentBase.cs b/src/Components/Components/src/ComponentBase.cs index 7c8b491b10..af4a36a946 100644 --- a/src/Components/Components/src/ComponentBase.cs +++ b/src/Components/Components/src/ComponentBase.cs @@ -67,7 +67,7 @@ namespace Microsoft.AspNetCore.Components /// /// Method invoked when the component is ready to start, having received its /// initial parameters from its parent in the render tree. - /// + /// /// Override this method if you will perform an asynchronous operation and /// want the component to refresh when that operation is completed. /// @@ -210,8 +210,8 @@ namespace Microsoft.AspNetCore.Components catch when (task.IsCanceled) { // Ignore exceptions from task cancelletions. - // Awaiting a canceled task may produce either an OperationCanceledException (if produced as a consequence of - // CancellationToken.ThrowIfCancellationRequested()) or a TaskCanceledException (produced as a consequence of awaiting Task.FromCanceled). + // Awaiting a canceled task may produce either an OperationCanceledException (if produced as a consequence of + // CancellationToken.ThrowIfCancellationRequested()) or a TaskCanceledException (produced as a consequence of awaiting Task.FromCanceled). // It's much easier to check the state of the Task (i.e. Task.IsCanceled) rather than catch two distinct exceptions. } @@ -255,9 +255,9 @@ namespace Microsoft.AspNetCore.Components StateHasChanged(); } - Task IHandleEvent.HandleEventAsync(EventHandlerInvoker binding, UIEventArgs args) + Task IHandleEvent.HandleEventAsync(EventCallbackWorkItem callback, object arg) { - var task = binding.Invoke(args); + var task = callback.InvokeAsync(arg); var shouldAwaitTask = task.Status != TaskStatus.RanToCompletion && task.Status != TaskStatus.Canceled; diff --git a/src/Components/Components/src/EventCallback.cs b/src/Components/Components/src/EventCallback.cs new file mode 100644 index 0000000000..ca3e3d6adc --- /dev/null +++ b/src/Components/Components/src/EventCallback.cs @@ -0,0 +1,115 @@ +// 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; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Components +{ + /// + /// A bound event handler delegate. + /// + public readonly struct EventCallback + { + /// + /// Gets a reference to the . + /// + public static readonly EventCallbackFactory Factory = new EventCallbackFactory(); + + /// + /// Gets an empty . + /// + public static readonly EventCallback Empty = new EventCallback(null, (Action)(() => { })); + + internal readonly MulticastDelegate Delegate; + internal readonly IHandleEvent Receiver; + + /// + /// Creates the new . + /// + /// The event receiver. + /// The delegate to bind. + public EventCallback(IHandleEvent receiver, MulticastDelegate @delegate) + { + Receiver = receiver; + Delegate = @delegate; + } + + /// + /// Gets a value that indicates whether the delegate associated with this event dispatcher is non-null. + /// + public bool HasDelegate => Delegate != null; + + // This is a hint to the runtime that Receiver is a different object than what + // Delegate.Target points to. This allows us to avoid boxing the command object + // when building the render tree. See logic where this is used. + internal bool RequiresExplicitReceiver => Receiver != null && !object.ReferenceEquals(Receiver, Delegate?.Target); + + /// + /// Invokes the delegate associated with this binding and dispatches an event notification to the + /// appropriate component. + /// + /// The argument. + /// A which completes asynchronously once event processing has completed. + public Task InvokeAsync(object arg) + { + if (Receiver == null) + { + return EventCallbackWorkItem.InvokeAsync(Delegate, arg); + } + + return Receiver.HandleEventAsync(new EventCallbackWorkItem(Delegate), arg); + } + } + + /// + /// A bound event handler delegate. + /// + public readonly struct EventCallback + { + internal readonly MulticastDelegate Delegate; + internal readonly IHandleEvent Receiver; + + /// + /// Creates the new . + /// + /// The event receiver. + /// The delegate to bind. + public EventCallback(IHandleEvent receiver, MulticastDelegate @delegate) + { + Receiver = receiver; + Delegate = @delegate; + } + + /// + /// Gets a value that indicates whether the delegate associated with this event dispatcher is non-null. + /// + public bool HasDelegate => Delegate != null; + + // This is a hint to the runtime that Reciever is a different object than what + // Delegate.Target points to. This allows us to avoid boxing the command object + // when building the render tree. See logic where this is used. + internal bool RequiresExplicitReceiver => Receiver != null && !object.ReferenceEquals(Receiver, Delegate?.Target); + + /// + /// Invokes the delegate associated with this binding and dispatches an event notification to the + /// appropriate component. + /// + /// The argument. + /// A which completes asynchronously once event processing has completed. + public Task InvokeAsync(T arg) + { + if (Receiver == null) + { + return EventCallbackWorkItem.InvokeAsync(Delegate, arg); + } + + return Receiver.HandleEventAsync(new EventCallbackWorkItem(Delegate), arg); + } + + internal EventCallback AsUntyped() + { + return new EventCallback(Receiver ?? Delegate?.Target as IHandleEvent, Delegate); + } + } +} diff --git a/src/Components/Components/src/EventCallbackFactory.cs b/src/Components/Components/src/EventCallbackFactory.cs new file mode 100644 index 0000000000..41f282f1df --- /dev/null +++ b/src/Components/Components/src/EventCallbackFactory.cs @@ -0,0 +1,211 @@ +// 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; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Components +{ + /// + /// A factory for creating and + /// instances. + /// + public sealed class EventCallbackFactory + { + /// + /// Creates an for the provided and + /// . + /// + /// The event receiver. + /// The event callback. + /// The . + public EventCallback Create(object receiver, Action callback) + { + if (receiver == null) + { + throw new ArgumentNullException(nameof(receiver)); + } + + if (callback == null) + { + throw new ArgumentNullException(nameof(callback)); + } + + return CreateCore(receiver, callback); + } + + /// + /// Creates an for the provided and + /// . + /// + /// The event receiver. + /// The event callback. + /// The . + public EventCallback Create(object receiver, Action callback) + { + if (receiver == null) + { + throw new ArgumentNullException(nameof(receiver)); + } + + if (callback == null) + { + throw new ArgumentNullException(nameof(callback)); + } + + return CreateCore(receiver, callback); + } + + /// + /// Creates an for the provided and + /// . + /// + /// The event receiver. + /// The event callback. + /// The . + public EventCallback Create(object receiver, Func callback) + { + if (receiver == null) + { + throw new ArgumentNullException(nameof(receiver)); + } + + if (callback == null) + { + throw new ArgumentNullException(nameof(callback)); + } + + return CreateCore(receiver, callback); + } + + /// + /// Creates an for the provided and + /// . + /// + /// The event receiver. + /// The event callback. + /// The . + public EventCallback Create(object receiver, Func callback) + { + if (receiver == null) + { + throw new ArgumentNullException(nameof(receiver)); + } + + if (callback == null) + { + throw new ArgumentNullException(nameof(callback)); + } + + return CreateCore(receiver, callback); + } + + /// + /// Creates an for the provided and + /// . + /// + /// The event receiver. + /// The event callback. + /// The . + public EventCallback Create(object receiver, Action callback) + { + if (receiver == null) + { + throw new ArgumentNullException(nameof(receiver)); + } + + if (callback == null) + { + throw new ArgumentNullException(nameof(callback)); + } + + return CreateCore(receiver, callback); + } + + /// + /// Creates an for the provided and + /// . + /// + /// The event receiver. + /// The event callback. + /// The . + public EventCallback Create(object receiver, Action callback) + { + if (receiver == null) + { + throw new ArgumentNullException(nameof(receiver)); + } + + if (callback == null) + { + throw new ArgumentNullException(nameof(callback)); + } + + return CreateCore(receiver, callback); + } + + /// + /// Creates an for the provided and + /// . + /// + /// The event receiver. + /// The event callback. + /// The . + public EventCallback Create(object receiver, Func callback) + { + if (receiver == null) + { + throw new ArgumentNullException(nameof(receiver)); + } + + if (callback == null) + { + throw new ArgumentNullException(nameof(callback)); + } + + return CreateCore(receiver, callback); + } + + /// + /// Creates an for the provided and + /// . + /// + /// The event receiver. + /// The event callback. + /// The . + public EventCallback Create(object receiver, Func callback) + { + if (receiver == null) + { + throw new ArgumentNullException(nameof(receiver)); + } + + if (callback == null) + { + throw new ArgumentNullException(nameof(callback)); + } + + return CreateCore(receiver, callback); + } + + private EventCallback CreateCore(object receiver, MulticastDelegate callback) + { + if (!object.ReferenceEquals(receiver, callback.Target) && receiver is IHandleEvent handler) + { + return new EventCallback(handler, callback); + } + + return new EventCallback(callback.Target as IHandleEvent, callback); + } + + private EventCallback CreateCore(object receiver, MulticastDelegate callback) + { + if (!object.ReferenceEquals(receiver, callback.Target) && receiver is IHandleEvent handler) + { + return new EventCallback(handler, callback); + } + + return new EventCallback(callback.Target as IHandleEvent, callback); + } + } +} diff --git a/src/Components/Components/src/EventCallbackFactoryBinderExtensions.cs b/src/Components/Components/src/EventCallbackFactoryBinderExtensions.cs new file mode 100644 index 0000000000..d35eb287c1 --- /dev/null +++ b/src/Components/Components/src/EventCallbackFactoryBinderExtensions.cs @@ -0,0 +1,411 @@ +// 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; +using System.Globalization; + +namespace Microsoft.AspNetCore.Components +{ + /// + /// Contains extension methods for two-way binding using . For internal use only. + /// + public static class EventCallbackFactoryBinderExtensions + { + // Perf: conversion delegates are written as static funcs so we can prevent + // allocations for these simple cases. + private static Func ConvertToString = (obj) => (string)obj; + + private static Func ConvertToBool = (obj) => (bool)obj; + private static Func ConvertToNullableBool = (obj) => (bool?)obj; + + private static Func ConvertToInt = (obj) => int.Parse((string)obj); + private static Func ConvertToNullableInt = (obj) => + { + if (int.TryParse((string)obj, out var value)) + { + return value; + } + + return null; + }; + + private static Func ConvertToLong = (obj) => long.Parse((string)obj); + private static Func ConvertToNullableLong = (obj) => + { + if (long.TryParse((string)obj, out var value)) + { + return value; + } + + return null; + }; + + private static Func ConvertToFloat = (obj) => float.Parse((string)obj); + private static Func ConvertToNullableFloat = (obj) => + { + if (float.TryParse((string)obj, out var value)) + { + return value; + } + + return null; + }; + + private static Func ConvertToDouble = (obj) => double.Parse((string)obj); + private static Func ConvertToNullableDouble = (obj) => + { + if (double.TryParse((string)obj, out var value)) + { + return value; + } + + return null; + }; + + private static Func ConvertToDecimal = (obj) => decimal.Parse((string)obj); + private static Func ConvertToNullableDecimal = (obj) => + { + if (decimal.TryParse((string)obj, out var value)) + { + return value; + } + + return null; + }; + + private static class EnumConverter where T : Enum + { + public static Func Convert = (obj) => + { + return (T)Enum.Parse(typeof(T), (string)obj); + }; + } + + /// + /// For internal use only. + /// + /// + /// + /// + /// + /// + public static EventCallback CreateBinder( + this EventCallbackFactory factory, + object receiver, + Action setter, + string existingValue) + { + ; + return CreateBinderCore(factory, receiver, setter, ConvertToString); + } + + /// + /// For internal use only. + /// + /// + /// + /// + /// + /// + public static EventCallback CreateBinder( + this EventCallbackFactory factory, + object receiver, + Action setter, + bool existingValue) + { + return CreateBinderCore(factory, receiver, setter, ConvertToBool); + } + + /// + /// For internal use only. + /// + /// + /// + /// + /// + /// + public static EventCallback CreateBinder( + this EventCallbackFactory factory, + object receiver, + Action setter, + bool? existingValue) + { + return CreateBinderCore(factory, receiver, setter, ConvertToNullableBool); + } + + /// + /// For internal use only. + /// + /// + /// + /// + /// + /// + public static EventCallback CreateBinder( + this EventCallbackFactory factory, + object receiver, + Action setter, + int existingValue) + { + return CreateBinderCore(factory, receiver, setter, ConvertToInt); + } + + /// + /// For internal use only. + /// + /// + /// + /// + /// + /// + public static EventCallback CreateBinder( + this EventCallbackFactory factory, + object receiver, + Action setter, + int? existingValue) + { + return CreateBinderCore(factory, receiver, setter, ConvertToNullableInt); + } + + /// + /// For internal use only. + /// + /// + /// + /// + /// + /// + public static EventCallback CreateBinder( + this EventCallbackFactory factory, + object receiver, + Action setter, + long existingValue) + { + return CreateBinderCore(factory, receiver, setter, ConvertToLong); + } + + /// + /// For internal use only. + /// + /// + /// + /// + /// + /// + public static EventCallback CreateBinder( + this EventCallbackFactory factory, + object receiver, + Action setter, + long? existingValue) + { + return CreateBinderCore(factory, receiver, setter, ConvertToNullableLong); + } + + /// + /// For internal use only. + /// + /// + /// + /// + /// + /// + public static EventCallback CreateBinder( + this EventCallbackFactory factory, + object receiver, + Action setter, + float existingValue) + { + return CreateBinderCore(factory, receiver, setter, ConvertToFloat); + } + + /// + /// For internal use only. + /// + /// + /// + /// + /// + /// + public static EventCallback CreateBinder( + this EventCallbackFactory factory, + object receiver, + Action setter, + float? existingValue) + { + return CreateBinderCore(factory, receiver, setter, ConvertToNullableFloat); + } + + /// + /// For internal use only. + /// + /// + /// + /// + /// + /// + public static EventCallback CreateBinder( + this EventCallbackFactory factory, + object receiver, + Action setter, + double existingValue) + { + return CreateBinderCore(factory, receiver, setter, ConvertToDouble); + } + + /// + /// For internal use only. + /// + /// + /// + /// + /// + /// + public static EventCallback CreateBinder( + this EventCallbackFactory factory, + object receiver, + Action setter, + double? existingValue) + { + return CreateBinderCore(factory, receiver, setter, ConvertToNullableDouble); + } + + /// + /// For internal use only. + /// + /// + /// + /// + /// + /// + public static EventCallback CreateBinder( + this EventCallbackFactory factory, + object receiver, + Action setter, + decimal existingValue) + { + return CreateBinderCore(factory, receiver, setter, ConvertToDecimal); + } + + /// + /// For internal use only. + /// + /// + /// + /// + /// + /// + public static EventCallback CreateBinder( + this EventCallbackFactory factory, + object receiver, + Action setter, + decimal? existingValue) + { + Func converter = (obj) => + { + if (decimal.TryParse((string)obj, out var value)) + { + return value; + } + + return null; + }; + return CreateBinderCore(factory, receiver, setter, ConvertToNullableDecimal); + } + + /// + /// For internal use only. + /// + /// + /// + /// + /// + /// + public static EventCallback CreateBinder( + this EventCallbackFactory factory, + object receiver, + Action setter, + DateTime existingValue) + { + // Avoiding CreateBinderCore so we can avoid an extra allocating lambda + // when a format is used. + Action callback = (e) => + { + setter(ConvertDateTime(e.Value, format: null)); + }; + return factory.Create(receiver, callback); + } + + /// + /// For internal use only. + /// + /// + /// + /// + /// + /// + /// + public static EventCallback CreateBinder( + this EventCallbackFactory factory, + object receiver, + Action setter, + DateTime existingValue, + string format) + { + // Avoiding CreateBinderCore so we can avoid an extra allocating lambda + // when a format is used. + Action callback = (e) => + { + setter(ConvertDateTime(e.Value, format)); + }; + return factory.Create(receiver, callback); + } + + /// + /// For internal use only. + /// + /// + /// + /// + /// + /// + /// + public static EventCallback CreateBinder( + this EventCallbackFactory factory, + object receiver, + Action setter, + T existingValue) where T : Enum + { + return CreateBinderCore(factory, receiver, setter, EnumConverter.Convert); + } + + private static DateTime ConvertDateTime(object obj, string format) + { + var text = (string)obj; + if (string.IsNullOrEmpty(text)) + { + return default; + } + else if (format != null && DateTime.TryParseExact(text, format, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var value)) + { + return value; + } + else + { + return DateTime.Parse(text); + } + } + + private static EventCallback CreateBinderCore( + this EventCallbackFactory factory, + object receiver, + Action setter, + Func converter) + { + Action callback = e => + { + setter(converter(e.Value)); + }; + return factory.Create(receiver, callback); + } + } +} diff --git a/src/Components/Components/src/EventCallbackFactoryUIEventArgsExtensions.cs b/src/Components/Components/src/EventCallbackFactoryUIEventArgsExtensions.cs new file mode 100644 index 0000000000..0e8151d56a --- /dev/null +++ b/src/Components/Components/src/EventCallbackFactoryUIEventArgsExtensions.cs @@ -0,0 +1,446 @@ +// 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; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Components +{ + /// + /// Provides extension methods for and types. For internal + /// framework use. + /// + public static class EventCallbackFactoryUIEventArgsExtensions + { + /// + /// Creates an for the provided and + /// . + /// + /// The . + /// The event receiver. + /// The event callback. + /// The . + public static EventCallback Create(this EventCallbackFactory factory, object receiver, Action callback) + { + if (factory == null) + { + throw new ArgumentNullException(nameof(factory)); + } + + return factory.Create(receiver, callback); + } + + /// + /// Creates an for the provided and + /// . + /// + /// The . + /// The event receiver. + /// The event callback. + /// The . + public static EventCallback Create(this EventCallbackFactory factory, object receiver, Func callback) + { + if (factory == null) + { + throw new ArgumentNullException(nameof(factory)); + } + + return factory.Create(receiver, callback); + } + + /// + /// Creates an for the provided and + /// . + /// + /// The . + /// The event receiver. + /// The event callback. + /// The . + public static EventCallback Create(this EventCallbackFactory factory, object receiver, Action callback) + { + if (factory == null) + { + throw new ArgumentNullException(nameof(factory)); + } + + return factory.Create(receiver, callback); + } + + /// + /// Creates an for the provided and + /// . + /// + /// The . + /// The event receiver. + /// The event callback. + /// The . + public static EventCallback Create(this EventCallbackFactory factory, object receiver, Func callback) + { + if (factory == null) + { + throw new ArgumentNullException(nameof(factory)); + } + + return factory.Create(receiver, callback); + } + + /// + /// Creates an for the provided and + /// . + /// + /// The . + /// The event receiver. + /// The event callback. + /// The . + public static EventCallback Create(this EventCallbackFactory factory, object receiver, Action callback) + { + if (factory == null) + { + throw new ArgumentNullException(nameof(factory)); + } + + return factory.Create(receiver, callback); + } + + /// + /// Creates an for the provided and + /// . + /// + /// The . + /// The event receiver. + /// The event callback. + /// The . + public static EventCallback Create(this EventCallbackFactory factory, object receiver, Func callback) + { + if (factory == null) + { + throw new ArgumentNullException(nameof(factory)); + } + + return factory.Create(receiver, callback); + } + + /// + /// Creates an for the provided and + /// . + /// + /// The . + /// The event receiver. + /// The event callback. + /// The . + public static EventCallback Create(this EventCallbackFactory factory, object receiver, Action callback) + { + if (factory == null) + { + throw new ArgumentNullException(nameof(factory)); + } + + return factory.Create(receiver, callback); + } + + /// + /// Creates an for the provided and + /// . + /// + /// The . + /// The event receiver. + /// The event callback. + /// The . + public static EventCallback Create(this EventCallbackFactory factory, object receiver, Func callback) + { + if (factory == null) + { + throw new ArgumentNullException(nameof(factory)); + } + + return factory.Create(receiver, callback); + } + + /// + /// Creates an for the provided and + /// . + /// + /// The . + /// The event receiver. + /// The event callback. + /// The . + public static EventCallback Create(this EventCallbackFactory factory, object receiver, Action callback) + { + if (factory == null) + { + throw new ArgumentNullException(nameof(factory)); + } + + return factory.Create(receiver, callback); + } + + /// + /// Creates an for the provided and + /// . + /// + /// The . + /// The event receiver. + /// The event callback. + /// The . + public static EventCallback Create(this EventCallbackFactory factory, object receiver, Func callback) + { + if (factory == null) + { + throw new ArgumentNullException(nameof(factory)); + } + + return factory.Create(receiver, callback); + } + + /// + /// Creates an for the provided and + /// . + /// + /// The . + /// The event receiver. + /// The event callback. + /// The . + public static EventCallback Create(this EventCallbackFactory factory, object receiver, Action callback) + { + if (factory == null) + { + throw new ArgumentNullException(nameof(factory)); + } + + return factory.Create(receiver, callback); + } + + /// + /// Creates an for the provided and + /// . + /// + /// The . + /// The event receiver. + /// The event callback. + /// The . + public static EventCallback Create(this EventCallbackFactory factory, object receiver, Func callback) + { + if (factory == null) + { + throw new ArgumentNullException(nameof(factory)); + } + + return factory.Create(receiver, callback); + } + + /// + /// Creates an for the provided and + /// . + /// + /// The . + /// The event receiver. + /// The event callback. + /// The . + public static EventCallback Create(this EventCallbackFactory factory, object receiver, Action callback) + { + if (factory == null) + { + throw new ArgumentNullException(nameof(factory)); + } + + return factory.Create(receiver, callback); + } + + /// + /// Creates an for the provided and + /// . + /// + /// The . + /// The event receiver. + /// The event callback. + /// The . + public static EventCallback Create(this EventCallbackFactory factory, object receiver, Func callback) + { + if (factory == null) + { + throw new ArgumentNullException(nameof(factory)); + } + + return factory.Create(receiver, callback); + } + + /// + /// Creates an for the provided and + /// . + /// + /// The . + /// The event receiver. + /// The event callback. + /// The . + public static EventCallback Create(this EventCallbackFactory factory, object receiver, Action callback) + { + if (factory == null) + { + throw new ArgumentNullException(nameof(factory)); + } + + return factory.Create(receiver, callback); + } + + /// + /// Creates an for the provided and + /// . + /// + /// The . + /// The event receiver. + /// The event callback. + /// The . + public static EventCallback Create(this EventCallbackFactory factory, object receiver, Func callback) + { + if (factory == null) + { + throw new ArgumentNullException(nameof(factory)); + } + + return factory.Create(receiver, callback); + } + /// + /// Creates an for the provided and + /// . + /// + /// The . + /// The event receiver. + /// The event callback. + /// The . + public static EventCallback Create(this EventCallbackFactory factory, object receiver, Action callback) + { + if (factory == null) + { + throw new ArgumentNullException(nameof(factory)); + } + + return factory.Create(receiver, callback); + } + + /// + /// Creates an for the provided and + /// . + /// + /// The . + /// The event receiver. + /// The event callback. + /// The . + public static EventCallback Create(this EventCallbackFactory factory, object receiver, Func callback) + { + if (factory == null) + { + throw new ArgumentNullException(nameof(factory)); + } + + return factory.Create(receiver, callback); + } + + /// + /// Creates an for the provided and + /// . + /// + /// The . + /// The event receiver. + /// The event callback. + /// The . + public static EventCallback Create(this EventCallbackFactory factory, object receiver, Action callback) + { + if (factory == null) + { + throw new ArgumentNullException(nameof(factory)); + } + + return factory.Create(receiver, callback); + } + + /// + /// Creates an for the provided and + /// . + /// + /// The . + /// The event receiver. + /// The event callback. + /// The . + public static EventCallback Create(this EventCallbackFactory factory, object receiver, Func callback) + { + if (factory == null) + { + throw new ArgumentNullException(nameof(factory)); + } + + return factory.Create(receiver, callback); + } + + /// + /// Creates an for the provided and + /// . + /// + /// The . + /// The event receiver. + /// The event callback. + /// The . + public static EventCallback Create(this EventCallbackFactory factory, object receiver, Action callback) + { + if (factory == null) + { + throw new ArgumentNullException(nameof(factory)); + } + + return factory.Create(receiver, callback); + } + + /// + /// Creates an for the provided and + /// . + /// + /// The . + /// The event receiver. + /// The event callback. + /// The . + public static EventCallback Create(this EventCallbackFactory factory, object receiver, Func callback) + { + if (factory == null) + { + throw new ArgumentNullException(nameof(factory)); + } + + return factory.Create(receiver, callback); + } + + /// + /// Creates an for the provided and + /// . + /// + /// The . + /// The event receiver. + /// The event callback. + /// The . + public static EventCallback Create(this EventCallbackFactory factory, object receiver, Action callback) + { + if (factory == null) + { + throw new ArgumentNullException(nameof(factory)); + } + + return factory.Create(receiver, callback); + } + + /// + /// Creates an for the provided and + /// . + /// + /// The . + /// The event receiver. + /// The event callback. + /// The . + public static EventCallback Create(this EventCallbackFactory factory, object receiver, Func callback) + { + if (factory == null) + { + throw new ArgumentNullException(nameof(factory)); + } + + return factory.Create(receiver, callback); + } + } +} diff --git a/src/Components/Components/src/EventCallbackWorkItem.cs b/src/Components/Components/src/EventCallbackWorkItem.cs new file mode 100644 index 0000000000..6a18069050 --- /dev/null +++ b/src/Components/Components/src/EventCallbackWorkItem.cs @@ -0,0 +1,79 @@ +// 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; +using System.Reflection; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Components +{ + /// + /// Wraps a callback delegate associated with an event. + /// + public struct EventCallbackWorkItem + { + /// + /// An empty . + /// + public static readonly EventCallbackWorkItem Empty = new EventCallbackWorkItem(null); + + private readonly MulticastDelegate _delegate; + + /// + /// Creates a new with the provided . + /// + /// The callback delegate. + public EventCallbackWorkItem(MulticastDelegate @delegate) + { + _delegate = @delegate; + } + + /// + /// Invokes the delegate associated with this . + /// + /// The argument to provide to the delegate. May be null. + /// A then will complete asynchronously once the delegate has completed. + public Task InvokeAsync(object arg) + { + return InvokeAsync(_delegate, arg); + } + + internal static Task InvokeAsync(MulticastDelegate @delegate, T arg) + { + switch (@delegate) + { + case null: + return Task.CompletedTask; + + case Action action: + action.Invoke(); + return Task.CompletedTask; + + case Action actionEventArgs: + actionEventArgs.Invoke(arg); + return Task.CompletedTask; + + case Func func: + return func.Invoke(); + + case Func funcEventArgs: + return funcEventArgs.Invoke(arg); + + default: + { + try + { + return @delegate.DynamicInvoke(arg) as Task ?? Task.CompletedTask; + } + catch (TargetInvocationException e) + { + // Since we fell into the DynamicInvoke case, any exception will be wrapped + // in a TIE. We can expect this to be thrown synchronously, so it's low overhead + // to unwrap it. + return Task.FromException(e.InnerException); + } + } + } + } + } +} diff --git a/src/Components/Components/src/EventHandlerInvoker.cs b/src/Components/Components/src/EventHandlerInvoker.cs deleted file mode 100644 index 9e65c66594..0000000000 --- a/src/Components/Components/src/EventHandlerInvoker.cs +++ /dev/null @@ -1,55 +0,0 @@ -// 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; -using System.Threading.Tasks; - -namespace Microsoft.AspNetCore.Components -{ - /// - /// A bound event handler delegate. - /// - public readonly struct EventHandlerInvoker - { - private readonly MulticastDelegate _delegate; - - /// - /// Creates the new . - /// - /// The delegate to bind. - public EventHandlerInvoker(MulticastDelegate @delegate) - { - _delegate = @delegate; - } - /// - /// Invokes the delegate associated with this binding. - /// - /// The . - /// - public Task Invoke(UIEventArgs e) - { - switch (_delegate) - { - case Action action: - action.Invoke(); - return Task.CompletedTask; - - case Action actionEventArgs: - actionEventArgs.Invoke(e); - return Task.CompletedTask; - - case Func func: - return func.Invoke(); - - case Func funcEventArgs: - return funcEventArgs.Invoke(e); - - case MulticastDelegate @delegate: - return @delegate.DynamicInvoke(e) as Task ?? Task.CompletedTask; - - case null: - return Task.CompletedTask; - } - } - } -} diff --git a/src/Components/Components/src/IHandleEvent.cs b/src/Components/Components/src/IHandleEvent.cs index 7fc50c6b1e..478102cbe8 100644 --- a/src/Components/Components/src/IHandleEvent.cs +++ b/src/Components/Components/src/IHandleEvent.cs @@ -6,16 +6,18 @@ using System.Threading.Tasks; namespace Microsoft.AspNetCore.Components { /// - /// Interface implemented by components that receive notification of their events. + /// Interface implemented by components that receive notification of state changes. /// public interface IHandleEvent { /// - /// Notifies the component that one of its event handlers has been triggered. + /// Notifies the a state change has been triggered. /// - /// The event binding. - /// Arguments for the event handler. - /// A that represents the asynchronous event handling operation. - Task HandleEventAsync(EventHandlerInvoker binding, UIEventArgs args); + /// The associated with this event. + /// The argument associated with this event. + /// + /// A that completes once the component has processed the state change. + /// + Task HandleEventAsync(EventCallbackWorkItem item, object arg); } } diff --git a/src/Components/Components/src/RenderTree/RenderTreeBuilder.cs b/src/Components/Components/src/RenderTree/RenderTreeBuilder.cs index f3e533f0e8..29ba31268c 100644 --- a/src/Components/Components/src/RenderTree/RenderTreeBuilder.cs +++ b/src/Components/Components/src/RenderTree/RenderTreeBuilder.cs @@ -277,6 +277,84 @@ namespace Microsoft.AspNetCore.Components.RenderTree } } + /// + /// + /// Appends a frame representing an attribute. + /// + /// + /// The attribute is associated with the most recently added element. If the value is null and the + /// current element is not a component, the frame will be omitted. + /// + /// + /// An integer that represents the position of the instruction in the source code. + /// The name of the attribute. + /// The value of the attribute. + /// + /// This method is provided for infrastructure purposes, and is used to support generated code + /// that uses . + /// + public void AddAttribute(int sequence, string name, EventCallback value) + { + AssertCanAddAttribute(); + if (_lastNonAttributeFrameType == RenderTreeFrameType.Component) + { + // Since this is a component, we need to preserve the type of the EventCallabck, so we have + // to box. + Append(RenderTreeFrame.Attribute(sequence, name, (object)value)); + } + else if (value.RequiresExplicitReceiver) + { + // If we need to preserve the receiver, we just box the EventCallback + // so we can get it out on the other side. + Append(RenderTreeFrame.Attribute(sequence, name, (object)value)); + } + else + { + // In the common case the receiver is also the delegate's target, so we + // just need to retain the delegate. This allows us to avoid an allocation. + Append(RenderTreeFrame.Attribute(sequence, name, value.Delegate)); + } + } + + /// + /// + /// Appends a frame representing an attribute. + /// + /// + /// The attribute is associated with the most recently added element. If the value is null and the + /// current element is not a component, the frame will be omitted. + /// + /// + /// An integer that represents the position of the instruction in the source code. + /// The name of the attribute. + /// The value of the attribute. + /// + /// This method is provided for infrastructure purposes, and is used to support generated code + /// that uses . + /// + public void AddAttribute(int sequence, string name, EventCallback value) + { + AssertCanAddAttribute(); + if (_lastNonAttributeFrameType == RenderTreeFrameType.Component) + { + // Since this is a component, we need to preserve the type of the EventCallback, so we have + // to box. + Append(RenderTreeFrame.Attribute(sequence, name, (object)value)); + } + else if (value.RequiresExplicitReceiver) + { + // If we need to preserve the receiver - we convert this to an untyped EventCallback. We don't + // need to preserve the type of an EventCallback when it's invoked from the DOM. + Append(RenderTreeFrame.Attribute(sequence, name, (object)value.AsUntyped())); + } + else + { + // In the common case the receiver is also the delegate's target, so we + // just need to retain the delegate. This allows us to avoid an allocation. + Append(RenderTreeFrame.Attribute(sequence, name, value.Delegate)); + } + } + /// /// Appends a frame representing a string-valued attribute. /// The attribute is associated with the most recently added element. If the value is null, or diff --git a/src/Components/Components/src/RenderTree/RenderTreeDiffBuilder.cs b/src/Components/Components/src/RenderTree/RenderTreeDiffBuilder.cs index 4e94c3d2fa..6a0514462d 100644 --- a/src/Components/Components/src/RenderTree/RenderTreeDiffBuilder.cs +++ b/src/Components/Components/src/RenderTree/RenderTreeDiffBuilder.cs @@ -652,7 +652,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree // // We're following a simple heuristic here that's reflected in the ts runtime // based on the common usage of attributes for DOM events. - if (newFrame.AttributeValue is MulticastDelegate && + if ((newFrame.AttributeValue is MulticastDelegate || newFrame.AttributeValue is EventCallback) && newFrame.AttributeName.Length >= 3 && newFrame.AttributeName.StartsWith("on")) { diff --git a/src/Components/Components/src/Rendering/ComponentState.cs b/src/Components/Components/src/Rendering/ComponentState.cs index 3145280c05..7aace974a0 100644 --- a/src/Components/Components/src/Rendering/ComponentState.cs +++ b/src/Components/Components/src/Rendering/ComponentState.cs @@ -93,20 +93,6 @@ namespace Microsoft.AspNetCore.Components.Rendering } } - public Task DispatchEventAsync(EventHandlerInvoker binding, UIEventArgs eventArgs) - { - if (Component is IHandleEvent handleEventComponent) - { - return handleEventComponent.HandleEventAsync(binding, eventArgs); - } - else - { - throw new InvalidOperationException( - $"The component of type {Component.GetType().FullName} cannot receive " + - $"events because it does not implement {typeof(IHandleEvent).FullName}."); - } - } - public Task NotifyRenderCompletedAsync() { if (Component is IHandleAfterRender handlerAfterRender) diff --git a/src/Components/Components/src/Rendering/Renderer.cs b/src/Components/Components/src/Rendering/Renderer.cs index 07c9a56181..bf64cdf4a3 100644 --- a/src/Components/Components/src/Rendering/Renderer.cs +++ b/src/Components/Components/src/Rendering/Renderer.cs @@ -19,7 +19,7 @@ namespace Microsoft.AspNetCore.Components.Rendering private readonly ComponentFactory _componentFactory; private readonly Dictionary _componentStateById = new Dictionary(); private readonly RenderBatchBuilder _batchBuilder = new RenderBatchBuilder(); - private readonly Dictionary _eventBindings = new Dictionary(); + private readonly Dictionary _eventBindings = new Dictionary(); private IDispatcher _dispatcher; private int _nextComponentId = 0; // TODO: change to 'long' when Mono .NET->JS interop supports it @@ -200,39 +200,44 @@ namespace Microsoft.AspNetCore.Components.Rendering protected abstract Task UpdateDisplayAsync(in RenderBatch renderBatch); /// - /// Notifies the specified component that an event has occurred. + /// Notifies the renderer that an event has occurred. /// - /// The unique identifier for the component within the scope of this . /// The value from the original event attribute. /// Arguments to be passed to the event handler. - /// A representing the asynchronous execution operation. - public Task DispatchEventAsync(int componentId, int eventHandlerId, UIEventArgs eventArgs) + /// + /// A which will complete once all asynchronous processing related to the event + /// has completed. + /// + public Task DispatchEventAsync(int eventHandlerId, UIEventArgs eventArgs) { EnsureSynchronizationContext(); - if (_eventBindings.TryGetValue(eventHandlerId, out var binding)) - { - // The event handler might request multiple renders in sequence. Capture them - // all in a single batch. - var componentState = GetRequiredComponentState(componentId); - Task task = null; - try - { - _isBatchInProgress = true; - task = componentState.DispatchEventAsync(binding, eventArgs); - } - finally - { - _isBatchInProgress = false; - ProcessRenderQueue(); - } - - return GetErrorHandledTask(task); - } - else + if (!_eventBindings.TryGetValue(eventHandlerId, out var callback)) { throw new ArgumentException($"There is no event handler with ID {eventHandlerId}"); } + + Task task = null; + try + { + // The event handler might request multiple renders in sequence. Capture them + // all in a single batch. + _isBatchInProgress = true; + + task = callback.InvokeAsync(eventArgs); + } + finally + { + _isBatchInProgress = false; + + // Since the task has yielded - process any queued rendering work before we return control + // to the caller. + ProcessRenderQueue(); + } + + // Task completed synchronously or is still running. We already processed all of the rendering + // work that was queued so let our error handler deal with it. + return GetErrorHandledTask(task); } /// @@ -338,10 +343,27 @@ namespace Microsoft.AspNetCore.Components.Rendering { var id = ++_lastEventHandlerId; - if (frame.AttributeValue is MulticastDelegate @delegate) + if (frame.AttributeValue is EventCallback callback) { - _eventBindings.Add(id, new EventHandlerInvoker(@delegate)); + // We hit this case when a EventCallback object is produced that needs an explicit receiver. + // Common cases for this are "chained bind" or "chained event handler" when a component + // accepts a delegate as a parameter and then hooks it up to a DOM event. + // + // When that happens we intentionally box the EventCallback because we need to hold on to + // the receiver. + _eventBindings.Add(id, callback); } + else if (frame.AttributeValue is MulticastDelegate @delegate) + { + // This is the common case for a delegate, where the receiver of the event + // is the same as delegate.Target. In this case since the receiver is implicit we can + // avoid boxing the EventCallback object and just re-hydrate it on the other side of the + // render tree. + _eventBindings.Add(id, new EventCallback(@delegate.Target as IHandleEvent, @delegate)); + } + + // NOTE: we do not to handle EventCallback here. EventCallback is only used when passing + // a callback to a component, and never when used to attaching a DOM event handler. frame = frame.WithAttributeEventHandlerId(id); } diff --git a/src/Components/Components/src/Rendering/RendererSynchronizationContext.cs b/src/Components/Components/src/Rendering/RendererSynchronizationContext.cs index a6a4e1aea1..0b8a90ae31 100644 --- a/src/Components/Components/src/Rendering/RendererSynchronizationContext.cs +++ b/src/Components/Components/src/Rendering/RendererSynchronizationContext.cs @@ -47,6 +47,10 @@ namespace Microsoft.AspNetCore.Components.Rendering action(); completion.SetResult(null); } + catch (OperationCanceledException) + { + completion.SetCanceled(); + } catch (Exception exception) { completion.SetException(exception); @@ -66,6 +70,10 @@ namespace Microsoft.AspNetCore.Components.Rendering await asyncAction(); completion.SetResult(null); } + catch (OperationCanceledException) + { + completion.SetCanceled(); + } catch (Exception exception) { completion.SetException(exception); @@ -85,6 +93,10 @@ namespace Microsoft.AspNetCore.Components.Rendering var result = function(); completion.SetResult(result); } + catch (OperationCanceledException) + { + completion.SetCanceled(); + } catch (Exception exception) { completion.SetException(exception); @@ -104,6 +116,10 @@ namespace Microsoft.AspNetCore.Components.Rendering var result = await asyncFunction(); completion.SetResult(result); } + catch (OperationCanceledException) + { + completion.SetCanceled(); + } catch (Exception exception) { completion.SetException(exception); diff --git a/src/Components/Components/src/UIEventArgs.cs b/src/Components/Components/src/UIEventArgs.cs index 994aad82fc..8115a2808b 100644 --- a/src/Components/Components/src/UIEventArgs.cs +++ b/src/Components/Components/src/UIEventArgs.cs @@ -8,6 +8,11 @@ namespace Microsoft.AspNetCore.Components /// public class UIEventArgs { + /// + /// An empty instance of . + /// + public static readonly UIEventArgs Empty = new UIEventArgs(); + /// /// Gets or sets the type of the event. /// diff --git a/src/Components/Components/test/EventCallbackFactoryBinderExtensionsTest.cs b/src/Components/Components/test/EventCallbackFactoryBinderExtensionsTest.cs new file mode 100644 index 0000000000..c8dd5fc4d3 --- /dev/null +++ b/src/Components/Components/test/EventCallbackFactoryBinderExtensionsTest.cs @@ -0,0 +1,358 @@ +// 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; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNetCore.Components +{ + public class EventCallbackFactoryBinderExtensionsTest + { + [Fact] + public async Task CreateBinder_ThrowsConversionException() + { + // Arrange + var value = 17; + var component = new EventCountingComponent(); + Action setter = (_) => value = _; + + var binder = EventCallback.Factory.CreateBinder(component, setter, value); + + // Act + await Assert.ThrowsAsync(() => + { + return binder.InvokeAsync(new UIChangeEventArgs() { Value = "not-an-integer!", }); + }); + + Assert.Equal(17, value); + Assert.Equal(1, component.Count); + } + + [Fact] + public async Task CreateBinder_String() + { + // Arrange + var value = "hi"; + var component = new EventCountingComponent(); + Action setter = (_) => value = _; + + var binder = EventCallback.Factory.CreateBinder(component, setter, value); + + var expectedValue = "bye"; + + // Act + await binder.InvokeAsync(new UIChangeEventArgs() { Value = expectedValue, }); + + Assert.Equal(expectedValue, value); + Assert.Equal(1, component.Count); + } + + [Fact] + public async Task CreateBinder_Bool() + { + // Arrange + var value = false; + var component = new EventCountingComponent(); + Action setter = (_) => value = _; + + var binder = EventCallback.Factory.CreateBinder(component, setter, value); + + var expectedValue = true; + + // Act + await binder.InvokeAsync(new UIChangeEventArgs() { Value = true, }); + + Assert.Equal(expectedValue, value); + Assert.Equal(1, component.Count); + } + + [Fact] + public async Task CreateBinder_NullableBool() + { + // Arrange + var value = (bool?)false; + var component = new EventCountingComponent(); + Action setter = (_) => value = _; + + var binder = EventCallback.Factory.CreateBinder(component, setter, value); + + var expectedValue = (bool?)true; + + // Act + await binder.InvokeAsync(new UIChangeEventArgs() { Value = true, }); + + Assert.Equal(expectedValue, value); + Assert.Equal(1, component.Count); + } + + [Fact] + public async Task CreateBinder_Int() + { + // Arrange + var value = 17; + var component = new EventCountingComponent(); + Action setter = (_) => value = _; + + var binder = EventCallback.Factory.CreateBinder(component, setter, value); + + var expectedValue = 42; + + // Act + await binder.InvokeAsync(new UIChangeEventArgs() { Value = "42", }); + + Assert.Equal(expectedValue, value); + Assert.Equal(1, component.Count); + } + + [Fact] + public async Task CreateBinder_NullableInt() + { + // Arrange + var value = (int?)17; + var component = new EventCountingComponent(); + Action setter = (_) => value = _; + + var binder = EventCallback.Factory.CreateBinder(component, setter, value); + + var expectedValue = (int?)42; + + // Act + await binder.InvokeAsync(new UIChangeEventArgs() { Value = "42", }); + + Assert.Equal(expectedValue, value); + Assert.Equal(1, component.Count); + } + + [Fact] + public async Task CreateBinder_Long() + { + // Arrange + var value = (long)17; + var component = new EventCountingComponent(); + Action setter = (_) => value = _; + + var binder = EventCallback.Factory.CreateBinder(component, setter, value); + + var expectedValue = (long)42; + + // Act + await binder.InvokeAsync(new UIChangeEventArgs() { Value = "42", }); + + Assert.Equal(expectedValue, value); + Assert.Equal(1, component.Count); + } + + [Fact] + public async Task CreateBinder_NullableLong() + { + // Arrange + var value = (long?)17; + var component = new EventCountingComponent(); + Action setter = (_) => value = _; + + var binder = EventCallback.Factory.CreateBinder(component, setter, value); + + var expectedValue = (long?)42; + + // Act + await binder.InvokeAsync(new UIChangeEventArgs() { Value = "42", }); + + Assert.Equal(expectedValue, value); + Assert.Equal(1, component.Count); + } + + [Fact] + public async Task CreateBinder_Float() + { + // Arrange + var value = (float)17; + var component = new EventCountingComponent(); + Action setter = (_) => value = _; + + var binder = EventCallback.Factory.CreateBinder(component, setter, value); + + var expectedValue = (float)42; + + // Act + await binder.InvokeAsync(new UIChangeEventArgs() { Value = "42", }); + + Assert.Equal(expectedValue, value); + Assert.Equal(1, component.Count); + } + + [Fact] + public async Task CreateBinder_NullableFloat() + { + // Arrange + var value = (float?)17; + var component = new EventCountingComponent(); + Action setter = (_) => value = _; + + var binder = EventCallback.Factory.CreateBinder(component, setter, value); + + var expectedValue = (float?)42; + + // Act + await binder.InvokeAsync(new UIChangeEventArgs() { Value = "42", }); + + Assert.Equal(expectedValue, value); + Assert.Equal(1, component.Count); + } + + [Fact] + public async Task CreateBinder_Double() + { + // Arrange + var value = (double)17; + var component = new EventCountingComponent(); + Action setter = (_) => value = _; + + var binder = EventCallback.Factory.CreateBinder(component, setter, value); + + var expectedValue = (double)42; + + // Act + await binder.InvokeAsync(new UIChangeEventArgs() { Value = "42", }); + + Assert.Equal(expectedValue, value); + Assert.Equal(1, component.Count); + } + + [Fact] + public async Task CreateBinder_NullableDouble() + { + // Arrange + var value = (double?)17; + var component = new EventCountingComponent(); + Action setter = (_) => value = _; + + var binder = EventCallback.Factory.CreateBinder(component, setter, value); + + var expectedValue = (double?)42; + + // Act + await binder.InvokeAsync(new UIChangeEventArgs() { Value = "42", }); + + Assert.Equal(expectedValue, value); + Assert.Equal(1, component.Count); + } + + [Fact] + public async Task CreateBinder_Decimal() + { + // Arrange + var value = (decimal)17; + var component = new EventCountingComponent(); + Action setter = (_) => value = _; + + var binder = EventCallback.Factory.CreateBinder(component, setter, value); + + var expectedValue = (decimal)42; + + // Act + await binder.InvokeAsync(new UIChangeEventArgs() { Value = "42", }); + + Assert.Equal(expectedValue, value); + Assert.Equal(1, component.Count); + } + + [Fact] + public async Task CreateBinder_NullableDecimal() + { + // Arrange + var value = (decimal?)17; + var component = new EventCountingComponent(); + Action setter = (_) => value = _; + + var binder = EventCallback.Factory.CreateBinder(component, setter, value); + + var expectedValue = (decimal?)42; + + // Act + await binder.InvokeAsync(new UIChangeEventArgs() { Value = "42", }); + + Assert.Equal(expectedValue, value); + Assert.Equal(1, component.Count); + } + + [Fact] + public async Task CreateBinder_Enum() + { + // Arrange + var value = AttributeTargets.All; + var component = new EventCountingComponent(); + Action setter = (_) => value = _; + + var binder = EventCallback.Factory.CreateBinder(component, setter, value); + + var expectedValue = AttributeTargets.Class; + + // Act + await binder.InvokeAsync(new UIChangeEventArgs() { Value = expectedValue.ToString(), }); + + Assert.Equal(expectedValue, value); + Assert.Equal(1, component.Count); + } + + [Fact] + public async Task CreateBinder_DateTime() + { + // Arrange + var value = DateTime.Now; + var component = new EventCountingComponent(); + Action setter = (_) => value = _; + + var binder = EventCallback.Factory.CreateBinder(component, setter, value); + + var expectedValue = new DateTime(2018, 3, 4, 1, 2, 3); + + // Act + await binder.InvokeAsync(new UIChangeEventArgs() { Value = expectedValue.ToString(), }); + + Assert.Equal(expectedValue, value); + Assert.Equal(1, component.Count); + } + + [Fact] + public async Task CreateBinder_DateTime_Format() + { + // Arrange + var value = DateTime.Now; + var component = new EventCountingComponent(); + Action setter = (_) => value = _; + var format = "ddd yyyy-MM-dd"; + + var binder = EventCallback.Factory.CreateBinder(component, setter, value, format); + + var expectedValue = new DateTime(2018, 3, 4); + + // Act + await binder.InvokeAsync(new UIChangeEventArgs() { Value = expectedValue.ToString(format), }); + + Assert.Equal(expectedValue, value); + Assert.Equal(1, component.Count); + } + + private class EventCountingComponent : IComponent, IHandleEvent + { + public int Count; + + public Task HandleEventAsync(EventCallbackWorkItem item, object arg) + { + Count++; + return item.InvokeAsync(arg); + } + + public void Configure(RenderHandle renderHandle) + { + throw new System.NotImplementedException(); + } + + public Task SetParametersAsync(ParameterCollection parameters) + { + throw new System.NotImplementedException(); + } + } + } +} diff --git a/src/Components/Components/test/EventCallbackFactoryTest.cs b/src/Components/Components/test/EventCallbackFactoryTest.cs new file mode 100644 index 0000000000..1b816ab232 --- /dev/null +++ b/src/Components/Components/test/EventCallbackFactoryTest.cs @@ -0,0 +1,464 @@ +// 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; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNetCore.Components +{ + public class EventCallbackFactoryTest + { + [Fact] + public void Create_Action_AlreadyBoundToReceiver() + { + // Arrange + var component = new EventComponent(); + var @delegate = (Action)component.SomeAction; + + // Act + var callback = EventCallback.Factory.Create(component, @delegate); + + // Assert + Assert.Same(@delegate, callback.Delegate); + Assert.Same(component, callback.Receiver); + Assert.False(callback.RequiresExplicitReceiver); + } + + [Fact] + public void Create_Action_DifferentReceiver() + { + // Arrange + var component = new EventComponent(); + var @delegate = (Action)component.SomeAction; + + var anotherComponent = new EventComponent(); + + // Act + var callback = EventCallback.Factory.Create(anotherComponent, @delegate); + + // Assert + Assert.Same(@delegate, callback.Delegate); + Assert.Same(anotherComponent, callback.Receiver); + Assert.True(callback.RequiresExplicitReceiver); + } + + [Fact] + public void Create_Action_Unbound() + { + // Arrange + var component = new EventComponent(); + var @delegate = (Action)(() => { }); + + var anotherComponent = new EventComponent(); + + // Act + var callback = EventCallback.Factory.Create(anotherComponent, @delegate); + + // Assert + Assert.Same(@delegate, callback.Delegate); + Assert.Same(anotherComponent, callback.Receiver); + Assert.True(callback.RequiresExplicitReceiver); + } + + [Fact] + public void Create_ActionT_AlreadyBoundToReceiver() + { + // Arrange + var component = new EventComponent(); + var @delegate = (Action)component.SomeActionOfT; + + // Act + var callback = EventCallback.Factory.Create(component, @delegate); + + // Assert + Assert.Same(@delegate, callback.Delegate); + Assert.Same(component, callback.Receiver); + Assert.False(callback.RequiresExplicitReceiver); + } + + [Fact] + public void Create_ActionT_DifferentReceiver() + { + // Arrange + var component = new EventComponent(); + var @delegate = (Action)component.SomeActionOfT; + + var anotherComponent = new EventComponent(); + + // Act + var callback = EventCallback.Factory.Create(anotherComponent, @delegate); + + // Assert + Assert.Same(@delegate, callback.Delegate); + Assert.Same(anotherComponent, callback.Receiver); + Assert.True(callback.RequiresExplicitReceiver); + } + + [Fact] + public void Create_ActionT_Unbound() + { + // Arrange + var component = new EventComponent(); + var @delegate = (Action)((s) => { }); + + var anotherComponent = new EventComponent(); + + // Act + var callback = EventCallback.Factory.Create(anotherComponent, @delegate); + + // Assert + Assert.Same(@delegate, callback.Delegate); + Assert.Same(anotherComponent, callback.Receiver); + Assert.True(callback.RequiresExplicitReceiver); + } + + [Fact] + public void Create_FuncTask_AlreadyBoundToReceiver() + { + // Arrange + var component = new EventComponent(); + var @delegate = (Func)component.SomeFuncTask; + + // Act + var callback = EventCallback.Factory.Create(component, @delegate); + + // Assert + Assert.Same(@delegate, callback.Delegate); + Assert.Same(component, callback.Receiver); + Assert.False(callback.RequiresExplicitReceiver); + } + + [Fact] + public void Create_FuncTask_DifferentReceiver() + { + // Arrange + var component = new EventComponent(); + var @delegate = (Func)component.SomeFuncTask; + + var anotherComponent = new EventComponent(); + + // Act + var callback = EventCallback.Factory.Create(anotherComponent, @delegate); + + // Assert + Assert.Same(@delegate, callback.Delegate); + Assert.Same(anotherComponent, callback.Receiver); + Assert.True(callback.RequiresExplicitReceiver); + } + + [Fact] + public void Create_FuncTask_Unbound() + { + // Arrange + var component = new EventComponent(); + var @delegate = (Func)(() => Task.CompletedTask); + + var anotherComponent = new EventComponent(); + + // Act + var callback = EventCallback.Factory.Create(anotherComponent, @delegate); + + // Assert + Assert.Same(@delegate, callback.Delegate); + Assert.Same(anotherComponent, callback.Receiver); + Assert.True(callback.RequiresExplicitReceiver); + } + + [Fact] + public void Create_FuncTTask_AlreadyBoundToReceiver() + { + // Arrange + var component = new EventComponent(); + var @delegate = (Func)component.SomeFuncTTask; + + // Act + var callback = EventCallback.Factory.Create(component, @delegate); + + // Assert + Assert.Same(@delegate, callback.Delegate); + Assert.Same(component, callback.Receiver); + Assert.False(callback.RequiresExplicitReceiver); + } + + [Fact] + public void Create_FuncTTask_DifferentReceiver() + { + // Arrange + var component = new EventComponent(); + var @delegate = (Func)component.SomeFuncTTask; + + var anotherComponent = new EventComponent(); + + // Act + var callback = EventCallback.Factory.Create(anotherComponent, @delegate); + + // Assert + Assert.Same(@delegate, callback.Delegate); + Assert.Same(anotherComponent, callback.Receiver); + Assert.True(callback.RequiresExplicitReceiver); + } + + [Fact] + public void Create_FuncTTask_Unbound() + { + // Arrange + var component = new EventComponent(); + var @delegate = (Func)((s) => Task.CompletedTask); + + var anotherComponent = new EventComponent(); + + // Act + var callback = EventCallback.Factory.Create(anotherComponent, @delegate); + + // Assert + Assert.Same(@delegate, callback.Delegate); + Assert.Same(anotherComponent, callback.Receiver); + Assert.True(callback.RequiresExplicitReceiver); + } + + [Fact] + public void CreateT_Action_AlreadyBoundToReceiver() + { + // Arrange + var component = new EventComponent(); + var @delegate = (Action)component.SomeAction; + + // Act + var callback = EventCallback.Factory.Create(component, @delegate); + + // Assert + Assert.Same(@delegate, callback.Delegate); + Assert.Same(component, callback.Receiver); + Assert.False(callback.RequiresExplicitReceiver); + } + + [Fact] + public void CreateT_Action_DifferentReceiver() + { + // Arrange + var component = new EventComponent(); + var @delegate = (Action)component.SomeAction; + + var anotherComponent = new EventComponent(); + + // Act + var callback = EventCallback.Factory.Create(anotherComponent, @delegate); + + // Assert + Assert.Same(@delegate, callback.Delegate); + Assert.Same(anotherComponent, callback.Receiver); + Assert.True(callback.RequiresExplicitReceiver); + } + + [Fact] + public void CreateT_Action_Unbound() + { + // Arrange + var component = new EventComponent(); + var @delegate = (Action)(() => { }); + + var anotherComponent = new EventComponent(); + + // Act + var callback = EventCallback.Factory.Create(anotherComponent, @delegate); + + // Assert + Assert.Same(@delegate, callback.Delegate); + Assert.Same(anotherComponent, callback.Receiver); + Assert.True(callback.RequiresExplicitReceiver); + } + + [Fact] + public void CreateT_ActionT_AlreadyBoundToReceiver() + { + // Arrange + var component = new EventComponent(); + var @delegate = (Action)component.SomeActionOfT; + + // Act + var callback = EventCallback.Factory.Create(component, @delegate); + + // Assert + Assert.Same(@delegate, callback.Delegate); + Assert.Same(component, callback.Receiver); + Assert.False(callback.RequiresExplicitReceiver); + } + + [Fact] + public void CreateT_ActionT_DifferentReceiver() + { + // Arrange + var component = new EventComponent(); + var @delegate = (Action)component.SomeActionOfT; + + var anotherComponent = new EventComponent(); + + // Act + var callback = EventCallback.Factory.Create(anotherComponent, @delegate); + + // Assert + Assert.Same(@delegate, callback.Delegate); + Assert.Same(anotherComponent, callback.Receiver); + Assert.True(callback.RequiresExplicitReceiver); + } + + [Fact] + public void CreateT_ActionT_Unbound() + { + // Arrange + var component = new EventComponent(); + var @delegate = (Action)((s) => { }); + + var anotherComponent = new EventComponent(); + + // Act + var callback = EventCallback.Factory.Create(anotherComponent, @delegate); + + // Assert + Assert.Same(@delegate, callback.Delegate); + Assert.Same(anotherComponent, callback.Receiver); + Assert.True(callback.RequiresExplicitReceiver); + } + + [Fact] + public void CreateT_FuncTask_AlreadyBoundToReceiver() + { + // Arrange + var component = new EventComponent(); + var @delegate = (Func)component.SomeFuncTask; + + // Act + var callback = EventCallback.Factory.Create(component, @delegate); + + // Assert + Assert.Same(@delegate, callback.Delegate); + Assert.Same(component, callback.Receiver); + Assert.False(callback.RequiresExplicitReceiver); + } + + [Fact] + public void CreateT_FuncTask_DifferentReceiver() + { + // Arrange + var component = new EventComponent(); + var @delegate = (Func)component.SomeFuncTask; + + var anotherComponent = new EventComponent(); + + // Act + var callback = EventCallback.Factory.Create(anotherComponent, @delegate); + + // Assert + Assert.Same(@delegate, callback.Delegate); + Assert.Same(anotherComponent, callback.Receiver); + Assert.True(callback.RequiresExplicitReceiver); + } + + [Fact] + public void CreateT_FuncTask_Unbound() + { + // Arrange + var component = new EventComponent(); + var @delegate = (Func)(() => Task.CompletedTask); + + var anotherComponent = new EventComponent(); + + // Act + var callback = EventCallback.Factory.Create(anotherComponent, @delegate); + + // Assert + Assert.Same(@delegate, callback.Delegate); + Assert.Same(anotherComponent, callback.Receiver); + Assert.True(callback.RequiresExplicitReceiver); + } + + [Fact] + public void CreateT_FuncTTask_AlreadyBoundToReceiver() + { + // Arrange + var component = new EventComponent(); + var @delegate = (Func)component.SomeFuncTTask; + + // Act + var callback = EventCallback.Factory.Create(component, @delegate); + + // Assert + Assert.Same(@delegate, callback.Delegate); + Assert.Same(component, callback.Receiver); + Assert.False(callback.RequiresExplicitReceiver); + } + + [Fact] + public void CreateT_FuncTTask_DifferentReceiver() + { + // Arrange + var component = new EventComponent(); + var @delegate = (Func)component.SomeFuncTTask; + + var anotherComponent = new EventComponent(); + + // Act + var callback = EventCallback.Factory.Create(anotherComponent, @delegate); + + // Assert + Assert.Same(@delegate, callback.Delegate); + Assert.Same(anotherComponent, callback.Receiver); + Assert.True(callback.RequiresExplicitReceiver); + } + + [Fact] + public void CreateT_FuncTTask_Unbound() + { + // Arrange + var component = new EventComponent(); + var @delegate = (Func)((s) => Task.CompletedTask); + + var anotherComponent = new EventComponent(); + + // Act + var callback = EventCallback.Factory.Create(anotherComponent, @delegate); + + // Assert + Assert.Same(@delegate, callback.Delegate); + Assert.Same(anotherComponent, callback.Receiver); + Assert.True(callback.RequiresExplicitReceiver); + } + + private class EventComponent : IComponent, IHandleEvent + { + public void SomeAction() + { + } + + public void SomeActionOfT(string e) + { + } + + public Task SomeFuncTask() + { + return Task.CompletedTask; + } + + public Task SomeFuncTTask(string s) + { + return Task.CompletedTask; + } + + public void Configure(RenderHandle renderHandle) + { + throw new NotImplementedException(); + } + + public Task HandleEventAsync(EventCallbackWorkItem item, object arg) + { + throw new NotImplementedException(); + } + + public Task SetParametersAsync(ParameterCollection parameters) + { + throw new NotImplementedException(); + } + } + } +} diff --git a/src/Components/Components/test/EventCallbackTest.cs b/src/Components/Components/test/EventCallbackTest.cs new file mode 100644 index 0000000000..37d6b354ff --- /dev/null +++ b/src/Components/Components/test/EventCallbackTest.cs @@ -0,0 +1,457 @@ +// 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; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNetCore.Components +{ + public class EventCallbackTest + { + [Fact] + public async Task EventCallback_Default() + { + // Arrange + var callback = default(EventCallback); + + // Act & Assert (Does not throw) + await callback.InvokeAsync(null); + } + + [Fact] + public async Task EventCallbackOfT_Default() + { + // Arrange + var callback = default(EventCallback); + + // Act & Assert (Does not throw) + await callback.InvokeAsync(null); + } + + + [Fact] + public async Task EventCallback_NullReceiver() + { + // Arrange + int runCount = 0; + var callback = new EventCallback(null, (Action)(() => runCount++)); + + // Act + await callback.InvokeAsync(null); + + + // Assert + Assert.Equal(1, runCount); + } + + [Fact] + public async Task EventCallbackOfT_NullReceiver() + { + // Arrange + int runCount = 0; + var callback = new EventCallback(null, (Action)(() => runCount++)); + + // Act + await callback.InvokeAsync(null); + + + // Assert + Assert.Equal(1, runCount); + } + + [Fact] + public async Task EventCallback_Action_Null() + { + // Arrange + var component = new EventCountingComponent(); + + int runCount = 0; + var callback = new EventCallback(component, (Action)(() => runCount++)); + + // Act + await callback.InvokeAsync(null); + + + // Assert + Assert.Equal(1, runCount); + Assert.Equal(1, component.Count); + } + + [Fact] + public async Task EventCallback_Action_IgnoresArg() + { + // Arrange + var component = new EventCountingComponent(); + + int runCount = 0; + var callback = new EventCallback(component, (Action)(() => runCount++)); + + // Act + await callback.InvokeAsync(new UIEventArgs()); + + + // Assert + Assert.Equal(1, runCount); + Assert.Equal(1, component.Count); + } + + [Fact] + public async Task EventCallback_ActionT_Null() + { + // Arrange + var component = new EventCountingComponent(); + + int runCount = 0; + UIEventArgs arg = null; + var callback = new EventCallback(component, (Action)((e) => { arg = e; runCount++; })); + + // Act + await callback.InvokeAsync(null); + + + // Assert + Assert.Null(arg); + Assert.Equal(1, runCount); + Assert.Equal(1, component.Count); + } + + [Fact] + public async Task EventCallback_ActionT_Arg() + { + // Arrange + var component = new EventCountingComponent(); + + int runCount = 0; + UIEventArgs arg = null; + var callback = new EventCallback(component, (Action)((e) => { arg = e; runCount++; })); + + // Act + await callback.InvokeAsync(new UIEventArgs()); + + + // Assert + Assert.NotNull(arg); + Assert.Equal(1, runCount); + Assert.Equal(1, component.Count); + } + + [Fact] + public async Task EventCallback_ActionT_Arg_ValueType() + { + // Arrange + var component = new EventCountingComponent(); + + int runCount = 0; + int arg = -1; + var callback = new EventCallback(component, (Action)((e) => { arg = e; runCount++; })); + + // Act + await callback.InvokeAsync(17); + + + // Assert + Assert.Equal(17, arg); + Assert.Equal(1, runCount); + Assert.Equal(1, component.Count); + } + + [Fact] + public async Task EventCallback_ActionT_ArgMismatch() + { + // Arrange + var component = new EventCountingComponent(); + + int runCount = 0; + UIEventArgs arg = null; + var callback = new EventCallback(component, (Action)((e) => { arg = e; runCount++; })); + + // Act & Assert + await Assert.ThrowsAsync(() => + { + return callback.InvokeAsync(new StringBuilder()); + }); + } + + [Fact] + public async Task EventCallback_FuncTask_Null() + { + // Arrange + var component = new EventCountingComponent(); + + int runCount = 0; + var callback = new EventCallback(component, (Func)(() => { runCount++; return Task.CompletedTask; })); + + // Act + await callback.InvokeAsync(null); + + + // Assert + Assert.Equal(1, runCount); + Assert.Equal(1, component.Count); + } + + [Fact] + public async Task EventCallback_FuncTask_IgnoresArg() + { + // Arrange + var component = new EventCountingComponent(); + + int runCount = 0; + var callback = new EventCallback(component, (Func)(() => { runCount++; return Task.CompletedTask; })); + + // Act + await callback.InvokeAsync(new UIEventArgs()); + + + // Assert + Assert.Equal(1, runCount); + Assert.Equal(1, component.Count); + } + + [Fact] + public async Task EventCallback_FuncTTask_Null() + { + // Arrange + var component = new EventCountingComponent(); + + int runCount = 0; + UIEventArgs arg = null; + var callback = new EventCallback(component, (Func)((e) => { arg = e; runCount++; return Task.CompletedTask; })); + + // Act + await callback.InvokeAsync(null); + + + // Assert + Assert.Null(arg); + Assert.Equal(1, runCount); + Assert.Equal(1, component.Count); + } + + [Fact] + public async Task EventCallback_FuncTTask_Arg() + { + // Arrange + var component = new EventCountingComponent(); + + int runCount = 0; + UIEventArgs arg = null; + var callback = new EventCallback(component, (Func)((e) => { arg = e; runCount++; return Task.CompletedTask; })); + + // Act + await callback.InvokeAsync(new UIEventArgs()); + + + // Assert + Assert.NotNull(arg); + Assert.Equal(1, runCount); + Assert.Equal(1, component.Count); + } + + [Fact] + public async Task EventCallback_FuncTTask_Arg_ValueType() + { + // Arrange + var component = new EventCountingComponent(); + + int runCount = 0; + int arg = -1; + var callback = new EventCallback(component, (Func)((e) => { arg = e; runCount++; return Task.CompletedTask; })); + + // Act + await callback.InvokeAsync(17); + + + // Assert + Assert.Equal(17, arg); + Assert.Equal(1, runCount); + Assert.Equal(1, component.Count); + } + + [Fact] + public async Task EventCallback_FuncTTask_ArgMismatch() + { + // Arrange + var component = new EventCountingComponent(); + + int runCount = 0; + UIEventArgs arg = null; + var callback = new EventCallback(component, (Func)((e) => { arg = e; runCount++; return Task.CompletedTask; })); + + // Act & Assert + await Assert.ThrowsAsync(() => + { + return callback.InvokeAsync(new StringBuilder()); + }); + } + + [Fact] + public async Task EventCallbackOfT_Action_Null() + { + // Arrange + var component = new EventCountingComponent(); + + int runCount = 0; + var callback = new EventCallback(component, (Action)(() => runCount++)); + + // Act + await callback.InvokeAsync(null); + + + // Assert + Assert.Equal(1, runCount); + Assert.Equal(1, component.Count); + } + + [Fact] + public async Task EventCallbackOfT_Action_IgnoresArg() + { + // Arrange + var component = new EventCountingComponent(); + + int runCount = 0; + var callback = new EventCallback(component, (Action)(() => runCount++)); + + // Act + await callback.InvokeAsync(new UIEventArgs()); + + + // Assert + Assert.Equal(1, runCount); + Assert.Equal(1, component.Count); + } + + [Fact] + public async Task EventCallbackOfT_ActionT_Null() + { + // Arrange + var component = new EventCountingComponent(); + + int runCount = 0; + UIEventArgs arg = null; + var callback = new EventCallback(component, (Action)((e) => { arg = e; runCount++; })); + + // Act + await callback.InvokeAsync(null); + + + // Assert + Assert.Null(arg); + Assert.Equal(1, runCount); + Assert.Equal(1, component.Count); + } + + [Fact] + public async Task EventCallbackOfT_ActionT_Arg() + { + // Arrange + var component = new EventCountingComponent(); + + int runCount = 0; + UIEventArgs arg = null; + var callback = new EventCallback(component, (Action)((e) => { arg = e; runCount++; })); + + // Act + await callback.InvokeAsync(new UIEventArgs()); + + + // Assert + Assert.NotNull(arg); + Assert.Equal(1, runCount); + Assert.Equal(1, component.Count); + } + + [Fact] + public async Task EventCallbackOfT_FuncTask_Null() + { + // Arrange + var component = new EventCountingComponent(); + + int runCount = 0; + var callback = new EventCallback(component, (Func)(() => { runCount++; return Task.CompletedTask; })); + + // Act + await callback.InvokeAsync(null); + + + // Assert + Assert.Equal(1, runCount); + Assert.Equal(1, component.Count); + } + + [Fact] + public async Task EventCallbackOfT_FuncTask_IgnoresArg() + { + // Arrange + var component = new EventCountingComponent(); + + int runCount = 0; + var callback = new EventCallback(component, (Func)(() => { runCount++; return Task.CompletedTask; })); + + // Act + await callback.InvokeAsync(new UIEventArgs()); + + + // Assert + Assert.Equal(1, runCount); + Assert.Equal(1, component.Count); + } + + [Fact] + public async Task EventCallbackOfT_FuncTTask_Null() + { + // Arrange + var component = new EventCountingComponent(); + + int runCount = 0; + UIEventArgs arg = null; + var callback = new EventCallback(component, (Func)((e) => { arg = e; runCount++; return Task.CompletedTask; })); + + // Act + await callback.InvokeAsync(null); + + + // Assert + Assert.Null(arg); + Assert.Equal(1, runCount); + Assert.Equal(1, component.Count); + } + + [Fact] + public async Task EventCallbackOfT_FuncTTask_Arg() + { + // Arrange + var component = new EventCountingComponent(); + + int runCount = 0; + UIEventArgs arg = null; + var callback = new EventCallback(component, (Func)((e) => { arg = e; runCount++; return Task.CompletedTask; })); + + // Act + await callback.InvokeAsync(new UIEventArgs()); + + + // Assert + Assert.NotNull(arg); + Assert.Equal(1, runCount); + Assert.Equal(1, component.Count); + } + + private class EventCountingComponent : IComponent, IHandleEvent + { + public int Count; + + public Task HandleEventAsync(EventCallbackWorkItem item, object arg) + { + Count++; + return item.InvokeAsync(arg); + } + + public void Configure(RenderHandle renderHandle) => throw new NotImplementedException(); + + public Task SetParametersAsync(ParameterCollection parameters) => throw new NotImplementedException(); + } + } +} diff --git a/src/Components/Components/test/Microsoft.AspNetCore.Components.Tests.csproj b/src/Components/Components/test/Microsoft.AspNetCore.Components.Tests.csproj index a18f393d83..fc08c77c6e 100644 --- a/src/Components/Components/test/Microsoft.AspNetCore.Components.Tests.csproj +++ b/src/Components/Components/test/Microsoft.AspNetCore.Components.Tests.csproj @@ -1,7 +1,8 @@ - + netcoreapp3.0 + Microsoft.AspNetCore.Components diff --git a/src/Components/Components/test/RendererTest.cs b/src/Components/Components/test/RendererTest.cs index c0cb1c45a9..0a170b5ca2 100644 --- a/src/Components/Components/test/RendererTest.cs +++ b/src/Components/Components/test/RendererTest.cs @@ -443,14 +443,14 @@ namespace Microsoft.AspNetCore.Components.Test // Act/Assert: Event can be fired var eventArgs = new UIEventArgs(); - var task = renderer.DispatchEventAsync(componentId, eventHandlerId, eventArgs); + var task = renderer.DispatchEventAsync(eventHandlerId, eventArgs); // This should always be run synchronously Assert.True(task.IsCompletedSuccessfully); } [Fact] - public void CanDispatchEventsToTopLevelComponents() + public async Task CanDispatchEventsToTopLevelComponents() { // Arrange: Render a component with an event handler var renderer = new TestRenderer(); @@ -473,14 +473,15 @@ namespace Microsoft.AspNetCore.Components.Test // Act/Assert: Event can be fired var eventArgs = new UIEventArgs(); - var renderTask = renderer.DispatchEventAsync(componentId, eventHandlerId, eventArgs); - + var renderTask = renderer.DispatchEventAsync(eventHandlerId, eventArgs); Assert.True(renderTask.IsCompletedSuccessfully); Assert.Same(eventArgs, receivedArgs); + + await renderTask; // Does not throw } [Fact] - public void CanDispatchTypedEventsToTopLevelComponents() + public async Task CanDispatchTypedEventsToTopLevelComponents() { // Arrange: Render a component with an event handler var renderer = new TestRenderer(); @@ -503,14 +504,15 @@ namespace Microsoft.AspNetCore.Components.Test // Act/Assert: Event can be fired var eventArgs = new UIMouseEventArgs(); - var renderTask = renderer.DispatchEventAsync(componentId, eventHandlerId, eventArgs); - + var renderTask = renderer.DispatchEventAsync(eventHandlerId, eventArgs); Assert.True(renderTask.IsCompletedSuccessfully); Assert.Same(eventArgs, receivedArgs); + + await renderTask; // does not throw } [Fact] - public void CanDispatchActionEventsToTopLevelComponents() + public async Task CanDispatchActionEventsToTopLevelComponents() { // Arrange: Render a component with an event handler var renderer = new TestRenderer(); @@ -533,14 +535,15 @@ namespace Microsoft.AspNetCore.Components.Test // Act/Assert: Event can be fired var eventArgs = new UIMouseEventArgs(); - var renderTask = renderer.DispatchEventAsync(componentId, eventHandlerId, eventArgs); - + var renderTask = renderer.DispatchEventAsync(eventHandlerId, eventArgs); Assert.True(renderTask.IsCompletedSuccessfully); Assert.NotNull(receivedArgs); + + await renderTask; // does not throw } [Fact] - public void CanDispatchEventsToNestedComponents() + public async Task CanDispatchEventsToNestedComponents() { UIEventArgs receivedArgs = null; @@ -574,25 +577,33 @@ namespace Microsoft.AspNetCore.Components.Test // Act/Assert: Event can be fired var eventArgs = new UIEventArgs(); - var renderTask = renderer.DispatchEventAsync(nestedComponentId, eventHandlerId, eventArgs); - + var renderTask = renderer.DispatchEventAsync(eventHandlerId, eventArgs); Assert.True(renderTask.IsCompletedSuccessfully); Assert.Same(eventArgs, receivedArgs); + + await renderTask; // does not throw } [Fact] - public void ThrowsIfComponentDoesNotHandleEvents() + public async Task CanAsyncDispatchEventsToTopLevelComponents() { // Arrange: Render a component with an event handler var renderer = new TestRenderer(); - Action handler = args => throw new NotImplementedException(); - var component = new TestComponent(builder => - { - builder.OpenElement(0, "mybutton"); - builder.AddAttribute(1, "onclick", handler); - builder.CloseElement(); - }); + UIEventArgs receivedArgs = null; + var state = 0; + var tcs = new TaskCompletionSource(); + + var component = new EventComponent + { + OnTestAsync = async (args) => + { + receivedArgs = args; + state = 1; + await tcs.Task; + state = 2; + }, + }; var componentId = renderer.AssignRootComponentId(component); component.TriggerRender(); @@ -600,29 +611,1140 @@ namespace Microsoft.AspNetCore.Components.Test .ReferenceFrames .First(frame => frame.AttributeValue != null) .AttributeEventHandlerId; - var eventArgs = new UIEventArgs(); - // Act/Assert - var ex = Assert.Throws(() => - { - // Verifies that the exception is thrown synchronously. - _ = renderer.DispatchEventAsync(componentId, eventHandlerId, eventArgs); - }); - Assert.Equal($"The component of type {typeof(TestComponent).FullName} cannot receive " + - $"events because it does not implement {typeof(IHandleEvent).FullName}.", ex.Message); + // Assert: Event not yet fired + Assert.Null(receivedArgs); + + // Act/Assert: Event can be fired + var eventArgs = new UIEventArgs(); + var task = renderer.DispatchEventAsync(eventHandlerId, eventArgs); + Assert.Equal(1, state); + Assert.Same(eventArgs, receivedArgs); + + tcs.SetResult(null); + await task; + + Assert.Equal(2, state); } [Fact] - public void CannotDispatchEventsToUnknownComponents() + public async Task CanAsyncDispatchTypedEventsToTopLevelComponents() + { + // Arrange: Render a component with an event handler + var renderer = new TestRenderer(); + UIMouseEventArgs receivedArgs = null; + + var state = 0; + var tcs = new TaskCompletionSource(); + + var component = new EventComponent + { + OnClickAsync = async (args) => + { + receivedArgs = args; + state = 1; + await tcs.Task; + state = 2; + } + }; + var componentId = renderer.AssignRootComponentId(component); + component.TriggerRender(); + + var eventHandlerId = renderer.Batches.Single() + .ReferenceFrames + .First(frame => frame.AttributeValue != null) + .AttributeEventHandlerId; + + // Assert: Event not yet fired + Assert.Null(receivedArgs); + + // Act/Assert: Event can be fired + var eventArgs = new UIMouseEventArgs(); + var task = renderer.DispatchEventAsync(eventHandlerId, eventArgs); + Assert.Equal(1, state); + Assert.Same(eventArgs, receivedArgs); + + tcs.SetResult(null); + await task; + + Assert.Equal(2, state); + } + + [Fact] + public async Task CanAsyncDispatchActionEventsToTopLevelComponents() + { + // Arrange: Render a component with an event handler + var renderer = new TestRenderer(); + object receivedArgs = null; + + var state = 0; + var tcs = new TaskCompletionSource(); + + var component = new EventComponent + { + OnClickAsyncAction = async () => + { + receivedArgs = new object(); + state = 1; + await tcs.Task; + state = 2; + } + }; + var componentId = renderer.AssignRootComponentId(component); + component.TriggerRender(); + + var eventHandlerId = renderer.Batches.Single() + .ReferenceFrames + .First(frame => frame.AttributeValue != null) + .AttributeEventHandlerId; + + // Assert: Event not yet fired + Assert.Null(receivedArgs); + + // Act/Assert: Event can be fired + var eventArgs = new UIMouseEventArgs(); + var task = renderer.DispatchEventAsync(eventHandlerId, eventArgs); + Assert.Equal(1, state); + Assert.NotNull(receivedArgs); + + tcs.SetResult(null); + await task; + + Assert.Equal(2, state); + } + + [Fact] + public async Task CanAsyncDispatchEventsToNestedComponents() + { + UIEventArgs receivedArgs = null; + + var state = 0; + var tcs = new TaskCompletionSource(); + + // Arrange: Render parent component + var renderer = new TestRenderer(); + var parentComponent = new TestComponent(builder => + { + builder.OpenComponent(0); + builder.CloseComponent(); + }); + var parentComponentId = renderer.AssignRootComponentId(parentComponent); + parentComponent.TriggerRender(); + + // Arrange: Render nested component + var nestedComponentFrame = renderer.Batches.Single() + .ReferenceFrames + .Single(frame => frame.FrameType == RenderTreeFrameType.Component); + var nestedComponent = (EventComponent)nestedComponentFrame.Component; + nestedComponent.OnTestAsync = async (args) => + { + receivedArgs = args; + state = 1; + await tcs.Task; + state = 2; + }; + var nestedComponentId = nestedComponentFrame.ComponentId; + nestedComponent.TriggerRender(); + + // Find nested component's event handler ID + var eventHandlerId = renderer.Batches[1] + .ReferenceFrames + .First(frame => frame.AttributeValue != null) + .AttributeEventHandlerId; + + // Assert: Event not yet fired + Assert.Null(receivedArgs); + + // Act/Assert: Event can be fired + var eventArgs = new UIEventArgs(); + var task = renderer.DispatchEventAsync(eventHandlerId, eventArgs); + Assert.Equal(1, state); + Assert.Same(eventArgs, receivedArgs); + + tcs.SetResult(null); + await task; + + Assert.Equal(2, state); + } + + // This tests the behaviour of dispatching an event when the event-handler + // delegate is a bound-delegate with a target that points to the parent component. + // + // This is a very common case when a component accepts a delegate parameter that + // will be hooked up to a DOM event handler. It's essential that this will dispatch + // to the parent component so that manual StateHasChanged calls are not necessary. + [Fact] + public async Task EventDispatching_DelegateParameter_MethodToDelegateConversion() + { + // Arrange + var outerStateChangeCount = 0; + + var renderer = new TestRenderer(); + var parentComponent = new OuterEventComponent(); + parentComponent.RenderFragment = (builder) => + { + builder.OpenComponent(0); + builder.AddAttribute(1, nameof(EventComponent.OnClickAction), parentComponent.SomeMethod); + builder.CloseComponent(); + }; + parentComponent.OnEvent = () => + { + outerStateChangeCount++; + }; + + var parentComponentId = renderer.AssignRootComponentId(parentComponent); + await parentComponent.TriggerRenderAsync(); + + var eventHandlerId = renderer.Batches[0] + .ReferenceFrames + .First(frame => frame.AttributeName == "onclickaction") + .AttributeEventHandlerId; + + // Act + var eventArgs = new UIMouseEventArgs(); + await renderer.DispatchEventAsync(eventHandlerId, eventArgs); + + // Assert + Assert.Equal(1, parentComponent.SomeMethodCallCount); + Assert.Equal(1, outerStateChangeCount); + } + + // This is the inverse case of EventDispatching_DelegateParameter_MethodToDelegateConversion + // where the event-handling delegate has a target that is not a component. + // + // This is a degenerate case that we don't expect to occur in applications often, + // but it's important to verify the semantics. + [Fact] + public async Task EventDispatching_DelegateParameter_NoTargetLambda() + { + // Arrange + var outerStateChangeCount = 0; + + var renderer = new TestRenderer(); + var parentComponent = new OuterEventComponent(); + parentComponent.RenderFragment = (builder) => + { + builder.OpenComponent(0); + builder.AddAttribute(1, nameof(EventComponent.OnClickAction), () => + { + parentComponent.SomeMethod(); + }); + builder.CloseComponent(); + }; + parentComponent.OnEvent = () => + { + outerStateChangeCount++; + }; + + var parentComponentId = renderer.AssignRootComponentId(parentComponent); + await parentComponent.TriggerRenderAsync(); + + var eventHandlerId = renderer.Batches[0] + .ReferenceFrames + .First(frame => frame.AttributeName == "onclickaction") + .AttributeEventHandlerId; + + // Act + var eventArgs = new UIMouseEventArgs(); + await renderer.DispatchEventAsync(eventHandlerId, eventArgs); + + // Assert + Assert.Equal(1, parentComponent.SomeMethodCallCount); + Assert.Equal(0, outerStateChangeCount); + } + + // This is a similar case to EventDispatching_DelegateParameter_MethodToDelegateConversion + // but uses our event handling infrastructure to achieve the same effect. The call to CreateDelegate + // is not necessary for correctness in this case - it should just no op. + [Fact] + public async Task EventDispatching_EventCallback_MethodToDelegateConversion() + { + // Arrange + var outerStateChangeCount = 0; + + var renderer = new TestRenderer(); + var parentComponent = new OuterEventComponent(); + parentComponent.RenderFragment = (builder) => + { + builder.OpenComponent(0); + builder.AddAttribute(1, nameof(EventComponent.OnClickEventCallback), EventCallback.Factory.Create(parentComponent, (Action)parentComponent.SomeMethod)); + builder.CloseComponent(); + }; + parentComponent.OnEvent = () => + { + outerStateChangeCount++; + }; + + var parentComponentId = renderer.AssignRootComponentId(parentComponent); + await parentComponent.TriggerRenderAsync(); + + var eventHandlerId = renderer.Batches[0] + .ReferenceFrames + .First(frame => frame.AttributeName == "onclick") + .AttributeEventHandlerId; + + // Act + var eventArgs = new UIMouseEventArgs(); + await renderer.DispatchEventAsync(eventHandlerId, eventArgs); + + // Assert + Assert.Equal(1, parentComponent.SomeMethodCallCount); + Assert.Equal(1, outerStateChangeCount); + } + + // This is a similar case to EventDispatching_DelegateParameter_NoTargetLambda but it uses + // our event-handling infrastructure to avoid the need for a manual StateHasChanged() + [Fact] + public async Task EventDispatching_EventCallback_NoTargetLambda() + { + // Arrange + var outerStateChangeCount = 0; + + var renderer = new TestRenderer(); + var parentComponent = new OuterEventComponent(); + parentComponent.RenderFragment = (builder) => + { + builder.OpenComponent(0); + builder.AddAttribute(1, nameof(EventComponent.OnClickEventCallback), EventCallback.Factory.Create(parentComponent, (Action)(() => + { + parentComponent.SomeMethod(); + }))); + builder.CloseComponent(); + }; + parentComponent.OnEvent = () => + { + outerStateChangeCount++; + }; + + var parentComponentId = renderer.AssignRootComponentId(parentComponent); + await parentComponent.TriggerRenderAsync(); + + var eventHandlerId = renderer.Batches[0] + .ReferenceFrames + .First(frame => frame.AttributeName == "onclick") + .AttributeEventHandlerId; + + // Act + var eventArgs = new UIMouseEventArgs(); + await renderer.DispatchEventAsync(eventHandlerId, eventArgs); + + // Assert + Assert.Equal(1, parentComponent.SomeMethodCallCount); + Assert.Equal(1, outerStateChangeCount); + } + + // This is a similar case to EventDispatching_DelegateParameter_NoTargetLambda but it uses + // our event-handling infrastructure to avoid the need for a manual StateHasChanged() + [Fact] + public async Task EventDispatching_EventCallback_AsyncNoTargetLambda() + { + // Arrange + var outerStateChangeCount = 0; + + var renderer = new TestRenderer(); + var parentComponent = new OuterEventComponent(); + parentComponent.RenderFragment = (builder) => + { + builder.OpenComponent(0); + builder.AddAttribute(1, nameof(EventComponent.OnClickEventCallback), EventCallback.Factory.Create(parentComponent, (Func)(() => + { + parentComponent.SomeMethod(); + return Task.CompletedTask; + }))); + builder.CloseComponent(); + }; + parentComponent.OnEvent = () => + { + outerStateChangeCount++; + }; + + var parentComponentId = renderer.AssignRootComponentId(parentComponent); + await parentComponent.TriggerRenderAsync(); + + var eventHandlerId = renderer.Batches[0] + .ReferenceFrames + .First(frame => frame.AttributeName == "onclick") + .AttributeEventHandlerId; + + // Act + var eventArgs = new UIMouseEventArgs(); + await renderer.DispatchEventAsync(eventHandlerId, eventArgs); + + // Assert + Assert.Equal(1, parentComponent.SomeMethodCallCount); + Assert.Equal(1, outerStateChangeCount); + } + + [Fact] + public async Task EventDispatching_EventCallbackOfT_MethodToDelegateConversion() + { + // Arrange + var outerStateChangeCount = 0; + + var renderer = new TestRenderer(); + var parentComponent = new OuterEventComponent(); + parentComponent.RenderFragment = (builder) => + { + builder.OpenComponent(0); + builder.AddAttribute(1, nameof(EventComponent.OnClickEventCallbackOfT), EventCallback.Factory.Create(parentComponent, (Action)parentComponent.SomeMethod)); + builder.CloseComponent(); + }; + parentComponent.OnEvent = () => + { + outerStateChangeCount++; + }; + + var parentComponentId = renderer.AssignRootComponentId(parentComponent); + await parentComponent.TriggerRenderAsync(); + + var eventHandlerId = renderer.Batches[0] + .ReferenceFrames + .First(frame => frame.AttributeName == "onclick") + .AttributeEventHandlerId; + + // Act + var eventArgs = new UIMouseEventArgs(); + await renderer.DispatchEventAsync(eventHandlerId, eventArgs); + + // Assert + Assert.Equal(1, parentComponent.SomeMethodCallCount); + Assert.Equal(1, outerStateChangeCount); + } + + // This is a similar case to EventDispatching_DelegateParameter_NoTargetLambda but it uses + // our event-handling infrastructure to avoid the need for a manual StateHasChanged() + [Fact] + public async Task EventDispatching_EventCallbackOfT_NoTargetLambda() + { + // Arrange + var outerStateChangeCount = 0; + + var renderer = new TestRenderer(); + var parentComponent = new OuterEventComponent(); + parentComponent.RenderFragment = (builder) => + { + builder.OpenComponent(0); + builder.AddAttribute(1, nameof(EventComponent.OnClickEventCallbackOfT), EventCallback.Factory.Create(parentComponent, (Action)(() => + { + parentComponent.SomeMethod(); + }))); + builder.CloseComponent(); + }; + parentComponent.OnEvent = () => + { + outerStateChangeCount++; + }; + + var parentComponentId = renderer.AssignRootComponentId(parentComponent); + await parentComponent.TriggerRenderAsync(); + + var eventHandlerId = renderer.Batches[0] + .ReferenceFrames + .First(frame => frame.AttributeName == "onclick") + .AttributeEventHandlerId; + + // Act + var eventArgs = new UIMouseEventArgs(); + await renderer.DispatchEventAsync(eventHandlerId, eventArgs); + + // Assert + Assert.Equal(1, parentComponent.SomeMethodCallCount); + Assert.Equal(1, outerStateChangeCount); + } + + // This is a similar case to EventDispatching_DelegateParameter_NoTargetLambda but it uses + // our event-handling infrastructure to avoid the need for a manual StateHasChanged() + [Fact] + public async Task EventDispatching_EventCallbackOfT_AsyncNoTargetLambda() + { + // Arrange + var outerStateChangeCount = 0; + + var renderer = new TestRenderer(); + var parentComponent = new OuterEventComponent(); + parentComponent.RenderFragment = (builder) => + { + builder.OpenComponent(0); + builder.AddAttribute(1, nameof(EventComponent.OnClickEventCallbackOfT), EventCallback.Factory.Create(parentComponent, (Func)(() => + { + parentComponent.SomeMethod(); + return Task.CompletedTask; + }))); + builder.CloseComponent(); + }; + parentComponent.OnEvent = () => + { + outerStateChangeCount++; + }; + + var parentComponentId = renderer.AssignRootComponentId(parentComponent); + await parentComponent.TriggerRenderAsync(); + + var eventHandlerId = renderer.Batches[0] + .ReferenceFrames + .First(frame => frame.AttributeName == "onclick") + .AttributeEventHandlerId; + + // Act + var eventArgs = new UIMouseEventArgs(); + await renderer.DispatchEventAsync(eventHandlerId, eventArgs); + + // Assert + Assert.Equal(1, parentComponent.SomeMethodCallCount); + Assert.Equal(1, outerStateChangeCount); + } + + [Fact] + public async Task DispatchEventAsync_Delegate_SynchronousCompletion() + { + // Arrange + var renderer = new TestRenderer(); + var parentComponent = new OuterEventComponent(); + parentComponent.RenderFragment = (builder) => + { + builder.OpenComponent(0); + builder.AddAttribute(1, nameof(EventComponent.OnClickAction), () => + { + // Do nothing. + }); + builder.CloseComponent(); + }; + + var parentComponentId = renderer.AssignRootComponentId(parentComponent); + await parentComponent.TriggerRenderAsync(); + + var eventHandlerId = renderer.Batches[0] + .ReferenceFrames + .First(frame => frame.AttributeName == "onclickaction") + .AttributeEventHandlerId; + + // Act + var task = renderer.DispatchEventAsync(eventHandlerId, new UIMouseEventArgs()); + + // Assert + Assert.Equal(TaskStatus.RanToCompletion, task.Status); + await task; // Does not throw + } + + [Fact] + public async Task DispatchEventAsync_EventCallback_SynchronousCompletion() + { + // Arrange + var renderer = new TestRenderer(); + var parentComponent = new OuterEventComponent(); + parentComponent.RenderFragment = (builder) => + { + builder.OpenComponent(0); + builder.AddAttribute(1, nameof(EventComponent.OnClickEventCallback), EventCallback.Factory.Create(parentComponent, (Action)(() => + { + // Do nothing. + }))); + builder.CloseComponent(); + }; + + var parentComponentId = renderer.AssignRootComponentId(parentComponent); + await parentComponent.TriggerRenderAsync(); + + var eventHandlerId = renderer.Batches[0] + .ReferenceFrames + .First(frame => frame.AttributeName == "onclick") + .AttributeEventHandlerId; + + // Act + var task = renderer.DispatchEventAsync(eventHandlerId, new UIMouseEventArgs()); + + // Assert + Assert.Equal(TaskStatus.RanToCompletion, task.Status); + await task; // Does not throw + } + + [Fact] + public async Task DispatchEventAsync_EventCallbackOfT_SynchronousCompletion() + { + // Arrange + UIMouseEventArgs arg = null; + + var renderer = new TestRenderer(); + var parentComponent = new OuterEventComponent(); + parentComponent.RenderFragment = (builder) => + { + builder.OpenComponent(0); + builder.AddAttribute(1, nameof(EventComponent.OnClickEventCallbackOfT), EventCallback.Factory.Create(parentComponent, (Action)((e) => + { + arg = e; + }))); + builder.CloseComponent(); + }; + + var parentComponentId = renderer.AssignRootComponentId(parentComponent); + await parentComponent.TriggerRenderAsync(); + + var eventHandlerId = renderer.Batches[0] + .ReferenceFrames + .First(frame => frame.AttributeName == "onclick") + .AttributeEventHandlerId; + + // Act + var task = renderer.DispatchEventAsync(eventHandlerId, new UIMouseEventArgs()); + + // Assert + Assert.NotNull(arg); + Assert.Equal(TaskStatus.RanToCompletion, task.Status); + await task; // Does not throw + } + + [Fact] + public async Task DispatchEventAsync_Delegate_SynchronousCancellation() + { + // Arrange + var renderer = new TestRenderer(); + var parentComponent = new OuterEventComponent(); + parentComponent.RenderFragment = (builder) => + { + builder.OpenComponent(0); + builder.AddAttribute(1, nameof(EventComponent.OnClickAction), (Action)(() => + { + throw new OperationCanceledException(); + })); + builder.CloseComponent(); + }; + + var parentComponentId = renderer.AssignRootComponentId(parentComponent); + await parentComponent.TriggerRenderAsync(); + + var eventHandlerId = renderer.Batches[0] + .ReferenceFrames + .First(frame => frame.AttributeName == "onclickaction") + .AttributeEventHandlerId; + + // Act + var task = renderer.DispatchEventAsync(eventHandlerId, new UIMouseEventArgs()); + + // Assert + Assert.Equal(TaskStatus.Canceled, task.Status); + await Assert.ThrowsAsync(() => task); + } + + [Fact] + public async Task DispatchEventAsync_EventCallback_SynchronousCancellation() + { + // Arrange + var renderer = new TestRenderer(); + var parentComponent = new OuterEventComponent(); + parentComponent.RenderFragment = (builder) => + { + builder.OpenComponent(0); + builder.AddAttribute(1, nameof(EventComponent.OnClickEventCallback), EventCallback.Factory.Create(parentComponent, (Action)(() => + { + throw new OperationCanceledException(); + }))); + builder.CloseComponent(); + }; + + var parentComponentId = renderer.AssignRootComponentId(parentComponent); + await parentComponent.TriggerRenderAsync(); + + var eventHandlerId = renderer.Batches[0] + .ReferenceFrames + .First(frame => frame.AttributeName == "onclick") + .AttributeEventHandlerId; + + // Act + var task = renderer.DispatchEventAsync(eventHandlerId, new UIMouseEventArgs()); + + // Assert + Assert.Equal(TaskStatus.Canceled, task.Status); + await Assert.ThrowsAsync(() => task); + } + + [Fact] + public async Task DispatchEventAsync_EventCallbackOfT_SynchronousCancellation() + { + // Arrange + UIMouseEventArgs arg = null; + + var renderer = new TestRenderer(); + var parentComponent = new OuterEventComponent(); + parentComponent.RenderFragment = (builder) => + { + builder.OpenComponent(0); + builder.AddAttribute(1, nameof(EventComponent.OnClickEventCallbackOfT), EventCallback.Factory.Create(parentComponent, (Action)((e) => + { + arg = e; + throw new OperationCanceledException(); + }))); + builder.CloseComponent(); + }; + + var parentComponentId = renderer.AssignRootComponentId(parentComponent); + await parentComponent.TriggerRenderAsync(); + + var eventHandlerId = renderer.Batches[0] + .ReferenceFrames + .First(frame => frame.AttributeName == "onclick") + .AttributeEventHandlerId; + + // Act + var task = renderer.DispatchEventAsync(eventHandlerId, new UIMouseEventArgs()); + + // Assert + Assert.NotNull(arg); + Assert.Equal(TaskStatus.Canceled, task.Status); + await Assert.ThrowsAsync(() => task); + } + + [Fact] + public async Task DispatchEventAsync_Delegate_SynchronousException() + { + // Arrange + var renderer = new TestRenderer(); + var parentComponent = new OuterEventComponent(); + parentComponent.RenderFragment = (builder) => + { + builder.OpenComponent(0); + builder.AddAttribute(1, nameof(EventComponent.OnClickAction), (Action)(() => + { + throw new InvalidTimeZoneException(); + })); + builder.CloseComponent(); + }; + + var parentComponentId = renderer.AssignRootComponentId(parentComponent); + await parentComponent.TriggerRenderAsync(); + + var eventHandlerId = renderer.Batches[0] + .ReferenceFrames + .First(frame => frame.AttributeName == "onclickaction") + .AttributeEventHandlerId; + + // Act + var task = renderer.DispatchEventAsync(eventHandlerId, new UIMouseEventArgs()); + + // Assert + Assert.Equal(TaskStatus.Faulted, task.Status); + await Assert.ThrowsAsync(() => task); + } + + [Fact] + public async Task DispatchEventAsync_EventCallback_SynchronousException() + { + // Arrange + var renderer = new TestRenderer(); + var parentComponent = new OuterEventComponent(); + parentComponent.RenderFragment = (builder) => + { + builder.OpenComponent(0); + builder.AddAttribute(1, nameof(EventComponent.OnClickEventCallback), EventCallback.Factory.Create(parentComponent, (Action)(() => + { + throw new InvalidTimeZoneException(); + }))); + builder.CloseComponent(); + }; + + var parentComponentId = renderer.AssignRootComponentId(parentComponent); + await parentComponent.TriggerRenderAsync(); + + var eventHandlerId = renderer.Batches[0] + .ReferenceFrames + .First(frame => frame.AttributeName == "onclick") + .AttributeEventHandlerId; + + // Act + var task = renderer.DispatchEventAsync(eventHandlerId, new UIMouseEventArgs()); + + // Assert + Assert.Equal(TaskStatus.Faulted, task.Status); + await Assert.ThrowsAsync(() => task); + } + + [Fact] + public async Task DispatchEventAsync_EventCallbackOfT_SynchronousException() + { + // Arrange + UIMouseEventArgs arg = null; + + var renderer = new TestRenderer(); + var parentComponent = new OuterEventComponent(); + parentComponent.RenderFragment = (builder) => + { + builder.OpenComponent(0); + builder.AddAttribute(1, nameof(EventComponent.OnClickEventCallbackOfT), EventCallback.Factory.Create(parentComponent, (Action)((e) => + { + arg = e; + throw new InvalidTimeZoneException(); + }))); + builder.CloseComponent(); + }; + + var parentComponentId = renderer.AssignRootComponentId(parentComponent); + await parentComponent.TriggerRenderAsync(); + + var eventHandlerId = renderer.Batches[0] + .ReferenceFrames + .First(frame => frame.AttributeName == "onclick") + .AttributeEventHandlerId; + + // Act + var task = renderer.DispatchEventAsync(eventHandlerId, new UIMouseEventArgs()); + + // Assert + Assert.NotNull(arg); + Assert.Equal(TaskStatus.Faulted, task.Status); + await Assert.ThrowsAsync(() => task); + } + + [Fact] + public async Task DispatchEventAsync_Delegate_AsynchronousCompletion() + { + // Arrange + var tcs = new TaskCompletionSource(); + + var renderer = new TestRenderer(); + var parentComponent = new OuterEventComponent(); + parentComponent.RenderFragment = (builder) => + { + builder.OpenComponent(0); + builder.AddAttribute(1, nameof(EventComponent.OnClickAsyncAction), async () => + { + await tcs.Task; + }); + builder.CloseComponent(); + }; + + var parentComponentId = renderer.AssignRootComponentId(parentComponent); + await parentComponent.TriggerRenderAsync(); + + var eventHandlerId = renderer.Batches[0] + .ReferenceFrames + .First(frame => frame.AttributeName == "onclickaction") + .AttributeEventHandlerId; + + // Act + var task = renderer.DispatchEventAsync(eventHandlerId, new UIMouseEventArgs()); + + // Assert + Assert.Equal(TaskStatus.WaitingForActivation, task.Status); + tcs.SetResult(null); + await task; // Does not throw + } + + [Fact] + public async Task DispatchEventAsync_EventCallback_AsynchronousCompletion() + { + // Arrange + var tcs = new TaskCompletionSource(); + + var renderer = new TestRenderer(); + var parentComponent = new OuterEventComponent(); + parentComponent.RenderFragment = (builder) => + { + builder.OpenComponent(0); + builder.AddAttribute(1, nameof(EventComponent.OnClickEventCallback), EventCallback.Factory.Create(parentComponent, async () => + { + await tcs.Task; + })); + builder.CloseComponent(); + }; + + var parentComponentId = renderer.AssignRootComponentId(parentComponent); + await parentComponent.TriggerRenderAsync(); + + var eventHandlerId = renderer.Batches[0] + .ReferenceFrames + .First(frame => frame.AttributeName == "onclick") + .AttributeEventHandlerId; + + // Act + var task = renderer.DispatchEventAsync(eventHandlerId, new UIMouseEventArgs()); + + // Assert + Assert.Equal(TaskStatus.WaitingForActivation, task.Status); + tcs.SetResult(null); + await task; // Does not throw + } + + [Fact] + public async Task DispatchEventAsync_EventCallbackOfT_AsynchronousCompletion() + { + // Arrange + var tcs = new TaskCompletionSource(); + + UIMouseEventArgs arg = null; + + var renderer = new TestRenderer(); + var parentComponent = new OuterEventComponent(); + parentComponent.RenderFragment = (builder) => + { + builder.OpenComponent(0); + builder.AddAttribute(1, nameof(EventComponent.OnClickEventCallbackOfT), EventCallback.Factory.Create(parentComponent, async (e) => + { + arg = e; + await tcs.Task; + })); + builder.CloseComponent(); + }; + + var parentComponentId = renderer.AssignRootComponentId(parentComponent); + await parentComponent.TriggerRenderAsync(); + + var eventHandlerId = renderer.Batches[0] + .ReferenceFrames + .First(frame => frame.AttributeName == "onclick") + .AttributeEventHandlerId; + + // Act + var task = renderer.DispatchEventAsync(eventHandlerId, new UIMouseEventArgs()); + + // Assert + Assert.NotNull(arg); + Assert.Equal(TaskStatus.WaitingForActivation, task.Status); + tcs.SetResult(null); + await task; // Does not throw + } + + [Fact] + public async Task DispatchEventAsync_Delegate_AsynchronousCancellation() + { + // Arrange + var tcs = new TaskCompletionSource(); + + var renderer = new TestRenderer(); + var parentComponent = new OuterEventComponent(); + parentComponent.RenderFragment = (builder) => + { + builder.OpenComponent(0); + builder.AddAttribute(1, nameof(EventComponent.OnClickAsyncAction), async () => + { + await tcs.Task; + throw new TaskCanceledException(); + }); + builder.CloseComponent(); + }; + + var parentComponentId = renderer.AssignRootComponentId(parentComponent); + await parentComponent.TriggerRenderAsync(); + + var eventHandlerId = renderer.Batches[0] + .ReferenceFrames + .First(frame => frame.AttributeName == "onclickaction") + .AttributeEventHandlerId; + + // Act + var task = renderer.DispatchEventAsync(eventHandlerId, new UIMouseEventArgs()); + + // Assert + Assert.Equal(TaskStatus.WaitingForActivation, task.Status); + tcs.SetResult(null); + + await task; // Does not throw + Assert.Empty(renderer.HandledExceptions); + } + + [Fact] + public async Task DispatchEventAsync_EventCallback_AsynchronousCancellation() + { + // Arrange + var tcs = new TaskCompletionSource(); + + var renderer = new TestRenderer(); + var parentComponent = new OuterEventComponent(); + parentComponent.RenderFragment = (builder) => + { + builder.OpenComponent(0); + builder.AddAttribute(1, nameof(EventComponent.OnClickEventCallback), EventCallback.Factory.Create(parentComponent, async () => + { + await tcs.Task; + throw new TaskCanceledException(); + })); + builder.CloseComponent(); + }; + + var parentComponentId = renderer.AssignRootComponentId(parentComponent); + await parentComponent.TriggerRenderAsync(); + + var eventHandlerId = renderer.Batches[0] + .ReferenceFrames + .First(frame => frame.AttributeName == "onclick") + .AttributeEventHandlerId; + + // Act + var task = renderer.DispatchEventAsync(eventHandlerId, new UIMouseEventArgs()); + + // Assert + Assert.Equal(TaskStatus.WaitingForActivation, task.Status); + tcs.SetResult(null); + + await task; // Does not throw + Assert.Empty(renderer.HandledExceptions); + } + + [Fact] + public async Task DispatchEventAsync_EventCallbackOfT_AsynchronousCancellation() + { + // Arrange + var tcs = new TaskCompletionSource(); + + UIMouseEventArgs arg = null; + + var renderer = new TestRenderer(); + var parentComponent = new OuterEventComponent(); + parentComponent.RenderFragment = (builder) => + { + builder.OpenComponent(0); + builder.AddAttribute(1, nameof(EventComponent.OnClickEventCallbackOfT), EventCallback.Factory.Create(parentComponent, async (e) => + { + arg = e; + await tcs.Task; + throw new TaskCanceledException(); + })); + builder.CloseComponent(); + }; + + var parentComponentId = renderer.AssignRootComponentId(parentComponent); + await parentComponent.TriggerRenderAsync(); + + var eventHandlerId = renderer.Batches[0] + .ReferenceFrames + .First(frame => frame.AttributeName == "onclick") + .AttributeEventHandlerId; + + // Act + var task = renderer.DispatchEventAsync(eventHandlerId, new UIMouseEventArgs()); + + // Assert + Assert.NotNull(arg); + Assert.Equal(TaskStatus.WaitingForActivation, task.Status); + tcs.SetResult(null); + + await task; // Does not throw + Assert.Empty(renderer.HandledExceptions); + } + + [Fact] + public async Task DispatchEventAsync_Delegate_AsynchronousException() + { + // Arrange + var tcs = new TaskCompletionSource(); + + var renderer = new TestRenderer(); + var parentComponent = new OuterEventComponent(); + parentComponent.RenderFragment = (builder) => + { + builder.OpenComponent(0); + builder.AddAttribute(1, nameof(EventComponent.OnClickAsyncAction), async () => + { + await tcs.Task; + throw new InvalidTimeZoneException(); + }); + builder.CloseComponent(); + }; + + var parentComponentId = renderer.AssignRootComponentId(parentComponent); + await parentComponent.TriggerRenderAsync(); + + var eventHandlerId = renderer.Batches[0] + .ReferenceFrames + .First(frame => frame.AttributeName == "onclickaction") + .AttributeEventHandlerId; + + // Act + var task = renderer.DispatchEventAsync(eventHandlerId, new UIMouseEventArgs()); + + // Assert + Assert.Equal(TaskStatus.WaitingForActivation, task.Status); + tcs.SetResult(null); + + await Assert.ThrowsAsync(() => task); + } + + [Fact] + public async Task DispatchEventAsync_EventCallback_AsynchronousException() + { + // Arrange + var tcs = new TaskCompletionSource(); + + var renderer = new TestRenderer(); + var parentComponent = new OuterEventComponent(); + parentComponent.RenderFragment = (builder) => + { + builder.OpenComponent(0); + builder.AddAttribute(1, nameof(EventComponent.OnClickEventCallback), EventCallback.Factory.Create(parentComponent, async () => + { + await tcs.Task; + throw new InvalidTimeZoneException(); + })); + builder.CloseComponent(); + }; + + var parentComponentId = renderer.AssignRootComponentId(parentComponent); + await parentComponent.TriggerRenderAsync(); + + var eventHandlerId = renderer.Batches[0] + .ReferenceFrames + .First(frame => frame.AttributeName == "onclick") + .AttributeEventHandlerId; + + // Act + var task = renderer.DispatchEventAsync(eventHandlerId, new UIMouseEventArgs()); + + // Assert + Assert.Equal(TaskStatus.WaitingForActivation, task.Status); + tcs.SetResult(null); + + await Assert.ThrowsAsync(() => task); + } + + [Fact] + public async Task DispatchEventAsync_EventCallbackOfT_AsynchronousException() + { + // Arrange + var tcs = new TaskCompletionSource(); + + UIMouseEventArgs arg = null; + + var renderer = new TestRenderer(); + var parentComponent = new OuterEventComponent(); + parentComponent.RenderFragment = (builder) => + { + builder.OpenComponent(0); + builder.AddAttribute(1, nameof(EventComponent.OnClickEventCallbackOfT), EventCallback.Factory.Create(parentComponent, async (e) => + { + arg = e; + await tcs.Task; + throw new InvalidTimeZoneException(); + })); + builder.CloseComponent(); + }; + + var parentComponentId = renderer.AssignRootComponentId(parentComponent); + await parentComponent.TriggerRenderAsync(); + + var eventHandlerId = renderer.Batches[0] + .ReferenceFrames + .First(frame => frame.AttributeName == "onclick") + .AttributeEventHandlerId; + + // Act + var task = renderer.DispatchEventAsync(eventHandlerId, new UIMouseEventArgs()); + + // Assert + Assert.NotNull(arg); + Assert.Equal(TaskStatus.WaitingForActivation, task.Status); + tcs.SetResult(null); + + await Assert.ThrowsAsync(() => task); + } + + [Fact] + public async Task CannotDispatchEventsWithUnknownEventHandlers() { // Arrange var renderer = new TestRenderer(); // Act/Assert - Assert.Throws(() => + await Assert.ThrowsAsync(() => { - // Intentionally written this way to verify that the exception is thrown synchronously. - _ = renderer.DispatchEventAsync(123, 0, new UIEventArgs()); + return renderer.DispatchEventAsync(0, new UIEventArgs()); }); } @@ -825,7 +1947,7 @@ namespace Microsoft.AspNetCore.Components.Test } [Fact] - public void DisposesEventHandlersWhenAttributeValueChanged() + public async Task DisposesEventHandlersWhenAttributeValueChanged() { // Arrange var renderer = new TestRenderer(); @@ -842,9 +1964,10 @@ namespace Microsoft.AspNetCore.Components.Test // Act/Assert 1: Event handler fires when we trigger it Assert.Equal(0, eventCount); - var renderTask = renderer.DispatchEventAsync(componentId, origEventHandlerId, args: null); + var renderTask = renderer.DispatchEventAsync(origEventHandlerId, args: null); Assert.True(renderTask.IsCompletedSuccessfully); Assert.Equal(1, eventCount); + await renderTask; // Now change the attribute value var newEventCount = 0; @@ -852,21 +1975,21 @@ namespace Microsoft.AspNetCore.Components.Test component.TriggerRender(); // Act/Assert 2: Can no longer fire the original event, but can fire the new event - Assert.Throws(() => + await Assert.ThrowsAsync(() => { - // Verifies that the exception is thrown synchronously. - _ = renderer.DispatchEventAsync(componentId, origEventHandlerId, args: null); + return renderer.DispatchEventAsync(origEventHandlerId, args: null); }); Assert.Equal(1, eventCount); Assert.Equal(0, newEventCount); - renderTask = renderer.DispatchEventAsync(componentId, origEventHandlerId + 1, args: null); + renderTask = renderer.DispatchEventAsync(origEventHandlerId + 1, args: null); Assert.True(renderTask.IsCompletedSuccessfully); Assert.Equal(1, newEventCount); + await renderTask; } [Fact] - public void DisposesEventHandlersWhenAttributeRemoved() + public async Task DisposesEventHandlersWhenAttributeRemoved() { // Arrange var renderer = new TestRenderer(); @@ -883,25 +2006,25 @@ namespace Microsoft.AspNetCore.Components.Test // Act/Assert 1: Event handler fires when we trigger it Assert.Equal(0, eventCount); - var renderTask = renderer.DispatchEventAsync(componentId, origEventHandlerId, args: null); + var renderTask = renderer.DispatchEventAsync(origEventHandlerId, args: null); Assert.True(renderTask.IsCompletedSuccessfully); Assert.Equal(1, eventCount); + await renderTask; // Now remove the event attribute component.OnTest = null; component.TriggerRender(); // Act/Assert 2: Can no longer fire the original event - Assert.Throws(() => + await Assert.ThrowsAsync(() => { - // Verifies that the exception is thrown synchronously. - _ = renderer.DispatchEventAsync(componentId, origEventHandlerId, args: null); + return renderer.DispatchEventAsync(origEventHandlerId, args: null); }); Assert.Equal(1, eventCount); } [Fact] - public void DisposesEventHandlersWhenOwnerComponentRemoved() + public async Task DisposesEventHandlersWhenOwnerComponentRemoved() { // Arrange var renderer = new TestRenderer(); @@ -934,25 +2057,25 @@ namespace Microsoft.AspNetCore.Components.Test // Act/Assert 1: Event handler fires when we trigger it Assert.Equal(0, eventCount); - var renderTask = renderer.DispatchEventAsync(childComponentId, eventHandlerId, args: null); + var renderTask = renderer.DispatchEventAsync(eventHandlerId, args: null); Assert.True(renderTask.IsCompletedSuccessfully); Assert.Equal(1, eventCount); + await renderTask; // Now remove the EventComponent component.IncludeChild = false; component.TriggerRender(); // Act/Assert 2: Can no longer fire the original event - Assert.Throws(() => + await Assert.ThrowsAsync(() => { - // Verifies that the exception is thrown synchronously. - _ = renderer.DispatchEventAsync(eventHandlerId, eventHandlerId, args: null); + return renderer.DispatchEventAsync(eventHandlerId, args: null); }); Assert.Equal(1, eventCount); } [Fact] - public void DisposesEventHandlersWhenAncestorElementRemoved() + public async Task DisposesEventHandlersWhenAncestorElementRemoved() { // Arrange var renderer = new TestRenderer(); @@ -969,25 +2092,25 @@ namespace Microsoft.AspNetCore.Components.Test // Act/Assert 1: Event handler fires when we trigger it Assert.Equal(0, eventCount); - var renderTask = renderer.DispatchEventAsync(componentId, origEventHandlerId, args: null); + var renderTask = renderer.DispatchEventAsync(origEventHandlerId, args: null); Assert.True(renderTask.IsCompletedSuccessfully); Assert.Equal(1, eventCount); + await renderTask; // Now remove the ancestor element component.SkipElement = true; component.TriggerRender(); // Act/Assert 2: Can no longer fire the original event - Assert.Throws(() => + await Assert.ThrowsAsync(() => { - // Verifies that the exception is thrown synchronously. - _ = renderer.DispatchEventAsync(componentId, origEventHandlerId, args: null); + return renderer.DispatchEventAsync(origEventHandlerId, args: null); }); Assert.Equal(1, eventCount); } [Fact] - public void AllRendersTriggeredSynchronouslyDuringEventHandlerAreHandledAsSingleBatch() + public async Task AllRendersTriggeredSynchronouslyDuringEventHandlerAreHandledAsSingleBatch() { // Arrange: A root component with a child whose event handler explicitly queues // a re-render of both the root component and the child @@ -1021,10 +2144,12 @@ namespace Microsoft.AspNetCore.Components.Test Assert.Single(renderer.Batches); // Act - var renderTask = renderer.DispatchEventAsync(childComponentId, origEventHandlerId, args: null); + var renderTask = renderer.DispatchEventAsync(origEventHandlerId, args: null); // Assert Assert.True(renderTask.IsCompletedSuccessfully); + await renderTask; + Assert.Equal(2, renderer.Batches.Count); var batch = renderer.Batches.Last(); Assert.Collection(batch.DiffsInOrder, @@ -1212,7 +2337,7 @@ namespace Microsoft.AspNetCore.Components.Test // Act // The fact that there's no error here is the main thing we're testing - var renderTask = renderer.DispatchEventAsync(childComponentId, origEventHandlerId, args: null); + var renderTask = renderer.DispatchEventAsync(origEventHandlerId, args: null); // Assert: correct render result Assert.True(renderTask.IsCompletedSuccessfully); @@ -1228,7 +2353,7 @@ namespace Microsoft.AspNetCore.Components.Test } [Fact] - public void CanCombineBindAndConditionalAttribute() + public async Task CanCombineBindAndConditionalAttribute() { // This test represents https://github.com/aspnet/Blazor/issues/624 @@ -1244,7 +2369,7 @@ namespace Microsoft.AspNetCore.Components.Test // Act: Toggle the checkbox var eventArgs = new UIChangeEventArgs { Value = true }; - var renderTask = renderer.DispatchEventAsync(componentId, checkboxChangeEventHandlerId, eventArgs); + var renderTask = renderer.DispatchEventAsync(checkboxChangeEventHandlerId, eventArgs); Assert.True(renderTask.IsCompletedSuccessfully); var latestBatch = renderer.Batches.Last(); @@ -1257,6 +2382,8 @@ namespace Microsoft.AspNetCore.Components.Test Assert.Contains(latestDiff.Edits, edit => edit.SiblingIndex == 1 && edit.RemovedAttributeName == "disabled"); + + await renderTask; } [Fact] @@ -1427,14 +2554,14 @@ namespace Microsoft.AspNetCore.Components.Test // Act/Assert 1: Event can be fired for the first time var render1TCS = new TaskCompletionSource(); renderer.NextUpdateDisplayReturnTask = render1TCS.Task; - await renderer.DispatchEventAsync(componentId, eventHandlerId, new UIEventArgs()); + await renderer.DispatchEventAsync(eventHandlerId, new UIEventArgs()); Assert.Equal(1, numEventsFired); // Act/Assert 2: *Same* event handler ID can be reused prior to completion of // preceding UI update var render2TCS = new TaskCompletionSource(); renderer.NextUpdateDisplayReturnTask = render2TCS.Task; - await renderer.DispatchEventAsync(componentId, eventHandlerId, new UIEventArgs()); + await renderer.DispatchEventAsync(eventHandlerId, new UIEventArgs()); Assert.Equal(2, numEventsFired); // Act/Assert 3: After we complete the first UI update in which a given @@ -1442,7 +2569,9 @@ namespace Microsoft.AspNetCore.Components.Test render1TCS.SetResult(null); await Task.Delay(500); // From here we can't see when the async disposal is completed. Just give it plenty of time (Task.Yield isn't enough). var ex = await Assert.ThrowsAsync(() => - renderer.DispatchEventAsync(componentId, eventHandlerId, new UIEventArgs())); + { + return renderer.DispatchEventAsync(eventHandlerId, new UIEventArgs()); + }); Assert.Equal($"There is no event handler with ID {eventHandlerId}", ex.Message); Assert.Equal(2, numEventsFired); } @@ -2034,12 +3163,27 @@ namespace Microsoft.AspNetCore.Components.Test [Parameter] internal Action OnTest { get; set; } + [Parameter] + internal Func OnTestAsync { get; set; } + [Parameter] internal Action OnClick { get; set; } + [Parameter] + internal Func OnClickAsync { get; set; } + [Parameter] internal Action OnClickAction { get; set; } + [Parameter] + internal Func OnClickAsyncAction { get; set; } + + [Parameter] + internal EventCallback OnClickEventCallback { get; set; } + + [Parameter] + internal EventCallback OnClickEventCallbackOfT { get; set; } + public bool SkipElement { get; set; } private int renderCount = 0; @@ -2050,18 +3194,41 @@ namespace Microsoft.AspNetCore.Components.Test { builder.OpenElement(1, "parent"); builder.OpenElement(2, "some element"); + if (OnTest != null) { builder.AddAttribute(3, "ontest", OnTest); } + else if (OnTestAsync != null) + { + builder.AddAttribute(3, "ontest", OnTestAsync); + } + if (OnClick != null) { builder.AddAttribute(4, "onclick", OnClick); } + else if (OnClickAsync != null) + { + builder.AddAttribute(4, "onclick", OnClickAsync); + } + else if (OnClickEventCallback.HasDelegate) + { + builder.AddAttribute(4, "onclick", OnClickEventCallback); + } + else if (OnClickEventCallbackOfT.HasDelegate) + { + builder.AddAttribute(4, "onclick", OnClickEventCallbackOfT); + } + if (OnClickAction != null) { builder.AddAttribute(5, "onclickaction", OnClickAction); } + else if (OnClickAsyncAction != null) + { + builder.AddAttribute(5, "onclickaction", OnClickAsyncAction); + } builder.CloseElement(); builder.CloseElement(); } @@ -2069,10 +3236,10 @@ namespace Microsoft.AspNetCore.Components.Test builder.AddContent(6, $"Render count: {++renderCount}"); } - public Task HandleEventAsync(EventHandlerInvoker binding, UIEventArgs args) + public Task HandleEventAsync(EventCallbackWorkItem callback, object arg) { - binding.Invoke(args); - return Task.CompletedTask; + // Notice, we don't re-render. + return callback.InvokeAsync(arg); } } @@ -2140,10 +3307,11 @@ namespace Microsoft.AspNetCore.Components.Test return Task.CompletedTask; } - public async Task HandleEventAsync(EventHandlerInvoker binding, UIEventArgs args) + public Task HandleEventAsync(EventCallbackWorkItem callback, object arg) { - await binding.Invoke(args); + var task = callback.InvokeAsync(arg); Render(); + return task; } private void Render() @@ -2185,11 +3353,11 @@ namespace Microsoft.AspNetCore.Components.Test public bool CheckboxEnabled; public string SomeStringProperty; - public Task HandleEventAsync(EventHandlerInvoker binding, UIEventArgs args) + public Task HandleEventAsync(EventCallbackWorkItem callback, object arg) { - binding.Invoke(args); + var task = callback.InvokeAsync(arg); TriggerRender(); - return Task.CompletedTask; + return task; } protected override void BuildRenderTree(RenderTreeBuilder builder) @@ -2298,6 +3466,41 @@ namespace Microsoft.AspNetCore.Components.Test } } + private class OuterEventComponent : IComponent, IHandleEvent + { + private RenderHandle _renderHandle; + + public RenderFragment RenderFragment { get; set; } + + public Action OnEvent { get; set; } + + public int SomeMethodCallCount { get; set; } + + public void SomeMethod() + { + SomeMethodCallCount++; + } + + public void Configure(RenderHandle renderHandle) + { + _renderHandle = renderHandle; + } + + public Task HandleEventAsync(EventCallbackWorkItem callback, object arg) + { + var task = callback.InvokeAsync(arg); + OnEvent?.Invoke(); + return task; + } + + public Task SetParametersAsync(ParameterCollection parameters) + { + return TriggerRenderAsync(); + } + + public Task TriggerRenderAsync() => _renderHandle.Invoke(() => _renderHandle.Render(RenderFragment)); + } + private void AssertStream(int expectedId, (int id, NestedAsyncComponent.EventType @event)[] logStream) { // OnInit runs first diff --git a/src/Components/Components/test/Rendering/RendererSynchronizationContextTests.cs b/src/Components/Components/test/Rendering/RendererSynchronizationContextTests.cs index cba0d236ef..153f362b0c 100644 --- a/src/Components/Components/test/Rendering/RendererSynchronizationContextTests.cs +++ b/src/Components/Components/test/Rendering/RendererSynchronizationContextTests.cs @@ -459,6 +459,23 @@ namespace Microsoft.AspNetCore.Components.Rendering await Assert.ThrowsAsync(async () => await task); } + [Fact] + public async Task Invoke_Void_CanReportCancellation() + { + // Arrange + var context = new RendererSynchronizationContext(); + + // Act + var task = context.Invoke(() => + { + throw new OperationCanceledException(); + }); + + // Assert + Assert.Equal(TaskStatus.Canceled, task.Status); + await Assert.ThrowsAsync(async () => await task); + } + [Fact] public async Task Invoke_T_CanRunSynchronously_WhenNotBusy() { @@ -530,6 +547,23 @@ namespace Microsoft.AspNetCore.Components.Rendering await Assert.ThrowsAsync(async () => await task); } + [Fact] + public async Task Invoke_T_CanReportCancellation() + { + // Arrange + var context = new RendererSynchronizationContext(); + + // Act + var task = context.Invoke(() => + { + throw new OperationCanceledException(); + }); + + // Assert + Assert.Equal(TaskStatus.Canceled, task.Status); + await Assert.ThrowsAsync(async () => await task); + } + [Fact] public async Task InvokeAsync_Void_CanRunSynchronously_WhenNotBusy() { @@ -607,6 +641,23 @@ namespace Microsoft.AspNetCore.Components.Rendering await Assert.ThrowsAsync(async () => await task); } + [Fact] + public async Task InvokeAsync_Void_CanReportCancellation() + { + // Arrange + var context = new RendererSynchronizationContext(); + + // Act + var task = context.InvokeAsync(() => + { + throw new OperationCanceledException(); + }); + + // Assert + Assert.Equal(TaskStatus.Canceled, task.Status); + await Assert.ThrowsAsync(async () => await task); + } + [Fact] public async Task InvokeAsync_T_CanRunSynchronously_WhenNotBusy() { @@ -677,5 +728,22 @@ namespace Microsoft.AspNetCore.Components.Rendering // Assert await Assert.ThrowsAsync(async () => await task); } + + [Fact] + public async Task InvokeAsync_T_CanReportCancellation() + { + // Arrange + var context = new RendererSynchronizationContext(); + + // Act + var task = context.InvokeAsync(() => + { + throw new OperationCanceledException(); + }); + + // Assert + Assert.Equal(TaskStatus.Canceled, task.Status); + await Assert.ThrowsAsync(async () => await task); + } } } diff --git a/src/Components/Shared/test/TestRenderer.cs b/src/Components/Shared/test/TestRenderer.cs index 1caaf260c4..e9ea32b29e 100644 --- a/src/Components/Shared/test/TestRenderer.cs +++ b/src/Components/Shared/test/TestRenderer.cs @@ -49,11 +49,8 @@ namespace Microsoft.AspNetCore.Components.Test.Helpers public new Task RenderRootComponentAsync(int componentId, ParameterCollection parameters) => InvokeAsync(() => base.RenderRootComponentAsync(componentId, parameters)); - public new Task DispatchEventAsync(int componentId, int eventHandlerId, UIEventArgs args) - { - var task = InvokeAsync(() => base.DispatchEventAsync(componentId, eventHandlerId, args)); - return UnwrapTask(task); - } + public new Task DispatchEventAsync(int eventHandlerId, UIEventArgs args) + => InvokeAsync(() => base.DispatchEventAsync(eventHandlerId, args)); private static Task UnwrapTask(Task task) { diff --git a/src/Components/test/E2ETest/Infrastructure/SeleniumStandaloneServer.cs b/src/Components/test/E2ETest/Infrastructure/SeleniumStandaloneServer.cs index ed59345b52..46c88f103f 100644 --- a/src/Components/test/E2ETest/Infrastructure/SeleniumStandaloneServer.cs +++ b/src/Components/test/E2ETest/Infrastructure/SeleniumStandaloneServer.cs @@ -107,13 +107,16 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Infrastructure { } - await Task.Delay(1000); + await Task.Delay(1000); } }); try { - waitForStart.TimeoutAfter(Timeout).Wait(1000); + // Wait in intervals instead of indefinitely to prevent thread starvation. + while (!waitForStart.TimeoutAfter(Timeout).Wait(1000)) + { + } } catch (Exception ex) { diff --git a/src/Components/test/E2ETest/Tests/CascadingValueTest.cs b/src/Components/test/E2ETest/Tests/CascadingValueTest.cs index d385fb662e..115bb30f5c 100644 --- a/src/Components/test/E2ETest/Tests/CascadingValueTest.cs +++ b/src/Components/test/E2ETest/Tests/CascadingValueTest.cs @@ -77,10 +77,8 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests decrementButton.Click(); WaitAssert.Equal("98", () => currentCount.Text); - // Renders the descendant the same number of times we triggered - // events on it, because we always re-render components after they - // have an event - Assert.Equal("3", Browser.FindElement(By.Id("receive-by-interface-num-renders")).Text); + // Didn't re-render descendants + Assert.Equal("1", Browser.FindElement(By.Id("receive-by-interface-num-renders")).Text); } } } diff --git a/src/Components/test/E2ETest/Tests/EventCallbackTest.cs b/src/Components/test/E2ETest/Tests/EventCallbackTest.cs new file mode 100644 index 0000000000..43ffba6e92 --- /dev/null +++ b/src/Components/test/E2ETest/Tests/EventCallbackTest.cs @@ -0,0 +1,42 @@ +// 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 BasicTestApp; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; +using OpenQA.Selenium; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.AspNetCore.Components.E2ETest.Tests +{ + public class EventCallbackTest : BasicTestAppTestBase + { + public EventCallbackTest( + BrowserFixture browserFixture, + ToggleExecutionModeServerFixture serverFixture, + ITestOutputHelper output) + : base(browserFixture, serverFixture, output) + { + // On WebAssembly, page reloads are expensive so skip if possible + Navigate(ServerPathBase, noReload: !serverFixture.UsingAspNetHost); + MountTestComponent(); + } + + [Theory] + [InlineData("capturing_lambda")] + [InlineData("unbound_lambda")] + [InlineData("unbound_lambda_nested")] + [InlineData("unbound_lambda_strongly_typed")] + [InlineData("unbound_lambda_child_content")] + [InlineData("unbound_lambda_bind_to_component")] + public void EventCallback_RerendersOuterComponent(string @case) + { + var target = Browser.FindElement(By.CssSelector($"#{@case} button")); + var count = Browser.FindElement(By.Id("render_count")); + Assert.Equal("Render Count: 1", count.Text); + target.Click(); + Assert.Equal("Render Count: 2", count.Text); + } + } +} diff --git a/src/Components/test/testassets/BasicTestApp/CascadingValueTest/CascadingValueSupplier.cshtml b/src/Components/test/testassets/BasicTestApp/CascadingValueTest/CascadingValueSupplier.cshtml index a772b949dd..f1f70cfba0 100644 --- a/src/Components/test/testassets/BasicTestApp/CascadingValueTest/CascadingValueSupplier.cshtml +++ b/src/Components/test/testassets/BasicTestApp/CascadingValueTest/CascadingValueSupplier.cshtml @@ -16,7 +16,7 @@ -

+

diff --git a/src/Components/test/testassets/BasicTestApp/EventCallbackTest/ButtonComponent.cshtml b/src/Components/test/testassets/BasicTestApp/EventCallbackTest/ButtonComponent.cshtml new file mode 100644 index 0000000000..13d814bbe3 --- /dev/null +++ b/src/Components/test/testassets/BasicTestApp/EventCallbackTest/ButtonComponent.cshtml @@ -0,0 +1,14 @@ + + + +@functions { + [Parameter] int Count { get; set; } + [Parameter] EventCallback CountChanged { get; set; } + [Parameter] string Text { get; set; } + + Task OnClick(UIMouseEventArgs e) + { + Count++; + return CountChanged.InvokeAsync(Count); + } +} diff --git a/src/Components/test/testassets/BasicTestApp/EventCallbackTest/EventCallbackCases.cshtml b/src/Components/test/testassets/BasicTestApp/EventCallbackTest/EventCallbackCases.cshtml new file mode 100644 index 0000000000..2324f8d0d8 --- /dev/null +++ b/src/Components/test/testassets/BasicTestApp/EventCallbackTest/EventCallbackCases.cshtml @@ -0,0 +1,42 @@ +@* + Test cases for using EventCallback with various delegate scenarios that used to be troublesome. + + Currently these cases are **VERBOSE** because we haven't yet landed the compiler support for EventCallback. + This will be cleaned up soon, and all of the explicit calls to EventCallback.Factory will go away. +*@ +
+

Clicking any of these buttons should cause the count to go up by one!

+

Render Count: @(++renderCount)

+
+
+

Passing Capturing Lambda to Button

+ +
+
+

Passing Unbound Lambda to Button

+ +
+
+

Passing Unbound Lambda to Nested Button

+ +
+
+

Passing Capturing Lambda to Strongly Typed Button

+ +
+
+

Passing Child Content

+ + + +
+
+

Passing Child Content

+ +
+ +@functions { + int renderCount; + + int buttonComponentCount = 1; // Avoid CS0649 +} diff --git a/src/Components/test/testassets/BasicTestApp/EventCallbackTest/InnerButton.cshtml b/src/Components/test/testassets/BasicTestApp/EventCallbackTest/InnerButton.cshtml new file mode 100644 index 0000000000..32a59dbeed --- /dev/null +++ b/src/Components/test/testassets/BasicTestApp/EventCallbackTest/InnerButton.cshtml @@ -0,0 +1,7 @@ + + + +@functions { + [Parameter] EventCallback OnClick { get; set; } + [Parameter] string Text { get; set; } +} diff --git a/src/Components/test/testassets/BasicTestApp/EventCallbackTest/MiddleButton.cshtml b/src/Components/test/testassets/BasicTestApp/EventCallbackTest/MiddleButton.cshtml new file mode 100644 index 0000000000..642b201430 --- /dev/null +++ b/src/Components/test/testassets/BasicTestApp/EventCallbackTest/MiddleButton.cshtml @@ -0,0 +1,7 @@ + + + +@functions { + [Parameter] EventCallback OnClick { get; set; } + [Parameter] string Text { get; set; } +} diff --git a/src/Components/test/testassets/BasicTestApp/EventCallbackTest/StronglyTypedButton.cshtml b/src/Components/test/testassets/BasicTestApp/EventCallbackTest/StronglyTypedButton.cshtml new file mode 100644 index 0000000000..727aaa5e33 --- /dev/null +++ b/src/Components/test/testassets/BasicTestApp/EventCallbackTest/StronglyTypedButton.cshtml @@ -0,0 +1,7 @@ + + + +@functions { + [Parameter] EventCallback OnClick { get; set; } + [Parameter] string Text { get; set; } +} diff --git a/src/Components/test/testassets/BasicTestApp/EventCallbackTest/TemplatedControl.cshtml b/src/Components/test/testassets/BasicTestApp/EventCallbackTest/TemplatedControl.cshtml new file mode 100644 index 0000000000..f5a1bfbf1b --- /dev/null +++ b/src/Components/test/testassets/BasicTestApp/EventCallbackTest/TemplatedControl.cshtml @@ -0,0 +1,8 @@ + +
+ @ChildContent +
+ +@functions { + [Parameter] RenderFragment ChildContent { get; set; } +} diff --git a/src/Components/test/testassets/BasicTestApp/Index.cshtml b/src/Components/test/testassets/BasicTestApp/Index.cshtml index 82aa5498a2..2bf6ccf600 100644 --- a/src/Components/test/testassets/BasicTestApp/Index.cshtml +++ b/src/Components/test/testassets/BasicTestApp/Index.cshtml @@ -45,6 +45,7 @@ + @if (SelectedComponentType != null)