Browse Source

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.
Ryan Nowak 7 years ago
parent
commit
98fe8a8328
38 changed files with 4323 additions and 325 deletions
  1. 6 1
      src/Components/Blazor/Build/test/RazorIntegrationTestBase.cs
  2. 68 57
      src/Components/Blazor/Build/test/RenderingRazorIntegrationTest.cs
  3. 18 23
      src/Components/Browser.JS/src/Rendering/BrowserRenderer.ts
  4. 4 5
      src/Components/Browser.JS/src/Rendering/EventDelegator.ts
  5. 1 10
      src/Components/Browser/src/RendererRegistryEventDispatcher.cs
  6. 112 27
      src/Components/Components/src/BindMethods.cs
  7. 5 5
      src/Components/Components/src/ComponentBase.cs
  8. 115 0
      src/Components/Components/src/EventCallback.cs
  9. 211 0
      src/Components/Components/src/EventCallbackFactory.cs
  10. 411 0
      src/Components/Components/src/EventCallbackFactoryBinderExtensions.cs
  11. 446 0
      src/Components/Components/src/EventCallbackFactoryUIEventArgsExtensions.cs
  12. 79 0
      src/Components/Components/src/EventCallbackWorkItem.cs
  13. 0 55
      src/Components/Components/src/EventHandlerInvoker.cs
  14. 8 6
      src/Components/Components/src/IHandleEvent.cs
  15. 78 0
      src/Components/Components/src/RenderTree/RenderTreeBuilder.cs
  16. 1 1
      src/Components/Components/src/RenderTree/RenderTreeDiffBuilder.cs
  17. 0 14
      src/Components/Components/src/Rendering/ComponentState.cs
  18. 45 23
      src/Components/Components/src/Rendering/Renderer.cs
  19. 16 0
      src/Components/Components/src/Rendering/RendererSynchronizationContext.cs
  20. 5 0
      src/Components/Components/src/UIEventArgs.cs
  21. 358 0
      src/Components/Components/test/EventCallbackFactoryBinderExtensionsTest.cs
  22. 464 0
      src/Components/Components/test/EventCallbackFactoryTest.cs
  23. 457 0
      src/Components/Components/test/EventCallbackTest.cs
  24. 2 1
      src/Components/Components/test/Microsoft.AspNetCore.Components.Tests.csproj
  25. 1207 85
      src/Components/Components/test/RendererTest.cs
  26. 68 0
      src/Components/Components/test/Rendering/RendererSynchronizationContextTests.cs
  27. 2 5
      src/Components/Shared/test/TestRenderer.cs
  28. 5 2
      src/Components/test/E2ETest/Infrastructure/SeleniumStandaloneServer.cs
  29. 2 4
      src/Components/test/E2ETest/Tests/CascadingValueTest.cs
  30. 42 0
      src/Components/test/E2ETest/Tests/EventCallbackTest.cs
  31. 1 1
      src/Components/test/testassets/BasicTestApp/CascadingValueTest/CascadingValueSupplier.cshtml
  32. 14 0
      src/Components/test/testassets/BasicTestApp/EventCallbackTest/ButtonComponent.cshtml
  33. 42 0
      src/Components/test/testassets/BasicTestApp/EventCallbackTest/EventCallbackCases.cshtml
  34. 7 0
      src/Components/test/testassets/BasicTestApp/EventCallbackTest/InnerButton.cshtml
  35. 7 0
      src/Components/test/testassets/BasicTestApp/EventCallbackTest/MiddleButton.cshtml
  36. 7 0
      src/Components/test/testassets/BasicTestApp/EventCallbackTest/StronglyTypedButton.cshtml
  37. 8 0
      src/Components/test/testassets/BasicTestApp/EventCallbackTest/TemplatedControl.cshtml
  38. 1 0
      src/Components/test/testassets/BasicTestApp/Index.cshtml

+ 6 - 1
src/Components/Blazor/Build/test/RazorIntegrationTestBase.cs

@@ -376,6 +376,11 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
         protected RenderTreeFrame[] GetRenderTree(IComponent component)
         {
             var renderer = new TestRenderer();
+            return GetRenderTree(renderer, component);
+        }
+
+        protected private RenderTreeFrame[] GetRenderTree(TestRenderer renderer, IComponent component)
+        {
             renderer.AttachComponent(component);
             var task = renderer.InvokeAsync(() => component.SetParametersAsync(ParameterCollection.Empty));
             // we will have to change this method if we add a test that does actual async work.
@@ -432,7 +437,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
             public IEnumerable<Diagnostic> Diagnostics { get; set; }
         }
 
