diff --git a/src/Microsoft.AspNetCore.Blazor/Components/BlazorComponent.cs b/src/Microsoft.AspNetCore.Blazor/Components/BlazorComponent.cs
index fc3b85f757..ee064f5afa 100644
--- a/src/Microsoft.AspNetCore.Blazor/Components/BlazorComponent.cs
+++ b/src/Microsoft.AspNetCore.Blazor/Components/BlazorComponent.cs
@@ -11,7 +11,7 @@ namespace Microsoft.AspNetCore.Blazor.Components
/// Optional base class for Blazor components. Alternatively, Blazor components may
/// implement directly.
///
- public abstract class BlazorComponent : IComponent
+ public abstract class BlazorComponent : IComponent, IHandleEvent
{
private RenderHandle _renderHandle;
private bool _hasNeverRendered = true;
@@ -83,6 +83,16 @@ namespace Microsoft.AspNetCore.Blazor.Components
StateHasChanged();
}
+ void IHandleEvent.HandleEvent(UIEventHandler handler, UIEventArgs args)
+ {
+ handler(args);
+
+ // After each event, we synchronously re-render (unless !ShouldRender())
+ // This just saves the developer the trouble of putting "StateHasChanged();"
+ // at the end of every event callback.
+ StateHasChanged();
+ }
+
// At present, if you have a .cshtml file in a project with ,
// Visual Studio will run design-time builds for it, codegenning a class that attempts to override
// this method. Therefore the virtual method must be defined, even though it won't be used at runtime,
diff --git a/src/Microsoft.AspNetCore.Blazor/Components/IHandleEvent.cs b/src/Microsoft.AspNetCore.Blazor/Components/IHandleEvent.cs
new file mode 100644
index 0000000000..64f05c7880
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Blazor/Components/IHandleEvent.cs
@@ -0,0 +1,20 @@
+// 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 Microsoft.AspNetCore.Blazor.RenderTree;
+
+namespace Microsoft.AspNetCore.Blazor.Components
+{
+ ///
+ /// Interface implemented by components that receive notification of their events.
+ ///
+ public interface IHandleEvent
+ {
+ ///
+ /// Notifies the component that one of its event handlers has been triggered.
+ ///
+ /// The event handler.
+ /// Arguments for the event handler.
+ void HandleEvent(UIEventHandler handler, UIEventArgs args);
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Blazor/Rendering/ComponentState.cs b/src/Microsoft.AspNetCore.Blazor/Rendering/ComponentState.cs
index f0e7641125..84990d8af5 100644
--- a/src/Microsoft.AspNetCore.Blazor/Rendering/ComponentState.cs
+++ b/src/Microsoft.AspNetCore.Blazor/Rendering/ComponentState.cs
@@ -62,5 +62,19 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
RenderTreeDiffBuilder.DisposeFrames(batchBuilder, _renderTreeBuilderCurrent.GetFrames());
}
+
+ public void DispatchEvent(UIEventHandler handler, UIEventArgs eventArgs)
+ {
+ if (_component is IHandleEvent handleEventComponent)
+ {
+ handleEventComponent.HandleEvent(handler, eventArgs);
+ }
+ else
+ {
+ throw new InvalidOperationException(
+ $"The component of type {_component.GetType().FullName} cannot receive " +
+ $"events because it does not implement {typeof(IHandleEvent).FullName}.");
+ }
+ }
}
}
diff --git a/src/Microsoft.AspNetCore.Blazor/Rendering/Renderer.cs b/src/Microsoft.AspNetCore.Blazor/Rendering/Renderer.cs
index e512ecff1b..5bfb2088b2 100644
--- a/src/Microsoft.AspNetCore.Blazor/Rendering/Renderer.cs
+++ b/src/Microsoft.AspNetCore.Blazor/Rendering/Renderer.cs
@@ -120,11 +120,7 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
{
if (_eventHandlersById.TryGetValue(eventHandlerId, out var handler))
{
- handler.Invoke(eventArgs);
-
- // After any event, we synchronously re-render. Most of the time this means that
- // developers don't need to call Render() on their components explicitly.
- RenderNewBatch(componentId);
+ GetRequiredComponentState(componentId).DispatchEvent(handler, eventArgs);
}
else
{
diff --git a/test/Microsoft.AspNetCore.Blazor.Test/RendererTest.cs b/test/Microsoft.AspNetCore.Blazor.Test/RendererTest.cs
index 4ae670a37d..f7a960ce74 100644
--- a/test/Microsoft.AspNetCore.Blazor.Test/RendererTest.cs
+++ b/test/Microsoft.AspNetCore.Blazor.Test/RendererTest.cs
@@ -228,6 +228,37 @@ namespace Microsoft.AspNetCore.Blazor.Test
Assert.Same(eventArgs, receivedArgs);
}
+ [Fact]
+ public void ThrowsIfComponentDoesNotHandleEvents()
+ {
+ // Arrange: Render a component with an event handler
+ var renderer = new TestRenderer();
+ UIEventHandler handler = args => throw new NotImplementedException();
+ var component = new TestComponent(builder =>
+ {
+ builder.OpenElement(0, "mybutton");
+ builder.AddAttribute(1, "my click event", handler);
+ builder.CloseElement();
+ });
+
+ var componentId = renderer.AssignComponentId(component);
+ renderer.RenderNewBatch(componentId);
+
+ var eventHandlerId = renderer.Batches.Single()
+ .ReferenceFrames
+ .First(frame => frame.AttributeValue != null)
+ .AttributeEventHandlerId;
+ var eventArgs = new UIEventArgs();
+
+ // Act/Assert
+ var ex = Assert.Throws(() =>
+ {
+ renderer.DispatchEvent(componentId, eventHandlerId, eventArgs);
+ });
+ Assert.Equal($"The component of type {typeof(TestComponent).FullName} cannot receive " +
+ $"events because it does not implement {typeof(IHandleEvent).FullName}.", ex.Message);
+ }
+
[Fact]
public void CannotRenderUnknownComponents()
{
@@ -812,7 +843,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
=> parameters.AssignToProperties(this);
}
- private class EventComponent : AutoRenderComponent, IComponent
+ private class EventComponent : AutoRenderComponent, IComponent, IHandleEvent
{
public UIEventHandler Handler { get; set; }
public bool SkipElement { get; set; }
@@ -833,6 +864,9 @@ namespace Microsoft.AspNetCore.Blazor.Test
}
builder.CloseElement();
}
+
+ public void HandleEvent(UIEventHandler handler, UIEventArgs args)
+ => handler(args);
}
private class ConditionalParentComponent : AutoRenderComponent where T : IComponent