Improvements to bind and event handling

The changes here make event dispatching (including bind) more
user-friendly by avoiding the need for manual calls to StateHasChnaged.

We also introduce a new type `EventCallback` (and `EventCallback<T>`).
This is a new primitive that is like a super-powered version of a
delegate. When writing a component that accepts delegates as parameters,
consider using `EventCallback` for the following reasons:
- Allows consumer to pass a variety of different delegate signatures
- Does proper event dispatching and error handling

Using `EventCallback` will eliminate most of the remaining cases where a
manual `StateHasChanged` is required when components are passing content
and delegates to each other.

`EventCallback` is inherently async for the reason that this is really
the only way to provide correct error handling.

-----

The fix for this will be two-phase by first creating a set of APIs that
can be targeted by the compiler that has the desired behaviour and then
updating the compiler to target this new infrastructure.
This commit is contained in:
Ryan Nowak 2019-02-13 16:13:02 -08:00
parent cc7b35439c
commit 98fe8a8328
38 changed files with 4394 additions and 315 deletions

View File

@ -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<Diagnostic> Diagnostics { get; set; }
}
private class TestRenderer : Renderer
protected class TestRenderer : Renderer
{
public TestRenderer() : base(new TestServiceProvider(), CreateDefaultDispatcher())
{

View File

@ -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<UIEventArgs> 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<UIEventArgs>)frame.AttributeValue)(new UIChangeEventArgs
{
Value = "Modified value"
});
Assert.Equal("Modified value", myValueProperty.GetValue(component));
setter = Assert.IsType<Action<UIEventArgs>>(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<UIEventArgs> 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<UIEventArgs>)frame.AttributeValue)(new UIChangeEventArgs
{
Value = "Modified value"
});
Assert.Equal("Modified value", myValueProperty.GetValue(component));
setter = Assert.IsType<Action<UIEventArgs>>(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<UIEventArgs> 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<UIEventArgs>)frame.AttributeValue)(new UIChangeEventArgs
{
Value = newDateValue.ToString()
});
Assert.Equal(newDateValue, myDateProperty.GetValue(component));
setter = Assert.IsType<Action<UIEventArgs>>(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<UIEventArgs> 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<UIEventArgs>)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<Action<UIEventArgs>>(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<UIEventArgs> 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<UIEventArgs>)frame.AttributeValue)(new UIChangeEventArgs
{
Value = false
});
Assert.False((bool)myValueProperty.GetValue(component));
setter = Assert.IsType<Action<UIEventArgs>>(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<MyEnum>();
@ -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<UIEventArgs> 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<UIEventArgs>)frame.AttributeValue)(new UIChangeEventArgs
{
Value = MyEnum.SecondValue.ToString()
});
Assert.Equal(MyEnum.SecondValue, (MyEnum)myValueProperty.GetValue(component));
setter = Assert.IsType<Action<UIEventArgs>>(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 }

View File

@ -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<RenderTreeEdit>, referenceFrames: ArrayValues<RenderTreeFrame>) {
private applyEdits(batch: RenderBatch, parent: LogicalElement, childIndex: number, edits: ArraySegment<RenderTreeEdit>, referenceFrames: ArrayValues<RenderTreeFrame>) {
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<RenderTreeFrame>, frame: RenderTreeFrame, frameIndex: number): number {
private insertFrame(batch: RenderBatch, parent: LogicalElement, childIndex: number, frames: ArrayValues<RenderTreeFrame>, 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<RenderTreeFrame>, frame: RenderTreeFrame, frameIndex: number) {
private insertElement(batch: RenderBatch, parent: LogicalElement, childIndex: number, frames: ArrayValues<RenderTreeFrame>, 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<RenderTreeFrame>, startIndex: number, endIndexExcl: number): number {
private insertFrameRange(batch: RenderBatch, parent: LogicalElement, childIndex: number, frames: ArrayValues<RenderTreeFrame>, 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<UIEventArgs>) {
function raiseEvent(event: Event, browserRendererId: number, eventHandlerId: number, eventArgs: EventForDotNet<UIEventArgs>) {
if (preventDefaultEvents[event.type]) {
event.preventDefault();
}
const eventDescriptor = {
browserRendererId,
componentId,
eventHandlerId,
eventArgsType: eventArgs.type
};

View File

@ -6,7 +6,7 @@ const nonBubblingEvents = toLookup([
]);
export interface OnEventCallback {
(event: Event, componentId: number, eventHandlerId: number, eventArgs: EventForDotNet<UIEventArgs>): void;
(event: Event, eventHandlerId: number, eventArgs: EventForDotNet<UIEventArgs>): 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;
}

View File

@ -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
/// </summary>
public int BrowserRendererId { get; set; }
/// <summary>
/// For framework use only.
/// </summary>
public int ComponentId { get; set; }
/// <summary>
/// For framework use only.
/// </summary>

View File

@ -69,12 +69,34 @@ namespace Microsoft.AspNetCore.Components
return value;
}
/// <summary>
/// Not intended to be used directly.
/// </summary>
public static EventCallback GetEventHandlerValue<T>(EventCallback value)
where T : UIEventArgs
{
return value;
}
/// <summary>
/// Not intended to be used directly.
/// </summary>
public static EventCallback<T> GetEventHandlerValue<T>(EventCallback<T> value)
where T : UIEventArgs
{
return value;
}
/// <summary>
/// Not intended to be used directly.
/// </summary>
public static Action<UIEventArgs> SetValueHandler(Action<string> setter, string existingValue)
{
return _ => setter((string)((UIChangeEventArgs)_).Value);
return eventArgs =>
{
setter((string)((UIChangeEventArgs)eventArgs).Value);
_ = DispatchEventAsync(setter.Target, EventCallbackWorkItem.Empty, UIEventArgs.Empty);
};
}
/// <summary>
@ -82,7 +104,11 @@ namespace Microsoft.AspNetCore.Components
/// </summary>
public static Action<UIEventArgs> SetValueHandler(Action<bool> setter, bool existingValue)
{
return _ => setter((bool)((UIChangeEventArgs)_).Value);
return eventArgs =>
{
setter((bool)((UIChangeEventArgs)eventArgs).Value);
_ = DispatchEventAsync(setter.Target, EventCallbackWorkItem.Empty, UIEventArgs.Empty);
};
}
/// <summary>
@ -90,7 +116,11 @@ namespace Microsoft.AspNetCore.Components
/// </summary>
public static Action<UIEventArgs> SetValueHandler(Action<bool?> setter, bool? existingValue)
{
return _ => setter((bool?)((UIChangeEventArgs)_).Value);
return eventArgs =>
{
setter((bool?)((UIChangeEventArgs)eventArgs).Value);
_ = DispatchEventAsync(setter.Target, EventCallbackWorkItem.Empty, UIEventArgs.Empty);
};
}
/// <summary>
@ -98,7 +128,11 @@ namespace Microsoft.AspNetCore.Components
/// </summary>
public static Action<UIEventArgs> SetValueHandler(Action<int> 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);
};
}
/// <summary>
@ -106,9 +140,11 @@ namespace Microsoft.AspNetCore.Components
/// </summary>
public static Action<UIEventArgs> SetValueHandler(Action<int?> 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);
};
}
/// <summary>
@ -116,7 +152,11 @@ namespace Microsoft.AspNetCore.Components
/// </summary>
public static Action<UIEventArgs> SetValueHandler(Action<long> 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);
};
}
/// <summary>
@ -124,9 +164,11 @@ namespace Microsoft.AspNetCore.Components
/// </summary>
public static Action<UIEventArgs> SetValueHandler(Action<long?> 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);
};
}
/// <summary>
@ -134,7 +176,11 @@ namespace Microsoft.AspNetCore.Components
/// </summary>
public static Action<UIEventArgs> SetValueHandler(Action<float> 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);
};
}
/// <summary>
@ -142,9 +188,11 @@ namespace Microsoft.AspNetCore.Components
/// </summary>
public static Action<UIEventArgs> SetValueHandler(Action<float?> 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);
};
}
/// <summary>
@ -152,7 +200,11 @@ namespace Microsoft.AspNetCore.Components
/// </summary>
public static Action<UIEventArgs> SetValueHandler(Action<double> 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);
};
}
/// <summary>
@ -160,9 +212,11 @@ namespace Microsoft.AspNetCore.Components
/// </summary>
public static Action<UIEventArgs> SetValueHandler(Action<double?> 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);
};
}
/// <summary>
@ -170,7 +224,11 @@ namespace Microsoft.AspNetCore.Components
/// </summary>
public static Action<UIEventArgs> SetValueHandler(Action<decimal> 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);
};
}
/// <summary>
@ -178,9 +236,11 @@ namespace Microsoft.AspNetCore.Components
/// </summary>
public static Action<UIEventArgs> SetValueHandler(Action<decimal?> 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);
};
}
/// <summary>
@ -188,7 +248,10 @@ namespace Microsoft.AspNetCore.Components
/// </summary>
public static Action<UIEventArgs> SetValueHandler(Action<DateTime> setter, DateTime existingValue)
{
return _ => SetDateTimeValue(setter, ((UIChangeEventArgs)_).Value, null);
return eventArgs =>
{
SetDateTimeValue(setter, ((UIChangeEventArgs)eventArgs).Value, null);
};
}
/// <summary>
@ -196,7 +259,10 @@ namespace Microsoft.AspNetCore.Components
/// </summary>
public static Action<UIEventArgs> SetValueHandler(Action<DateTime> setter, DateTime existingValue, string format)
{
return _ => SetDateTimeValue(setter, ((UIChangeEventArgs)_).Value, format);
return eventArgs =>
{
SetDateTimeValue(setter, ((UIChangeEventArgs)eventArgs).Value, format);
};
}
/// <summary>
@ -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);
}
}
}

