Add IHandleEvent concept so components can define their own lifecycle around events
This commit is contained in:
parent
a889cd3152
commit
804ab2d89f
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue