From 804ab2d89fa5d0139041a09170d733101b2672c3 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Tue, 13 Feb 2018 16:42:21 +0000 Subject: [PATCH] Add IHandleEvent concept so components can define their own lifecycle around events --- .../Components/BlazorComponent.cs | 12 ++++++- .../Components/IHandleEvent.cs | 20 +++++++++++ .../Rendering/ComponentState.cs | 14 ++++++++ .../Rendering/Renderer.cs | 6 +--- .../RendererTest.cs | 36 ++++++++++++++++++- 5 files changed, 81 insertions(+), 7 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Blazor/Components/IHandleEvent.cs 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