View File

@ -67,7 +67,7 @@ namespace Microsoft.AspNetCore.Components
/// <summary>
/// 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.
/// </summary>
@ -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;

View File

@ -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
{
/// <summary>
/// A bound event handler delegate.
/// </summary>
public readonly struct EventCallback
{
/// <summary>
/// Gets a reference to the <see cref="EventCallbackFactory"/>.
/// </summary>
public static readonly EventCallbackFactory Factory = new EventCallbackFactory();
/// <summary>
/// Gets an empty <see cref="EventCallback{T}"/>.
/// </summary>
public static readonly EventCallback Empty = new EventCallback(null, (Action)(() => { }));
internal readonly MulticastDelegate Delegate;
internal readonly IHandleEvent Receiver;
/// <summary>
/// Creates the new <see cref="EventCallback{T}"/>.
/// </summary>
/// <param name="receiver">The event receiver.</param>
/// <param name="delegate">The delegate to bind.</param>
public EventCallback(IHandleEvent receiver, MulticastDelegate @delegate)
{
Receiver = receiver;
Delegate = @delegate;
}
/// <summary>
/// Gets a value that indicates whether the delegate associated with this event dispatcher is non-null.
/// </summary>
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);
/// <summary>
/// Invokes the delegate associated with this binding and dispatches an event notification to the
/// appropriate component.
/// </summary>
/// <param name="arg">The argument.</param>
/// <returns>A <see cref="Task"/> which completes asynchronously once event processing has completed.</returns>
public Task InvokeAsync(object arg)
{
if (Receiver == null)
{
return EventCallbackWorkItem.InvokeAsync<object>(Delegate, arg);
}
return Receiver.HandleEventAsync(new EventCallbackWorkItem(Delegate), arg);
}
}
/// <summary>
/// A bound event handler delegate.
/// </summary>
public readonly struct EventCallback<T>
{
internal readonly MulticastDelegate Delegate;
internal readonly IHandleEvent Receiver;
/// <summary>
/// Creates the new <see cref="EventCallback{T}"/>.
/// </summary>
/// <param name="receiver">The event receiver.</param>
/// <param name="delegate">The delegate to bind.</param>
public EventCallback(IHandleEvent receiver, MulticastDelegate @delegate)
{
Receiver = receiver;
Delegate = @delegate;
}
/// <summary>
/// Gets a value that indicates whether the delegate associated with this event dispatcher is non-null.
/// </summary>
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);
/// <summary>
/// Invokes the delegate associated with this binding and dispatches an event notification to the
/// appropriate component.
/// </summary>
/// <param name="arg">The argument.</param>
/// <returns>A <see cref="Task"/> which completes asynchronously once event processing has completed.</returns>
public Task InvokeAsync(T arg)
{
if (Receiver == null)
{
return EventCallbackWorkItem.InvokeAsync<T>(Delegate, arg);
}
return Receiver.HandleEventAsync(new EventCallbackWorkItem(Delegate), arg);
}
internal EventCallback AsUntyped()
{
return new EventCallback(Receiver ?? Delegate?.Target as IHandleEvent, Delegate);
}
}
}

View File

@ -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
{
/// <summary>
/// A factory for creating <see cref="EventCallback"/> and <see cref="EventCallback{T}"/>
/// instances.
/// </summary>
public sealed class EventCallbackFactory
{
/// <summary>
/// Creates an <see cref="EventCallback"/> for the provided <paramref name="receiver"/> and
/// <paramref name="callback"/>.
/// </summary>
/// <param name="receiver">The event receiver.</param>
/// <param name="callback">The event callback.</param>
/// <returns>The <see cref="EventCallback"/>.</returns>
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);
}
/// <summary>
/// Creates an <see cref="EventCallback"/> for the provided <paramref name="receiver"/> and
/// <paramref name="callback"/>.
/// </summary>
/// <param name="receiver">The event receiver.</param>
/// <param name="callback">The event callback.</param>
/// <returns>The <see cref="EventCallback"/>.</returns>
public EventCallback Create(object receiver, Action<object> callback)
{
if (receiver == null)
{
throw new ArgumentNullException(nameof(receiver));
}
if (callback == null)
{
throw new ArgumentNullException(nameof(callback));
}
return CreateCore(receiver, callback);
}
/// <summary>
/// Creates an <see cref="EventCallback"/> for the provided <paramref name="receiver"/> and
/// <paramref name="callback"/>.
/// </summary>
/// <param name="receiver">The event receiver.</param>
/// <param name="callback">The event callback.</param>
/// <returns>The <see cref="EventCallback"/>.</returns>
public EventCallback Create(object receiver, Func<Task> callback)
{
if (receiver == null)
{
throw new ArgumentNullException(nameof(receiver));
}
if (callback == null)
{
throw new ArgumentNullException(nameof(callback));
}
return CreateCore(receiver, callback);
}
/// <summary>
/// Creates an <see cref="EventCallback"/> for the provided <paramref name="receiver"/> and
/// <paramref name="callback"/>.
/// </summary>
/// <param name="receiver">The event receiver.</param>
/// <param name="callback">The event callback.</param>
/// <returns>The <see cref="EventCallback"/>.</returns>
public EventCallback Create(object receiver, Func<object, Task> callback)
{
if (receiver == null)
{
throw new ArgumentNullException(nameof(receiver));
}
if (callback == null)
{
throw new ArgumentNullException(nameof(callback));
}
return CreateCore(receiver, callback);
}
/// <summary>
/// Creates an <see cref="EventCallback"/> for the provided <paramref name="receiver"/> and
/// <paramref name="callback"/>.
/// </summary>
/// <param name="receiver">The event receiver.</param>
/// <param name="callback">The event callback.</param>
/// <returns>The <see cref="EventCallback"/>.</returns>
public EventCallback<T> Create<T>(object receiver, Action callback)
{
if (receiver == null)
{
throw new ArgumentNullException(nameof(receiver));
}
if (callback == null)
{
throw new ArgumentNullException(nameof(callback));
}
return CreateCore<T>(receiver, callback);
}
/// <summary>
/// Creates an <see cref="EventCallback"/> for the provided <paramref name="receiver"/> and
/// <paramref name="callback"/>.
/// </summary>
/// <param name="receiver">The event receiver.</param>
/// <param name="callback">The event callback.</param>
/// <returns>The <see cref="EventCallback"/>.</returns>
public EventCallback<T> Create<T>(object receiver, Action<T> callback)
{
if (receiver == null)
{
throw new ArgumentNullException(nameof(receiver));
}
if (callback == null)
{
throw new ArgumentNullException(nameof(callback));
}
return CreateCore<T>(receiver, callback);
}
/// <summary>
/// Creates an <see cref="EventCallback"/> for the provided <paramref name="receiver"/> and
/// <paramref name="callback"/>.
/// </summary>
/// <param name="receiver">The event receiver.</param>
/// <param name="callback">The event callback.</param>
/// <returns>The <see cref="EventCallback"/>.</returns>
public EventCallback<T> Create<T>(object receiver, Func<Task> callback)
{
if (receiver == null)
{
throw new ArgumentNullException(nameof(receiver));
}
if (callback == null)
{
throw new ArgumentNullException(nameof(callback));
}
return CreateCore<T>(receiver, callback);
}
/// <summary>
/// Creates an <see cref="EventCallback"/> for the provided <paramref name="receiver"/> and
/// <paramref name="callback"/>.
/// </summary>
/// <param name="receiver">The event receiver.</param>
/// <param name="callback">The event callback.</param>
/// <returns>The <see cref="EventCallback"/>.</returns>
public EventCallback<T> Create<T>(object receiver, Func<T, Task> callback)
{
if (receiver == null)
{
throw new ArgumentNullException(nameof(receiver));
}
if (callback == null)
{
throw new ArgumentNullException(nameof(callback));
}
return CreateCore<T>(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<T> CreateCore<T>(object receiver, MulticastDelegate callback)
{
if (!object.ReferenceEquals(receiver, callback.Target) && receiver is IHandleEvent handler)
{
return new EventCallback<T>(handler, callback);
}
return new EventCallback<T>(callback.Target as IHandleEvent, callback);
}
}
}