-        private class TestRenderer : Renderer
+        protected class TestRenderer : Renderer
         {
             public TestRenderer() : base(new TestServiceProvider(), CreateDefaultDispatcher())
             {

+ 68 - 57
src/Components/Blazor/Build/test/RenderingRazorIntegrationTest.cs

@@ -3,6 +3,7 @@
 
 using System;
 using System.Collections.Generic;
+using System.Threading.Tasks;
 using Microsoft.AspNetCore.Components;
 using Microsoft.AspNetCore.Components.RenderTree;
 using Microsoft.AspNetCore.Components.Test.Helpers;
@@ -351,7 +352,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
         }
 
         [Fact]
-        public void SupportsTwoWayBindingForTextboxes()
+        public async Task SupportsTwoWayBindingForTextboxes()
         {
             // Arrange/Act
             var component = CompileToComponent(
@@ -361,26 +362,27 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
                 }");
             var myValueProperty = component.GetType().GetProperty("MyValue");
 
+            var renderer = new TestRenderer();
+
             // Assert
-            var frames = GetRenderTree(component);
+            Action<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 }

+ 18 - 23
src/Components/Browser.JS/src/Rendering/BrowserRenderer.ts

@@ -1,6 +1,4 @@
-import { System_Array, MethodHandle } from '../Platform/Platform';
-import { RenderBatch, ArraySegment, ArrayRange, RenderTreeEdit, RenderTreeFrame, EditType, FrameType, ArrayValues } from './RenderBatch/RenderBatch';
-import { platform } from '../Environment';
+import { RenderBatch, ArraySegment, RenderTreeEdit, RenderTreeFrame, EditType, FrameType, ArrayValues } from './RenderBatch/RenderBatch';
 import { EventDelegator } from './EventDelegator';
 import { EventForDotNet, UIEventArgs } from './EventForDotNet';
 import { LogicalElement, toLogicalElement, insertLogicalChild, removeLogicalChild, getLogicalParent, getLogicalChild, createAndInsertLogicalContainer, isSvgElement } from './LogicalElements';
@@ -9,16 +7,14 @@ const selectValuePropname = '_blazorSelectValue';
 const sharedTemplateElemForParsing = document.createElement('template');
 const sharedSvgElemForParsing = document.createElementNS('http://www.w3.org/2000/svg', 'g');
 const preventDefaultEvents: { [eventType: string]: boolean } = { submit: true };
-let raiseEventMethod: MethodHandle;
-let renderComponentMethod: MethodHandle;
 
 export class BrowserRenderer {
   private eventDelegator: EventDelegator;
   private childComponentLocations: { [componentId: number]: LogicalElement } = {};
 
   constructor(private browserRendererId: number) {
-    this.eventDelegator = new EventDelegator((event, componentId, eventHandlerId, eventArgs) => {
-      raiseEvent(event, this.browserRendererId, componentId, eventHandlerId, eventArgs);
+    this.eventDelegator = new EventDelegator((event, eventHandlerId, eventArgs) => {
+      raiseEvent(event, this.browserRendererId, eventHandlerId, eventArgs);
     });
   }
 
@@ -32,7 +28,7 @@ export class BrowserRenderer {
       throw new Error(`No element is currently associated with component ${componentId}`);
     }
 
-    this.applyEdits(batch, componentId, element, 0, edits, referenceFrames);
+    this.applyEdits(batch, element, 0, edits, referenceFrames);
   }
 
   public disposeComponent(componentId: number) {
@@ -47,7 +43,7 @@ export class BrowserRenderer {
     this.childComponentLocations[componentId] = element;
   }
 
-  private applyEdits(batch: RenderBatch, componentId: number, parent: LogicalElement, childIndex: number, edits: ArraySegment<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
   };

+ 4 - 5
src/Components/Browser.JS/src/Rendering/EventDelegator.ts

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

+ 1 - 10
src/Components/Browser/src/RendererRegistryEventDispatcher.cs

@@ -22,11 +22,7 @@ namespace Microsoft.AspNetCore.Components.Browser
         {
             var eventArgs = ParseEventArgsJson(eventDescriptor.EventArgsType, eventArgsJson);
             var renderer = RendererRegistry.Current.Find(eventDescriptor.BrowserRendererId);
-
-            return renderer.DispatchEventAsync(
-                eventDescriptor.ComponentId,
-                eventDescriptor.EventHandlerId,
-                eventArgs);
+            return renderer.DispatchEventAsync(eventDescriptor.EventHandlerId, eventArgs);
         }
 
         private static UIEventArgs ParseEventArgsJson(string eventArgsType, string eventArgsJson)
@@ -72,11 +68,6 @@ namespace Microsoft.AspNetCore.Components.Browser
             /// </summary>
             public int BrowserRendererId { get; set; }
 
-            /// <summary>
-            /// For framework use only.
-            /// </summary>
-            public int ComponentId { get; set; }
-
             /// <summary>
             /// For framework use only.
             /// </summary>

+ 112 - 27
src/Components/Components/src/BindMethods.cs

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

+ 5 - 5
src/Components/Components/src/ComponentBase.cs

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

+ 115 - 0
src/Components/Components/src/EventCallback.cs

@@ -0,0 +1,115 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Threading.Tasks;
+
+namespace Microsoft.AspNetCore.Components
+{
+    /// <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);
+        }
+    }
+}

+ 211 - 0
src/Components/Components/src/EventCallbackFactory.cs

@@ -0,0 +1,211 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Threading.Tasks;
+
+namespace Microsoft.AspNetCore.Components
+{
+    /// <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);
+        }
+    }
+}

+ 411 - 0
src/Components/Components/src/EventCallbackFactoryBinderExtensions.cs

@@ -0,0 +1,411 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Globalization;
+
+namespace Microsoft.AspNetCore.Components
+{
+    /// <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);
+        }
+    }
+}

+ 446 - 0
src/Components/Components/src/EventCallbackFactoryUIEventArgsExtensions.cs

@@ -0,0 +1,446 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Threading.Tasks;
+
+namespace Microsoft.AspNetCore.Components
+{
+    /// <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);
+        }
+    }
+}

+ 79 - 0
src/Components/Components/src/EventCallbackWorkItem.cs

@@ -0,0 +1,79 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Reflection;
+using System.Threading.Tasks;
+
+namespace Microsoft.AspNetCore.Components
+{
+    /// <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);
+                        }
+                    }
+            }
+        }
+    }
+}

+ 0 - 55
src/Components/Components/src/EventHandlerInvoker.cs

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

+ 8 - 6
src/Components/Components/src/IHandleEvent.cs

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

+ 78 - 0
src/Components/Components/src/RenderTree/RenderTreeBuilder.cs

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

+ 1 - 1
src/Components/Components/src/RenderTree/RenderTreeDiffBuilder.cs

@@ -652,7 +652,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree
             //
             // We're following a simple heuristic here that's reflected in the ts runtime
             // based on the common usage of attributes for DOM events.
-            if (newFrame.AttributeValue is MulticastDelegate &&
+            if ((newFrame.AttributeValue is MulticastDelegate || newFrame.AttributeValue is EventCallback) &&
                 newFrame.AttributeName.Length >= 3 &&
                 newFrame.AttributeName.StartsWith("on"))
             {

+ 0 - 14
src/Components/Components/src/Rendering/ComponentState.cs

@@ -93,20 +93,6 @@ namespace Microsoft.AspNetCore.Components.Rendering
             }
         }
 
-        public Task DispatchEventAsync(EventHandlerInvoker binding, UIEventArgs eventArgs)
-        {
-            if (Component is IHandleEvent handleEventComponent)
-            {
-                return handleEventComponent.HandleEventAsync(binding, eventArgs);
-            }
-            else
-            {
-                throw new InvalidOperationException(
-                    $"The component of type {Component.GetType().FullName} cannot receive " +
-                    $"events because it does not implement {typeof(IHandleEvent).FullName}.");
-            }
-        }
-
         public Task NotifyRenderCompletedAsync()
         {
             if (Component is IHandleAfterRender handlerAfterRender)

+ 45 - 23
src/Components/Components/src/Rendering/Renderer.cs

@@ -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))
+            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.
-                var componentState = GetRequiredComponentState(componentId);
-                Task task = null;
-                try
-                {
-                    _isBatchInProgress = true;
-                    task = componentState.DispatchEventAsync(binding, eventArgs);
-                }
-                finally
-                {
-                    _isBatchInProgress = false;
-                    ProcessRenderQueue();
-                }
+                _isBatchInProgress = true;
 
-                return GetErrorHandledTask(task);
+                task = callback.InvokeAsync(eventArgs);
             }
