Add IHandleEvent concept so components can define their own lifecycle around events

This commit is contained in:
Steve Sanderson 2018-02-13 16:42:21 +00:00
parent a889cd3152
commit 804ab2d89f
5 changed files with 81 additions and 7 deletions

View File

@ -11,7 +11,7 @@ namespace Microsoft.AspNetCore.Blazor.Components
/// Optional base class for Blazor components. Alternatively, Blazor components may
/// implement <see cref="IComponent"/> directly.
/// </summary>
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 <Project Sdk="Microsoft.NET.Sdk.Web">,
// 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,

View File

@ -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
{
/// <summary>
/// Interface implemented by components that receive notification of their events.
/// </summary>
public interface IHandleEvent
{
/// <summary>
/// Notifies the component that one of its event handlers has been triggered.
/// </summary>
/// <param name="handler">The event handler.</param>
/// <param name="args">Arguments for the event handler.</param>
void HandleEvent(UIEventHandler handler, UIEventArgs args);
}
}

View File

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

View File

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

View File

@ -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<InvalidOperationException>(() =>
{
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<T> : AutoRenderComponent where T : IComponent