View File

@ -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
{
/// <summary>
/// Contains extension methods for two-way binding using <see cref="EventCallback"/>. For internal use only.
/// </summary>
public static class EventCallbackFactoryBinderExtensions
{
// Perf: conversion delegates are written as static funcs so we can prevent
// allocations for these simple cases.
private static Func<object, string> ConvertToString = (obj) => (string)obj;
private static Func<object, bool> ConvertToBool = (obj) => (bool)obj;
private static Func<object, bool?> ConvertToNullableBool = (obj) => (bool?)obj;
private static Func<object, int> ConvertToInt = (obj) => int.Parse((string)obj);
private static Func<object, int?> ConvertToNullableInt = (obj) =>
{
if (int.TryParse((string)obj, out var value))
{
return value;
}
return null;
};
private static Func<object, long> ConvertToLong = (obj) => long.Parse((string)obj);
private static Func<object, long?> ConvertToNullableLong = (obj) =>
{
if (long.TryParse((string)obj, out var value))
{
return value;
}
return null;
};
private static Func<object, float> ConvertToFloat = (obj) => float.Parse((string)obj);
private static Func<object, float?> ConvertToNullableFloat = (obj) =>
{
if (float.TryParse((string)obj, out var value))
{
return value;
}
return null;
};
private static Func<object, double> ConvertToDouble = (obj) => double.Parse((string)obj);
private static Func<object, double?> ConvertToNullableDouble = (obj) =>
{
if (double.TryParse((string)obj, out var value))
{
return value;
}
return null;
};
private static Func<object, decimal> ConvertToDecimal = (obj) => decimal.Parse((string)obj);
private static Func<object, decimal?> ConvertToNullableDecimal = (obj) =>
{
if (decimal.TryParse((string)obj, out var value))
{
return value;
}
return null;
};
private static class EnumConverter<T> where T : Enum
{
public static Func<object, T> Convert = (obj) =>
{
return (T)Enum.Parse(typeof(T), (string)obj);
};
}
/// <summary>
/// For internal use only.
/// </summary>
/// <param name="factory"></param>
/// <param name="receiver"></param>
/// <param name="setter"></param>
/// <param name="existingValue"></param>
/// <returns></returns>
public static EventCallback<UIChangeEventArgs> CreateBinder(
this EventCallbackFactory factory,
object receiver,
Action<string> setter,
string existingValue)
{
;
return CreateBinderCore<string>(factory, receiver, setter, ConvertToString);
}
/// <summary>
/// For internal use only.
/// </summary>
/// <param name="factory"></param>
/// <param name="receiver"></param>
/// <param name="setter"></param>
/// <param name="existingValue"></param>
/// <returns></returns>
public static EventCallback<UIChangeEventArgs> CreateBinder(
this EventCallbackFactory factory,
object receiver,
Action<bool> setter,
bool existingValue)
{
return CreateBinderCore<bool>(factory, receiver, setter, ConvertToBool);
}
/// <summary>
/// For internal use only.
/// </summary>
/// <param name="factory"></param>
/// <param name="receiver"></param>
/// <param name="setter"></param>
/// <param name="existingValue"></param>
/// <returns></returns>
public static EventCallback<UIChangeEventArgs> CreateBinder(
this EventCallbackFactory factory,
object receiver,
Action<bool?> setter,
bool? existingValue)
{
return CreateBinderCore<bool?>(factory, receiver, setter, ConvertToNullableBool);
}
/// <summary>
/// For internal use only.
/// </summary>
/// <param name="factory"></param>
/// <param name="receiver"></param>
/// <param name="setter"></param>
/// <param name="existingValue"></param>
/// <returns></returns>
public static EventCallback<UIChangeEventArgs> CreateBinder(
this EventCallbackFactory factory,
object receiver,
Action<int> setter,
int existingValue)
{
return CreateBinderCore<int>(factory, receiver, setter, ConvertToInt);
}
/// <summary>
/// For internal use only.
/// </summary>
/// <param name="factory"></param>
/// <param name="receiver"></param>
/// <param name="setter"></param>
/// <param name="existingValue"></param>
/// <returns></returns>
public static EventCallback<UIChangeEventArgs> CreateBinder(
this EventCallbackFactory factory,
object receiver,
Action<int?> setter,
int? existingValue)
{
return CreateBinderCore<int?>(factory, receiver, setter, ConvertToNullableInt);
}
/// <summary>
/// For internal use only.
/// </summary>
/// <param name="factory"></param>
/// <param name="receiver"></param>
/// <param name="setter"></param>
/// <param name="existingValue"></param>
/// <returns></returns>
public static EventCallback<UIChangeEventArgs> CreateBinder(
this EventCallbackFactory factory,
object receiver,
Action<long> setter,
long existingValue)
{
return CreateBinderCore<long>(factory, receiver, setter, ConvertToLong);
}
/// <summary>
/// For internal use only.
/// </summary>
/// <param name="factory"></param>
/// <param name="receiver"></param>
/// <param name="setter"></param>
/// <param name="existingValue"></param>
/// <returns></returns>
public static EventCallback<UIChangeEventArgs> CreateBinder(
this EventCallbackFactory factory,
object receiver,
Action<long?> setter,
long? existingValue)
{
return CreateBinderCore<long?>(factory, receiver, setter, ConvertToNullableLong);
}
/// <summary>
/// For internal use only.
/// </summary>
/// <param name="factory"></param>
/// <param name="receiver"></param>
/// <param name="setter"></param>
/// <param name="existingValue"></param>
/// <returns></returns>
public static EventCallback<UIChangeEventArgs> CreateBinder(
this EventCallbackFactory factory,
object receiver,
Action<float> setter,
float existingValue)
{
return CreateBinderCore<float>(factory, receiver, setter, ConvertToFloat);
}
/// <summary>
/// For internal use only.
/// </summary>
/// <param name="factory"></param>
/// <param name="receiver"></param>
/// <param name="setter"></param>
/// <param name="existingValue"></param>
/// <returns></returns>
public static EventCallback<UIChangeEventArgs> CreateBinder(
this EventCallbackFactory factory,
object receiver,
Action<float?> setter,
float? existingValue)
{
return CreateBinderCore<float?>(factory, receiver, setter, ConvertToNullableFloat);
}
/// <summary>
/// For internal use only.
/// </summary>
/// <param name="factory"></param>
/// <param name="receiver"></param>
/// <param name="setter"></param>
/// <param name="existingValue"></param>
/// <returns></returns>
public static EventCallback<UIChangeEventArgs> CreateBinder(
this EventCallbackFactory factory,
object receiver,
Action<double> setter,
double existingValue)
{
return CreateBinderCore<double>(factory, receiver, setter, ConvertToDouble);
}
/// <summary>
/// For internal use only.
/// </summary>
/// <param name="factory"></param>
/// <param name="receiver"></param>
/// <param name="setter"></param>
/// <param name="existingValue"></param>
/// <returns></returns>
public static EventCallback<UIChangeEventArgs> CreateBinder(
this EventCallbackFactory factory,
object receiver,
Action<double?> setter,
double? existingValue)
{
return CreateBinderCore<double?>(factory, receiver, setter, ConvertToNullableDouble);
}
/// <summary>
/// For internal use only.
/// </summary>
/// <param name="factory"></param>
/// <param name="receiver"></param>
/// <param name="setter"></param>
/// <param name="existingValue"></param>
/// <returns></returns>
public static EventCallback<UIChangeEventArgs> CreateBinder(
this EventCallbackFactory factory,
object receiver,
Action<decimal> setter,
decimal existingValue)
{
return CreateBinderCore<decimal>(factory, receiver, setter, ConvertToDecimal);
}
/// <summary>
/// For internal use only.
/// </summary>
/// <param name="factory"></param>
/// <param name="receiver"></param>
/// <param name="setter"></param>
/// <param name="existingValue"></param>
/// <returns></returns>
public static EventCallback<UIChangeEventArgs> CreateBinder(
this EventCallbackFactory factory,
object receiver,
Action<decimal?> setter,
decimal? existingValue)
{
Func<object, decimal?> converter = (obj) =>
{
if (decimal.TryParse((string)obj, out var value))
{
return value;
}
return null;
};
return CreateBinderCore<decimal?>(factory, receiver, setter, ConvertToNullableDecimal);
}
/// <summary>
/// For internal use only.
/// </summary>
/// <param name="factory"></param>
/// <param name="receiver"></param>
/// <param name="setter"></param>
/// <param name="existingValue"></param>
/// <returns></returns>
public static EventCallback<UIChangeEventArgs> CreateBinder(
this EventCallbackFactory factory,
object receiver,
Action<DateTime> setter,
DateTime existingValue)
{
// Avoiding CreateBinderCore so we can avoid an extra allocating lambda
// when a format is used.
Action<UIChangeEventArgs> callback = (e) =>
{
setter(ConvertDateTime(e.Value, format: null));
};
return factory.Create<UIChangeEventArgs>(receiver, callback);
}
/// <summary>
/// For internal use only.
/// </summary>
/// <param name="factory"></param>
/// <param name="receiver"></param>
/// <param name="setter"></param>
/// <param name="existingValue"></param>
/// <param name="format"></param>
/// <returns></returns>
public static EventCallback<UIChangeEventArgs> CreateBinder(
this EventCallbackFactory factory,
object receiver,
Action<DateTime> setter,
DateTime existingValue,
string format)
{
// Avoiding CreateBinderCore so we can avoid an extra allocating lambda
// when a format is used.
Action<UIChangeEventArgs> callback = (e) =>
{
setter(ConvertDateTime(e.Value, format));
};
return factory.Create<UIChangeEventArgs>(receiver, callback);
}
/// <summary>
/// For internal use only.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="factory"></param>
/// <param name="receiver"></param>
/// <param name="setter"></param>
/// <param name="existingValue"></param>
/// <returns></returns>
public static EventCallback<UIChangeEventArgs> CreateBinder<T>(
this EventCallbackFactory factory,
object receiver,
Action<T> setter,
T existingValue) where T : Enum
{
return CreateBinderCore<T>(factory, receiver, setter, EnumConverter<T>.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<UIChangeEventArgs> CreateBinderCore<T>(
this EventCallbackFactory factory,
object receiver,
Action<T> setter,
Func<object, T> converter)
{
Action<UIChangeEventArgs> callback = e =>
{
setter(converter(e.Value));
};
return factory.Create<UIChangeEventArgs>(receiver, callback);
}
}
}

View File

@ -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
{
/// <summary>
/// Provides extension methods for <see cref="EventCallbackFactory"/> and <see cref="UIEventArgs"/> types. For internal
/// framework use.
/// </summary>
public static class EventCallbackFactoryUIEventArgsExtensions
{
/// <summary>
/// Creates an <see cref="EventCallback"/> for the provided <paramref name="receiver"/> and
/// <paramref name="callback"/>.
/// </summary>
/// <param name="factory">The <see cref="EventCallbackFactory"/>.</param>
/// <param name="receiver">The event receiver.</param>
/// <param name="callback">The event callback.</param>
/// <returns>The <see cref="EventCallback"/>.</returns>
public static EventCallback<UIEventArgs> Create(this EventCallbackFactory factory, object receiver, Action<UIEventArgs> callback)
{
if (factory == null)
{
throw new ArgumentNullException(nameof(factory));
}
return factory.Create<UIEventArgs>(receiver, callback);
}
/// <summary>
/// Creates an <see cref="EventCallback"/> for the provided <paramref name="receiver"/> and
/// <paramref name="callback"/>.
/// </summary>
/// <param name="factory">The <see cref="EventCallbackFactory"/>.</param>
/// <param name="receiver">The event receiver.</param>
/// <param name="callback">The event callback.</param>
/// <returns>The <see cref="EventCallback"/>.</returns>
public static EventCallback<UIEventArgs> Create(this EventCallbackFactory factory, object receiver, Func<UIEventArgs, Task> callback)
{
if (factory == null)
{
throw new ArgumentNullException(nameof(factory));
}
return factory.Create<UIEventArgs>(receiver, callback);
}
/// <summary>
/// Creates an <see cref="EventCallback"/> for the provided <paramref name="receiver"/> and
/// <paramref name="callback"/>.
/// </summary>
/// <param name="factory">The <see cref="EventCallbackFactory"/>.</param>
/// <param name="receiver">The event receiver.</param>
/// <param name="callback">The event callback.</param>
/// <returns>The <see cref="EventCallback"/>.</returns>
public static EventCallback<UIChangeEventArgs> Create(this EventCallbackFactory factory, object receiver, Action<UIChangeEventArgs> callback)
{
if (factory == null)
{
throw new ArgumentNullException(nameof(factory));
}
return factory.Create<UIChangeEventArgs>(receiver, callback);
}
/// <summary>
/// Creates an <see cref="EventCallback"/> for the provided <paramref name="receiver"/> and
/// <paramref name="callback"/>.
/// </summary>
/// <param name="factory">The <see cref="EventCallbackFactory"/>.</param>
/// <param name="receiver">The event receiver.</param>
/// <param name="callback">The event callback.</param>
/// <returns>The <see cref="EventCallback"/>.</returns>
public static EventCallback<UIChangeEventArgs> Create(this EventCallbackFactory factory, object receiver, Func<UIChangeEventArgs, Task> callback)
{
if (factory == null)
{
throw new ArgumentNullException(nameof(factory));
}
return factory.Create<UIChangeEventArgs>(receiver, callback);
}
/// <summary>
/// Creates an <see cref="EventCallback"/> for the provided <paramref name="receiver"/> and
/// <paramref name="callback"/>.
/// </summary>
/// <param name="factory">The <see cref="EventCallbackFactory"/>.</param>
/// <param name="receiver">The event receiver.</param>
/// <param name="callback">The event callback.</param>
/// <returns>The <see cref="EventCallback"/>.</returns>
public static EventCallback<UIClipboardEventArgs> Create(this EventCallbackFactory factory, object receiver, Action<UIClipboardEventArgs> callback)
{
if (factory == null)
{
throw new ArgumentNullException(nameof(factory));
}
return factory.Create<UIClipboardEventArgs>(receiver, callback);
}
/// <summary>
/// Creates an <see cref="EventCallback"/> for the provided <paramref name="receiver"/> and
/// <paramref name="callback"/>.
/// </summary>
/// <param name="factory">The <see cref="EventCallbackFactory"/>.</param>
/// <param name="receiver">The event receiver.</param>
/// <param name="callback">The event callback.</param>
/// <returns>The <see cref="EventCallback"/>.</returns>
public static EventCallback<UIClipboardEventArgs> Create(this EventCallbackFactory factory, object receiver, Func<UIClipboardEventArgs, Task> callback)
{
if (factory == null)
{
throw new ArgumentNullException(nameof(factory));
}
return factory.Create<UIClipboardEventArgs>(receiver, callback);
}
/// <summary>
/// Creates an <see cref="EventCallback"/> for the provided <paramref name="receiver"/> and
/// <paramref name="callback"/>.
/// </summary>
/// <param name="factory">The <see cref="EventCallbackFactory"/>.</param>
/// <param name="receiver">The event receiver.</param>
/// <param name="callback">The event callback.</param>
/// <returns>The <see cref="EventCallback"/>.</returns>
public static EventCallback<UIDragEventArgs> Create(this EventCallbackFactory factory, object receiver, Action<UIDragEventArgs> callback)
{
if (factory == null)
{
throw new ArgumentNullException(nameof(factory));
}
return factory.Create<UIDragEventArgs>(receiver, callback);
}
/// <summary>
/// Creates an <see cref="EventCallback"/> for the provided <paramref name="receiver"/> and
/// <paramref name="callback"/>.
/// </summary>
/// <param name="factory">The <see cref="EventCallbackFactory"/>.</param>
/// <param name="receiver">The event receiver.</param>
/// <param name="callback">The event callback.</param>
/// <returns>The <see cref="EventCallback"/>.</returns>
public static EventCallback<UIDragEventArgs> Create(this EventCallbackFactory factory, object receiver, Func<UIDragEventArgs, Task> callback)
{
if (factory == null)
{
throw new ArgumentNullException(nameof(factory));
}
return factory.Create<UIDragEventArgs>(receiver, callback);
}
/// <summary>
/// Creates an <see cref="EventCallback"/> for the provided <paramref name="receiver"/> and
/// <paramref name="callback"/>.
/// </summary>
/// <param name="factory">The <see cref="EventCallbackFactory"/>.</param>
/// <param name="receiver">The event receiver.</param>
/// <param name="callback">The event callback.</param>
/// <returns>The <see cref="EventCallback"/>.</returns>
public static EventCallback<UIErrorEventArgs> Create(this EventCallbackFactory factory, object receiver, Action<UIErrorEventArgs> callback)
{
if (factory == null)
{
throw new ArgumentNullException(nameof(factory));
}
return factory.Create<UIErrorEventArgs>(receiver, callback);
}
/// <summary>
/// Creates an <see cref="EventCallback"/> for the provided <paramref name="receiver"/> and
/// <paramref name="callback"/>.
/// </summary>
/// <param name="factory">The <see cref="EventCallbackFactory"/>.</param>
/// <param name="receiver">The event receiver.</param>
/// <param name="callback">The event callback.</param>
/// <returns>The <see cref="EventCallback"/>.</returns>
public static EventCallback<UIErrorEventArgs> Create(this EventCallbackFactory factory, object receiver, Func<UIErrorEventArgs, Task> callback)
{
if (factory == null)
{
throw new ArgumentNullException(nameof(factory));
}
return factory.Create<UIErrorEventArgs>(receiver, callback);
}
/// <summary>
/// Creates an <see cref="EventCallback"/> for the provided <paramref name="receiver"/> and
/// <paramref name="callback"/>.
/// </summary>
/// <param name="factory">The <see cref="EventCallbackFactory"/>.</param>
/// <param name="receiver">The event receiver.</param>
/// <param name="callback">The event callback.</param>
/// <returns>The <see cref="EventCallback"/>.</returns>
public static EventCallback<UIFocusEventArgs> Create(this EventCallbackFactory factory, object receiver, Action<UIFocusEventArgs> callback)
{
if (factory == null)
{
throw new ArgumentNullException(nameof(factory));
}
return factory.Create<UIFocusEventArgs>(receiver, callback);
}
/// <summary>
/// Creates an <see cref="EventCallback"/> for the provided <paramref name="receiver"/> and
/// <paramref name="callback"/>.
/// </summary>
/// <param name="factory">The <see cref="EventCallbackFactory"/>.</param>
/// <param name="receiver">The event receiver.</param>
/// <param name="callback">The event callback.</param>
/// <returns>The <see cref="EventCallback"/>.</returns>
public static EventCallback<UIFocusEventArgs> Create(this EventCallbackFactory factory, object receiver, Func<UIFocusEventArgs, Task> callback)
{
if (factory == null)
{
throw new ArgumentNullException(nameof(factory));
}
return factory.Create<UIFocusEventArgs>(receiver, callback);
}
/// <summary>
/// Creates an <see cref="EventCallback"/> for the provided <paramref name="receiver"/> and
/// <paramref name="callback"/>.
/// </summary>
/// <param name="factory">The <see cref="EventCallbackFactory"/>.</param>
/// <param name="receiver">The event receiver.</param>
/// <param name="callback">The event callback.</param>
/// <returns>The <see cref="EventCallback"/>.</returns>
public static EventCallback<UIKeyboardEventArgs> Create(this EventCallbackFactory factory, object receiver, Action<UIKeyboardEventArgs> callback)
{
if (factory == null)
{
throw new ArgumentNullException(nameof(factory));
}
return factory.Create<UIKeyboardEventArgs>(receiver, callback);
}
/// <summary>
/// Creates an <see cref="EventCallback"/> for the provided <paramref name="receiver"/> and
/// <paramref name="callback"/>.
/// </summary>
/// <param name="factory">The <see cref="EventCallbackFactory"/>.</param>
/// <param name="receiver">The event receiver.</param>
/// <param name="callback">The event callback.</param>
/// <returns>The <see cref="EventCallback"/>.</returns>
public static EventCallback<UIKeyboardEventArgs> Create(this EventCallbackFactory factory, object receiver, Func<UIKeyboardEventArgs, Task> callback)
{
if (factory == null)
{
throw new ArgumentNullException(nameof(factory));
}
return factory.Create<UIKeyboardEventArgs>(receiver, callback);
}
/// <summary>
/// Creates an <see cref="EventCallback"/> for the provided <paramref name="receiver"/> and
/// <paramref name="callback"/>.
/// </summary>
/// <param name="factory">The <see cref="EventCallbackFactory"/>.</param>
/// <param name="receiver">The event receiver.</param>
/// <param name="callback">The event callback.</param>
/// <returns>The <see cref="EventCallback"/>.</returns>
public static EventCallback<UIMouseEventArgs> Create(this EventCallbackFactory factory, object receiver, Action<UIMouseEventArgs> callback)
{
if (factory == null)
{
throw new ArgumentNullException(nameof(factory));
}
return factory.Create<UIMouseEventArgs>(receiver, callback);
}
/// <summary>
/// Creates an <see cref="EventCallback"/> for the provided <paramref name="receiver"/> and
/// <paramref name="callback"/>.
/// </summary>
/// <param name="factory">The <see cref="EventCallbackFactory"/>.</param>
/// <param name="receiver">The event receiver.</param>
/// <param name="callback">The event callback.</param>
/// <returns>The <see cref="EventCallback"/>.</returns>
public static EventCallback<UIMouseEventArgs> Create(this EventCallbackFactory factory, object receiver, Func<UIMouseEventArgs, Task> callback)
{
if (factory == null)
{
throw new ArgumentNullException(nameof(factory));
}
return factory.Create<UIMouseEventArgs>(receiver, callback);
}
/// <summary>
/// Creates an <see cref="EventCallback"/> for the provided <paramref name="receiver"/> and
/// <paramref name="callback"/>.
/// </summary>
/// <param name="factory">The <see cref="EventCallbackFactory"/>.</param>
/// <param name="receiver">The event receiver.</param>
/// <param name="callback">The event callback.</param>
/// <returns>The <see cref="EventCallback"/>.</returns>
public static EventCallback<UIPointerEventArgs> Create(this EventCallbackFactory factory, object receiver, Action<UIPointerEventArgs> callback)
{
if (factory == null)
{
throw new ArgumentNullException(nameof(factory));
}
return factory.Create<UIPointerEventArgs>(receiver, callback);
}
/// <summary>
/// Creates an <see cref="EventCallback"/> for the provided <paramref name="receiver"/> and
/// <paramref name="callback"/>.
/// </summary>
/// <param name="factory">The <see cref="EventCallbackFactory"/>.</param>
/// <param name="receiver">The event receiver.</param>
/// <param name="callback">The event callback.</param>
/// <returns>The <see cref="EventCallback"/>.</returns>
public static EventCallback<UIPointerEventArgs> Create(this EventCallbackFactory factory, object receiver, Func<UIPointerEventArgs, Task> callback)
{
if (factory == null)
{
throw new ArgumentNullException(nameof(factory));
}
return factory.Create<UIPointerEventArgs>(receiver, callback);
}
/// <summary>
/// Creates an <see cref="EventCallback"/> for the provided <paramref name="receiver"/> and
/// <paramref name="callback"/>.
/// </summary>
/// <param name="factory">The <see cref="EventCallbackFactory"/>.</param>
/// <param name="receiver">The event receiver.</param>
/// <param name="callback">The event callback.</param>
/// <returns>The <see cref="EventCallback"/>.</returns>
public static EventCallback<UIProgressEventArgs> Create(this EventCallbackFactory factory, object receiver, Action<UIProgressEventArgs> callback)
{
if (factory == null)
{
throw new ArgumentNullException(nameof(factory));
}
return factory.Create<UIProgressEventArgs>(receiver, callback);
}
/// <summary>
/// Creates an <see cref="EventCallback"/> for the provided <paramref name="receiver"/> and
/// <paramref name="callback"/>.
/// </summary>
/// <param name="factory">The <see cref="EventCallbackFactory"/>.</param>
/// <param name="receiver">The event receiver.</param>
/// <param name="callback">The event callback.</param>
/// <returns>The <see cref="EventCallback"/>.</returns>
public static EventCallback<UIProgressEventArgs> Create(this EventCallbackFactory factory, object receiver, Func<UIProgressEventArgs, Task> callback)
{
if (factory == null)
{
throw new ArgumentNullException(nameof(factory));
}
return factory.Create<UIProgressEventArgs>(receiver, callback);
}
/// <summary>
/// Creates an <see cref="EventCallback"/> for the provided <paramref name="receiver"/> and
/// <paramref name="callback"/>.
/// </summary>
/// <param name="factory">The <see cref="EventCallbackFactory"/>.</param>
/// <param name="receiver">The event receiver.</param>
/// <param name="callback">The event callback.</param>
/// <returns>The <see cref="EventCallback"/>.</returns>
public static EventCallback<UITouchEventArgs> Create(this EventCallbackFactory factory, object receiver, Action<UITouchEventArgs> callback)
{
if (factory == null)
{
throw new ArgumentNullException(nameof(factory));
}
return factory.Create<UITouchEventArgs>(receiver, callback);
}
/// <summary>
/// Creates an <see cref="EventCallback"/> for the provided <paramref name="receiver"/> and
/// <paramref name="callback"/>.
/// </summary>
/// <param name="factory">The <see cref="EventCallbackFactory"/>.</param>
/// <param name="receiver">The event receiver.</param>
/// <param name="callback">The event callback.</param>
/// <returns>The <see cref="EventCallback"/>.</returns>
public static EventCallback<UITouchEventArgs> Create(this EventCallbackFactory factory, object receiver, Func<UITouchEventArgs, Task> callback)
{
if (factory == null)
{
throw new ArgumentNullException(nameof(factory));
}
return factory.Create<UITouchEventArgs>(receiver, callback);
}
/// <summary>
/// Creates an <see cref="EventCallback"/> for the provided <paramref name="receiver"/> and
/// <paramref name="callback"/>.
/// </summary>
/// <param name="factory">The <see cref="EventCallbackFactory"/>.</param>
/// <param name="receiver">The event receiver.</param>
/// <param name="callback">The event callback.</param>
/// <returns>The <see cref="EventCallback"/>.</returns>
public static EventCallback<UIWheelEventArgs> Create(this EventCallbackFactory factory, object receiver, Action<UIWheelEventArgs> callback)
{
if (factory == null)
{
throw new ArgumentNullException(nameof(factory));
}
return factory.Create<UIWheelEventArgs>(receiver, callback);
}
/// <summary>
/// Creates an <see cref="EventCallback"/> for the provided <paramref name="receiver"/> and
/// <paramref name="callback"/>.
/// </summary>
/// <param name="factory">The <see cref="EventCallbackFactory"/>.</param>
/// <param name="receiver">The event receiver.</param>
/// <param name="callback">The event callback.</param>
/// <returns>The <see cref="EventCallback"/>.</returns>
public static EventCallback<UIWheelEventArgs> Create(this EventCallbackFactory factory, object receiver, Func<UIWheelEventArgs, Task> callback)
{
if (factory == null)
{
throw new ArgumentNullException(nameof(factory));
}
return factory.Create<UIWheelEventArgs>(receiver, callback);
}
}
}

View File

@ -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
{
/// <summary>
/// Wraps a callback delegate associated with an event.
/// </summary>
public struct EventCallbackWorkItem
{
/// <summary>
/// An empty <see cref="EventCallbackWorkItem"/>.
/// </summary>
public static readonly EventCallbackWorkItem Empty = new EventCallbackWorkItem(null);
private readonly MulticastDelegate _delegate;
/// <summary>
/// Creates a new <see cref="EventCallbackWorkItem"/> with the provided <paramref name="delegate"/>.
/// </summary>
/// <param name="delegate">The callback delegate.</param>
public EventCallbackWorkItem(MulticastDelegate @delegate)
{
_delegate = @delegate;
}
/// <summary>
/// Invokes the delegate associated with this <see cref="EventCallbackWorkItem"/>.
/// </summary>
/// <param name="arg">The argument to provide to the delegate. May be <c>null</c>.</param>
/// <returns>A <see cref="Task"/> then will complete asynchronously once the delegate has completed.</returns>
public Task InvokeAsync(object arg)
{
return InvokeAsync<object>(_delegate, arg);
}
internal static Task InvokeAsync<T>(MulticastDelegate @delegate, T arg)
{
switch (@delegate)
{
case null:
return Task.CompletedTask;
case Action action:
action.Invoke();
return Task.CompletedTask;
case Action<T> actionEventArgs:
actionEventArgs.Invoke(arg);
return Task.CompletedTask;
case Func<Task> func:
return func.Invoke();
case Func<T, Task> 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);
}
}
}
}
}
}