-            else
+            finally
             {
-                throw new ArgumentException($"There is no event handler with ID {eventHandlerId}");
+                _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,11 +343,28 @@ namespace Microsoft.AspNetCore.Components.Rendering
         {
             var id = ++_lastEventHandlerId;
 
-            if (frame.AttributeValue is MulticastDelegate @delegate)
+            if (frame.AttributeValue is EventCallback callback)
+            {
+                // 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)
             {
-                _eventBindings.Add(id, new EventHandlerInvoker(@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);
         }
 

+ 16 - 0
src/Components/Components/src/Rendering/RendererSynchronizationContext.cs

@@ -47,6 +47,10 @@ namespace Microsoft.AspNetCore.Components.Rendering
                     action();
                     completion.SetResult(null);
                 }
+                catch (OperationCanceledException)
+                {
+                    completion.SetCanceled();
+                }
                 catch (Exception exception)
                 {
                     completion.SetException(exception);
@@ -66,6 +70,10 @@ namespace Microsoft.AspNetCore.Components.Rendering
                     await asyncAction();
                     completion.SetResult(null);
                 }
+                catch (OperationCanceledException)
+                {
+                    completion.SetCanceled();
+                }
                 catch (Exception exception)
                 {
                     completion.SetException(exception);
@@ -85,6 +93,10 @@ namespace Microsoft.AspNetCore.Components.Rendering
                     var result = function();
                     completion.SetResult(result);
                 }
+                catch (OperationCanceledException)
+                {
+                    completion.SetCanceled();
+                }
                 catch (Exception exception)
                 {
                     completion.SetException(exception);
@@ -104,6 +116,10 @@ namespace Microsoft.AspNetCore.Components.Rendering
                     var result = await asyncFunction();
                     completion.SetResult(result);
                 }
+                catch (OperationCanceledException)
+                {
+                    completion.SetCanceled();
+                }
                 catch (Exception exception)
                 {
                     completion.SetException(exception);

+ 5 - 0
src/Components/Components/src/UIEventArgs.cs

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

+ 358 - 0
src/Components/Components/test/EventCallbackFactoryBinderExtensionsTest.cs

@@ -0,0 +1,358 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Components
+{
+    public class EventCallbackFactoryBinderExtensionsTest
+    {
+        [Fact]
+        public async Task CreateBinder_ThrowsConversionException()
+        {
+            // Arrange
+            var value = 17;
+            var component = new EventCountingComponent();
+            Action<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();
+            }
+        }
+    }
+}

+ 464 - 0
src/Components/Components/test/EventCallbackFactoryTest.cs

@@ -0,0 +1,464 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Components
+{
+    public class EventCallbackFactoryTest
+    {
+        [Fact]
+        public void Create_Action_AlreadyBoundToReceiver()
+        {
+            // Arrange
+            var component = new EventComponent();
+            var @delegate = (Action)component.SomeAction;
+
+            // Act
+            var callback = EventCallback.Factory.Create(component, @delegate);
+
+            // Assert
+            Assert.Same(@delegate, callback.Delegate);
+            Assert.Same(component, callback.Receiver);
+            Assert.False(callback.RequiresExplicitReceiver);
+        }
+
+        [Fact]
+        public void Create_Action_DifferentReceiver()
+        {
+            // Arrange
+            var component = new EventComponent();
+            var @delegate = (Action)component.SomeAction;
+
+            var anotherComponent = new EventComponent();
+
+            // Act
+            var callback = EventCallback.Factory.Create(anotherComponent, @delegate);
+
+            // Assert
+            Assert.Same(@delegate, callback.Delegate);
+            Assert.Same(anotherComponent, callback.Receiver);
+            Assert.True(callback.RequiresExplicitReceiver);
+        }
+
+        [Fact]
+        public void Create_Action_Unbound()
+        {
+            // Arrange
+            var component = new EventComponent();
+            var @delegate = (Action)(() => { });
+
+            var anotherComponent = new EventComponent();
+
+            // Act
+            var callback = EventCallback.Factory.Create(anotherComponent, @delegate);
+
+            // Assert
+            Assert.Same(@delegate, callback.Delegate);
+            Assert.Same(anotherComponent, callback.Receiver);
+            Assert.True(callback.RequiresExplicitReceiver);
+        }
+
+        [Fact]
+        public void Create_ActionT_AlreadyBoundToReceiver()
+        {
+            // Arrange
+            var component = new EventComponent();
+            var @delegate = (Action<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();
+            }
+        }
+    }
+}

+ 457 - 0
src/Components/Components/test/EventCallbackTest.cs

@@ -0,0 +1,457 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Text;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Components
+{
+    public class EventCallbackTest
+    {
+        [Fact]
+        public async Task EventCallback_Default()
+        {
+            // Arrange
+            var callback = default(EventCallback);
+
+            // Act & Assert (Does not throw)
+            await callback.InvokeAsync(null);
+        }
+
+        [Fact]
+        public async Task EventCallbackOfT_Default()
+        {
+            // Arrange
+            var callback = default(EventCallback<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();
+        }
+    }
+}

+ 2 - 1
src/Components/Components/test/Microsoft.AspNetCore.Components.Tests.csproj

@@ -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
+ 1207 - 85
src/Components/Components/test/RendererTest.cs


+ 68 - 0
src/Components/Components/test/Rendering/RendererSynchronizationContextTests.cs

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

+ 2 - 5
src/Components/Shared/test/TestRenderer.cs

@@ -49,11 +49,8 @@ namespace Microsoft.AspNetCore.Components.Test.Helpers
         public new Task RenderRootComponentAsync(int componentId, ParameterCollection parameters)
             => InvokeAsync(() => base.RenderRootComponentAsync(componentId, parameters));
 
-        public new Task DispatchEventAsync(int componentId, int eventHandlerId, UIEventArgs args)
-        {
-            var task = InvokeAsync(() => base.DispatchEventAsync(componentId, eventHandlerId, args));
-            return UnwrapTask(task);
-        }
+        public new Task DispatchEventAsync(int eventHandlerId, UIEventArgs args)
+            => InvokeAsync(() => base.DispatchEventAsync(eventHandlerId, args));
 
         private static Task UnwrapTask(Task task)
         {

+ 5 - 2
src/Components/test/E2ETest/Infrastructure/SeleniumStandaloneServer.cs

@@ -107,13 +107,16 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Infrastructure
                     {
 
                     }
-                    await Task.Delay(1000); 
+                    await Task.Delay(1000);
                 }
             });
 
             try
             {
-                waitForStart.TimeoutAfter(Timeout).Wait(1000);
+                // Wait in intervals instead of indefinitely to prevent thread starvation.
+                while (!waitForStart.TimeoutAfter(Timeout).Wait(1000))
+                {
+                }
             }
             catch (Exception ex)
             {

+ 2 - 4
src/Components/test/E2ETest/Tests/CascadingValueTest.cs

@@ -77,10 +77,8 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
             decrementButton.Click();
             WaitAssert.Equal("98", () => currentCount.Text);
 
-            // Renders the descendant the same number of times we triggered
-            // events on it, because we always re-render components after they
-            // have an event
-            Assert.Equal("3", Browser.FindElement(By.Id("receive-by-interface-num-renders")).Text);
+            // Didn't re-render descendants
+            Assert.Equal("1", Browser.FindElement(By.Id("receive-by-interface-num-renders")).Text);
         }
     }
 }

+ 42 - 0
src/Components/test/E2ETest/Tests/EventCallbackTest.cs

@@ -0,0 +1,42 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using BasicTestApp;
+using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
+using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
+using OpenQA.Selenium;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Microsoft.AspNetCore.Components.E2ETest.Tests
+{
+    public class EventCallbackTest : BasicTestAppTestBase
+    {
+        public EventCallbackTest(
+            BrowserFixture browserFixture,
+            ToggleExecutionModeServerFixture<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);
+        }
+    }
+}

+ 1 - 1
src/Components/test/testassets/BasicTestApp/CascadingValueTest/CascadingValueSupplier.cshtml

@@ -16,7 +16,7 @@
     </CascadingValue>
 </CascadingValue>
 
-<p><button id="increment-count" [email protected]>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>
 

+ 14 - 0
src/Components/test/testassets/BasicTestApp/EventCallbackTest/ButtonComponent.cshtml

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

+ 42 - 0
src/Components/test/testassets/BasicTestApp/EventCallbackTest/EventCallbackCases.cshtml

@@ -0,0 +1,42 @@
+@*
+    Test cases for using EventCallback with various delegate scenarios that used to be troublesome.
+
+    Currently these cases are **VERBOSE** because we haven't yet landed the compiler support for EventCallback.
+    This will be cleaned up soon, and all of the explicit calls to EventCallback.Factory will go away.
+*@
+<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
+}

+ 7 - 0
src/Components/test/testassets/BasicTestApp/EventCallbackTest/InnerButton.cshtml

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

+ 7 - 0
src/Components/test/testassets/BasicTestApp/EventCallbackTest/MiddleButton.cshtml

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

+ 7 - 0
src/Components/test/testassets/BasicTestApp/EventCallbackTest/StronglyTypedButton.cshtml

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

+ 8 - 0
src/Components/test/testassets/BasicTestApp/EventCallbackTest/TemplatedControl.cshtml

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

+ 1 - 0
src/Components/test/testassets/BasicTestApp/Index.cshtml

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

Some files were not shown because too many files changed in this diff