View File

@ -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
{
/// <summary>
/// A bound event handler delegate.
/// </summary>
public readonly struct EventHandlerInvoker
{
private readonly MulticastDelegate _delegate;
/// <summary>
/// Creates the new <see cref="EventHandlerInvoker"/>.
/// </summary>
/// <param name="delegate">The delegate to bind.</param>
public EventHandlerInvoker(MulticastDelegate @delegate)
{
_delegate = @delegate;
}
/// <summary>
/// Invokes the delegate associated with this binding.
/// </summary>
/// <param name="e">The <see cref="UIEventArgs"/>.</param>
/// <returns></returns>
public Task Invoke(UIEventArgs e)
{
switch (_delegate)
{
case Action action:
action.Invoke();
return Task.CompletedTask;
case Action<UIEventArgs> actionEventArgs:
actionEventArgs.Invoke(e);
return Task.CompletedTask;
case Func<Task> func:
return func.Invoke();
case Func<UIEventArgs, Task> funcEventArgs:
return funcEventArgs.Invoke(e);
case MulticastDelegate @delegate:
return @delegate.DynamicInvoke(e) as Task ?? Task.CompletedTask;
case null:
return Task.CompletedTask;
}
}
}
}

View File

@ -6,16 +6,18 @@ using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Components
{
/// <summary>
/// Interface implemented by components that receive notification of their events.
/// Interface implemented by components that receive notification of state changes.
/// </summary>
public interface IHandleEvent
{
/// <summary>
/// Notifies the component that one of its event handlers has been triggered.
/// Notifies the a state change has been triggered.
/// </summary>
/// <param name="binding">The event binding.</param>
/// <param name="args">Arguments for the event handler.</param>
/// <returns>A <see cref="Task"/> that represents the asynchronous event handling operation.</returns>
Task HandleEventAsync(EventHandlerInvoker binding, UIEventArgs args);
/// <param name="item">The <see cref="EventCallbackWorkItem"/> associated with this event.</param>
/// <param name="arg">The argument associated with this event.</param>
/// <returns>
/// A <see cref="Task"/> that completes once the component has processed the state change.
/// </returns>
Task HandleEventAsync(EventCallbackWorkItem item, object arg);
}
}

View File

@ -277,6 +277,84 @@ namespace Microsoft.AspNetCore.Components.RenderTree
}
}
/// <summary>
/// <para>
/// Appends a frame representing an <see cref="EventCallback"/> attribute.
/// </para>
/// <para>
/// The attribute is associated with the most recently added element. If the value is <c>null</c> and the
/// current element is not a component, the frame will be omitted.
/// </para>
/// </summary>
/// <param name="sequence">An integer that represents the position of the instruction in the source code.</param>
/// <param name="name">The name of the attribute.</param>
/// <param name="value">The value of the attribute.</param>
/// <remarks>
/// This method is provided for infrastructure purposes, and is used to support generated code
/// that uses <see cref="EventCallbackFactory"/>.
/// </remarks>
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));
}
}
/// <summary>
/// <para>
/// Appends a frame representing an <see cref="EventCallback"/> attribute.
/// </para>
/// <para>
/// The attribute is associated with the most recently added element. If the value is <c>null</c> and the
/// current element is not a component, the frame will be omitted.
/// </para>
/// </summary>
/// <param name="sequence">An integer that represents the position of the instruction in the source code.</param>
/// <param name="name">The name of the attribute.</param>
/// <param name="value">The value of the attribute.</param>
/// <remarks>
/// This method is provided for infrastructure purposes, and is used to support generated code
/// that uses <see cref="EventCallbackFactory"/>.
/// </remarks>
public void AddAttribute<T>(int sequence, string name, EventCallback<T> 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<T> 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));
}
}
/// <summary>
/// Appends a frame representing a string-valued attribute.
/// The attribute is associated with the most recently added element. If the value is <c>null</c>, or

View File

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

View File

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

View File

@ -19,7 +19,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
private readonly ComponentFactory _componentFactory;
private readonly Dictionary<int, ComponentState> _componentStateById = new Dictionary<int, ComponentState>();
private readonly RenderBatchBuilder _batchBuilder = new RenderBatchBuilder();
private readonly Dictionary<int, EventHandlerInvoker> _eventBindings = new Dictionary<int, EventHandlerInvoker>();
private readonly Dictionary<int, EventCallback> _eventBindings = new Dictionary<int, EventCallback>();
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);
/// <summary>
/// Notifies the specified component that an event has occurred.
/// Notifies the renderer that an event has occurred.
/// </summary>
/// <param name="componentId">The unique identifier for the component within the scope of this <see cref="Renderer"/>.</param>
/// <param name="eventHandlerId">The <see cref="RenderTreeFrame.AttributeEventHandlerId"/> value from the original event attribute.</param>
/// <param name="eventArgs">Arguments to be passed to the event handler.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous execution operation.</returns>
public Task DispatchEventAsync(int componentId, int eventHandlerId, UIEventArgs eventArgs)
/// <returns>
/// A <see cref="Task"/> which will complete once all asynchronous processing related to the event
/// has completed.
/// </returns>
public Task DispatchEventAsync(int eventHandlerId, UIEventArgs eventArgs)
{
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);
}
/// <summary>
@ -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<T> here. EventCallback<T> is only used when passing
// a callback to a component, and never when used to attaching a DOM event handler.
frame = frame.WithAttributeEventHandlerId(id);
}

View File

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

View File

@ -8,6 +8,11 @@ namespace Microsoft.AspNetCore.Components
/// </summary>
public class UIEventArgs
{
/// <summary>
/// An empty instance of <see cref="UIEventArgs"/>.
/// </summary>
public static readonly UIEventArgs Empty = new UIEventArgs();
/// <summary>
/// Gets or sets the type of the event.
/// </summary>

View File

@ -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<int> setter = (_) => value = _;
var binder = EventCallback.Factory.CreateBinder(component, setter, value);
// Act
await Assert.ThrowsAsync<FormatException>(() =>
{
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<string> 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<bool> 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<bool?> 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<int> 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<int?> 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<long> 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<long?> 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<float> 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<float?> 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<double> 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<double?> 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<decimal> 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<decimal?> 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<AttributeTargets> 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<DateTime> 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<DateTime> 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();
}
}
}
}

View File

@ -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<string>)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<string>)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<string>)((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<Task>)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<Task>)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>)(() => 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<string, Task>)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<string, Task>)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<string, Task>)((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<string>(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<string>(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<string>(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<string>)component.SomeActionOfT;
// Act
var callback = EventCallback.Factory.Create<string>(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<string>)component.SomeActionOfT;
var anotherComponent = new EventComponent();
// Act
var callback = EventCallback.Factory.Create<string>(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<string>)((s) => { });
var anotherComponent = new EventComponent();
// Act
var callback = EventCallback.Factory.Create<string>(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<Task>)component.SomeFuncTask;
// Act
var callback = EventCallback.Factory.Create<string>(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<Task>)component.SomeFuncTask;
var anotherComponent = new EventComponent();
// Act
var callback = EventCallback.Factory.Create<string>(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>)(() => Task.CompletedTask);
var anotherComponent = new EventComponent();
// Act
var callback = EventCallback.Factory.Create<string>(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<string, Task>)component.SomeFuncTTask;
// Act
var callback = EventCallback.Factory.Create<string>(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<string, Task>)component.SomeFuncTTask;
var anotherComponent = new EventComponent();
// Act
var callback = EventCallback.Factory.Create<string>(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<string, Task>)((s) => Task.CompletedTask);
var anotherComponent = new EventComponent();
// Act
var callback = EventCallback.Factory.Create<string>(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();
}
}
}
}

View File

@ -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<UIEventArgs>);
// 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<UIEventArgs>(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<UIEventArgs>)((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<UIEventArgs>)((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<int>)((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<UIEventArgs>)((e) => { arg = e; runCount++; }));
// Act & Assert
await Assert.ThrowsAsync<ArgumentException>(() =>
{
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<Task>)(() => { 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<Task>)(() => { 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<UIEventArgs, Task>)((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<UIEventArgs, Task>)((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<int, Task>)((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<UIEventArgs, Task>)((e) => { arg = e; runCount++; return Task.CompletedTask; }));
// Act & Assert
await Assert.ThrowsAsync<ArgumentException>(() =>
{
return callback.InvokeAsync(new StringBuilder());
});
}
[Fact]
public async Task EventCallbackOfT_Action_Null()
{
// Arrange
var component = new EventCountingComponent();
int runCount = 0;
var callback = new EventCallback<UIEventArgs>(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<UIEventArgs>(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<UIEventArgs>(component, (Action<UIEventArgs>)((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<UIEventArgs>(component, (Action<UIEventArgs>)((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<UIEventArgs>(component, (Func<Task>)(() => { 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<UIEventArgs>(component, (Func<Task>)(() => { 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<UIEventArgs>(component, (Func<UIEventArgs, Task>)((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<UIEventArgs>(component, (Func<UIEventArgs, Task>)((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();
}
}
}

View File

@ -1,7 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
<RootNamespace>Microsoft.AspNetCore.Components</RootNamespace>
</PropertyGroup>
<ItemGroup>

File diff suppressed because it is too large Load Diff

View File

@ -459,6 +459,23 @@ namespace Microsoft.AspNetCore.Components.Rendering
await Assert.ThrowsAsync<InvalidTimeZoneException>(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<TaskCanceledException>(async () => await task);
}
[Fact]
public async Task Invoke_T_CanRunSynchronously_WhenNotBusy()
{
@ -530,6 +547,23 @@ namespace Microsoft.AspNetCore.Components.Rendering
await Assert.ThrowsAsync<InvalidTimeZoneException>(async () => await task);
}
[Fact]
public async Task Invoke_T_CanReportCancellation()
{
// Arrange
var context = new RendererSynchronizationContext();
// Act
var task = context.Invoke<string>(() =>
{
throw new OperationCanceledException();
});
// Assert
Assert.Equal(TaskStatus.Canceled, task.Status);
await Assert.ThrowsAsync<TaskCanceledException>(async () => await task);
}
[Fact]
public async Task InvokeAsync_Void_CanRunSynchronously_WhenNotBusy()
{
@ -607,6 +641,23 @@ namespace Microsoft.AspNetCore.Components.Rendering
await Assert.ThrowsAsync<InvalidTimeZoneException>(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<TaskCanceledException>(async () => await task);
}
[Fact]
public async Task InvokeAsync_T_CanRunSynchronously_WhenNotBusy()
{
@ -677,5 +728,22 @@ namespace Microsoft.AspNetCore.Components.Rendering
// Assert
await Assert.ThrowsAsync<InvalidTimeZoneException>(async () => await task);
}
[Fact]
public async Task InvokeAsync_T_CanReportCancellation()
{
// Arrange
var context = new RendererSynchronizationContext();
// Act
var task = context.InvokeAsync<string>(() =>
{
throw new OperationCanceledException();
});
// Assert
Assert.Equal(TaskStatus.Canceled, task.Status);
await Assert.ThrowsAsync<TaskCanceledException>(async () => await task);
}
}
}

View File

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

View File

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

View File

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

View File

@ -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<Program> serverFixture,
ITestOutputHelper output)
: base(browserFixture, serverFixture, output)
{
// On WebAssembly, page reloads are expensive so skip if possible
Navigate(ServerPathBase, noReload: !serverFixture.UsingAspNetHost);
MountTestComponent<BasicTestApp.EventCallbackTest.EventCallbackCases>();
}
[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);
}
}
}

View File

@ -16,7 +16,7 @@
</CascadingValue>
</CascadingValue>
<p><button id="increment-count" onclick=@counterState.IncrementCount>Increment</button></p>
<p><button id="increment-count" onclick="@((args) => counterState.IncrementCount())">Increment</button></p>
<p><label><input type="checkbox" id="toggle-flag-1" bind=currentFlagValue1 /> Flag 1</label></p>
<p><label><input type="checkbox" id="toggle-flag-2" bind=currentFlagValue2 /> Flag 2</label></p>

View File

@ -0,0 +1,14 @@
<button onclick="@OnClick">@Text</button>
@functions {
[Parameter] int Count { get; set; }
[Parameter] EventCallback<int> CountChanged { get; set; }
[Parameter] string Text { get; set; }
Task OnClick(UIMouseEventArgs e)
{
Count++;
return CountChanged.InvokeAsync(Count);
}
}

View File

@ -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.
*@
<div>
<p>Clicking any of these buttons should cause the count to go up by one!</p>
<p id="render_count">Render Count: @(++renderCount)</p>
</div>
<div id="capturing_lambda">
<h3>Passing Capturing Lambda to Button</h3>
<InnerButton OnClick="@EventCallback.Factory.Create(this, () => { GC.KeepAlive(this); })" Text="Capturing Lambda" />
</div>
<div id="unbound_lambda">
<h3>Passing Unbound Lambda to Button</h3>
<InnerButton OnClick="@EventCallback.Factory.Create(this, () => { })" Text="Unbound Lambda" />
</div>
<div id="unbound_lambda_nested">
<h3>Passing Unbound Lambda to Nested Button</h3>
<MiddleButton OnClick="@EventCallback.Factory.Create(this, () => { })" Text="Unbound Lambda Nested" />
</div>
<div id="unbound_lambda_strongly_typed">
<h3>Passing Capturing Lambda to Strongly Typed Button</h3>
<StronglyTypedButton OnClick="@(EventCallback.Factory.Create<UIMouseEventArgs>(this, () => { GC.KeepAlive(this); }))" Text="Unbound Lambda Strongly-Typed" />
</div>
<div id="unbound_lambda_child_content">
<h3>Passing Child Content</h3>
<TemplatedControl>
<button onclick="@EventCallback.Factory.Create(this, () => { })">Unbound Lambda Child Content</button>
</TemplatedControl>
</div>
<div id="unbound_lambda_bind_to_component">
<h3>Passing Child Content</h3>
<ButtonComponent Count="@buttonComponentCount" CountChanged="@(EventCallback.Factory.Create<int>(this, () => { }))" Text="Unbound Lambda Bind-To-Component" />
</div>
@functions {
int renderCount;
int buttonComponentCount = 1; // Avoid CS0649
}

View File

@ -0,0 +1,7 @@
<button onclick="@OnClick">@Text</button>
@functions {
[Parameter] EventCallback OnClick { get; set; }
[Parameter] string Text { get; set; }
}

View File

@ -0,0 +1,7 @@
<InnerButton OnClick="@OnClick" Text="@Text"/>
@functions {
[Parameter] EventCallback OnClick { get; set; }
[Parameter] string Text { get; set; }
}

View File

@ -0,0 +1,7 @@
<button onclick="@OnClick">@Text</button>
@functions {
[Parameter] EventCallback<UIMouseEventArgs> OnClick { get; set; }
[Parameter] string Text { get; set; }
}

View File

@ -0,0 +1,8 @@
<div>
@ChildContent
</div>
@functions {
[Parameter] RenderFragment ChildContent { get; set; }
}

View File

@ -45,6 +45,7 @@
<option value="BasicTestApp.CascadingValueTest.CascadingValueSupplier">Cascading values</option>
<option value="BasicTestApp.ConcurrentRenderParent">Concurrent rendering</option>
<option value="BasicTestApp.DispatchingComponent">Dispatching to sync context</option>
<option value="BasicTestApp.EventCallbackTest.EventCallbackCases">EventCallback</option>
</select>
@if (SelectedComponentType != null)