Blazor API Review: Design concept for Dispatcher (#11930)

* Design concept for Dispatcher

Part of: #11610

This change brings forward the Dispatcher as a more primary and more
expandable concept.
- Dispatcher shows up in more places
- Dispatcher is an abstract class for horizontal scalability
- Dispatcher has parallels with S.Windows.Threading.Dispatcher where
possible

Looking for feedback on this approach. I feel pretty strongly that
making this an abstract class is the right choice, I want to see
opinions on how much to push it into people's faces.

* WIP

* PR feedback
This commit is contained in:
Ryan Nowak 2019-07-11 17:31:57 -07:00 committed by GitHub
parent 62f00a77e6
commit a27b9fc335
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 333 additions and 249 deletions

View File

@ -59,6 +59,7 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
public partial class WebAssemblyRenderer : Microsoft.AspNetCore.Components.Rendering.Renderer
{
public WebAssemblyRenderer(System.IServiceProvider serviceProvider, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory) : base (default(System.IServiceProvider), default(Microsoft.Extensions.Logging.ILoggerFactory)) { }
public override Microsoft.AspNetCore.Components.Dispatcher Dispatcher { get { throw null; } }
public System.Threading.Tasks.Task AddComponentAsync(System.Type componentType, string domElementSelector) { throw null; }
public System.Threading.Tasks.Task AddComponentAsync<TComponent>(string domElementSelector) where TComponent : Microsoft.AspNetCore.Components.IComponent { throw null; }
public override System.Threading.Tasks.Task DispatchEventAsync(int eventHandlerId, Microsoft.AspNetCore.Components.Rendering.EventFieldInfo eventFieldInfo, Microsoft.AspNetCore.Components.UIEventArgs eventArgs) { throw null; }

View File

@ -0,0 +1,61 @@
// 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 Microsoft.AspNetCore.Components;
namespace Microsoft.AspNetCore.Blazor.Rendering
{
internal class NullDispatcher : Dispatcher
{
public static readonly Dispatcher Instance = new NullDispatcher();
private NullDispatcher()
{
}
public override bool CheckAccess() => true;
public override Task InvokeAsync(Action workItem)
{
if (workItem is null)
{
throw new ArgumentNullException(nameof(workItem));
}
workItem();
return Task.CompletedTask;
}
public override Task InvokeAsync(Func<Task> workItem)
{
if (workItem is null)
{
throw new ArgumentNullException(nameof(workItem));
}
return workItem();
}
public override Task<TResult> InvokeAsync<TResult>(Func<TResult> workItem)
{
if (workItem is null)
{
throw new ArgumentNullException(nameof(workItem));
}
return Task.FromResult(workItem());
}
public override Task<TResult> InvokeAsync<TResult>(Func<Task<TResult>> workItem)
{
if (workItem is null)
{
throw new ArgumentNullException(nameof(workItem));
}
return workItem();
}
}
}

View File

@ -37,6 +37,8 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
_webAssemblyRendererId = RendererRegistry.Current.Add(this);
}
public override Dispatcher Dispatcher => NullDispatcher.Instance;
/// <summary>
/// Attaches a new root component to the renderer,
/// causing it to be displayed in the specified DOM element.

View File

@ -385,7 +385,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
protected private RenderTreeFrame[] GetRenderTree(TestRenderer renderer, IComponent component)
{
renderer.AttachComponent(component);
var task = renderer.InvokeAsync(() => component.SetParametersAsync(ParameterCollection.Empty));
var task = renderer.Dispatcher.InvokeAsync(() => component.SetParametersAsync(ParameterCollection.Empty));
// we will have to change this method if we add a test that does actual async work.
Assert.True(task.Status.HasFlag(TaskStatus.RanToCompletion) || task.Status.HasFlag(TaskStatus.Faulted));
if (task.IsFaulted)
@ -442,10 +442,12 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
protected class TestRenderer : Renderer
{
public TestRenderer() : base(new TestServiceProvider(), NullLoggerFactory.Instance, CreateDefaultDispatcher())
public TestRenderer() : base(new TestServiceProvider(), NullLoggerFactory.Instance)
{
}
public override Dispatcher Dispatcher { get; } = Dispatcher.CreateDefault();
public RenderTreeFrame[] LatestBatchReferenceFrames { get; private set; }
public void AttachComponent(IComponent component)

View File

@ -332,7 +332,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
// Trigger the change event to show it updates the property
//
// This should always complete synchronously.
var task = renderer.InvokeAsync(() => setter.InvokeAsync(new UIChangeEventArgs { Value = "Modified value", }));
var task = renderer.Dispatcher.InvokeAsync(() => setter.InvokeAsync(new UIChangeEventArgs { Value = "Modified value", }));
Assert.Equal(TaskStatus.RanToCompletion, task.Status);
await task;
@ -367,7 +367,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
// Trigger the change event to show it updates the property
//
// This should always complete synchronously.
var task = renderer.InvokeAsync(() => setter.InvokeAsync(new UIChangeEventArgs { Value = "Modified value", }));
var task = renderer.Dispatcher.InvokeAsync(() => setter.InvokeAsync(new UIChangeEventArgs { Value = "Modified value", }));
Assert.Equal(TaskStatus.RanToCompletion, task.Status);
await task;
@ -404,7 +404,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
//
// This should always complete synchronously.
var newDateValue = new DateTime(2018, 3, 5, 4, 5, 6);
var task = renderer.InvokeAsync(() => setter.InvokeAsync(new UIChangeEventArgs { Value = newDateValue.ToString(), }));
var task = renderer.Dispatcher.InvokeAsync(() => setter.InvokeAsync(new UIChangeEventArgs { Value = newDateValue.ToString(), }));
Assert.Equal(TaskStatus.RanToCompletion, task.Status);
await task;
@ -440,7 +440,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
// Trigger the change event to show it updates the property
//
// This should always complete synchronously.
var task = renderer.InvokeAsync(() => setter.InvokeAsync(new UIChangeEventArgs { Value = new DateTime(2018, 3, 5).ToString(testDateFormat), }));
var task = renderer.Dispatcher.InvokeAsync(() => setter.InvokeAsync(new UIChangeEventArgs { Value = new DateTime(2018, 3, 5).ToString(testDateFormat), }));
Assert.Equal(TaskStatus.RanToCompletion, task.Status);
await task;
@ -559,7 +559,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
// Trigger the change event to show it updates the property
//
// This should always complete synchronously.
var task = renderer.InvokeAsync(() => setter.InvokeAsync(new UIChangeEventArgs() { Value = false, }));
var task = renderer.Dispatcher.InvokeAsync(() => setter.InvokeAsync(new UIChangeEventArgs() { Value = false, }));
Assert.Equal(TaskStatus.RanToCompletion, task.Status);
await task;
@ -595,7 +595,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
// Trigger the change event to show it updates the property
//
// This should always complete synchronously.
var task = renderer.InvokeAsync(() => setter.InvokeAsync(new UIChangeEventArgs { Value = MyEnum.SecondValue.ToString(), }));
var task = renderer.Dispatcher.InvokeAsync(() => setter.InvokeAsync(new UIChangeEventArgs { Value = MyEnum.SecondValue.ToString(), }));
Assert.Equal(TaskStatus.RanToCompletion, task.Status);
await task;

View File

@ -88,10 +88,12 @@ namespace Microsoft.AspNetCore.Components.Performance
private class FakeRenderer : Renderer
{
public FakeRenderer()
: base(new TestServiceProvider(), NullLoggerFactory.Instance, new RendererSynchronizationContext())
: base(new TestServiceProvider(), NullLoggerFactory.Instance)
{
}
public override Dispatcher Dispatcher { get; } = Dispatcher.CreateDefault();
protected override void HandleException(Exception exception)
{
throw new NotImplementedException();

View File

@ -85,6 +85,17 @@ namespace Microsoft.AspNetCore.Components
public Microsoft.AspNetCore.Components.UIDataTransferItem[] Items { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public string[] Types { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
}
public abstract partial class Dispatcher
{
protected Dispatcher() { }
public abstract bool CheckAccess();
public static Microsoft.AspNetCore.Components.Dispatcher CreateDefault() { throw null; }
public abstract System.Threading.Tasks.Task InvokeAsync(System.Action workItem);
public abstract System.Threading.Tasks.Task InvokeAsync(System.Func<System.Threading.Tasks.Task> workItem);
public abstract System.Threading.Tasks.Task<TResult> InvokeAsync<TResult>(System.Func<System.Threading.Tasks.Task<TResult>> workItem);
public abstract System.Threading.Tasks.Task<TResult> InvokeAsync<TResult>(System.Func<TResult> workItem);
protected void OnUnhandledException(System.UnhandledExceptionEventArgs e) { }
}
[System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)]
public readonly partial struct ElementRef
{
@ -303,13 +314,6 @@ namespace Microsoft.AspNetCore.Components
{
bool IsConnected { get; }
}
public partial interface IDispatcher
{
System.Threading.Tasks.Task InvokeAsync(System.Action workItem);
System.Threading.Tasks.Task InvokeAsync(System.Func<System.Threading.Tasks.Task> workItem);
System.Threading.Tasks.Task<TResult> InvokeAsync<TResult>(System.Func<System.Threading.Tasks.Task<TResult>> workItem);
System.Threading.Tasks.Task<TResult> InvokeAsync<TResult>(System.Func<TResult> workItem);
}
public partial interface IHandleAfterRender
{
System.Threading.Tasks.Task OnAfterRenderAsync();
@ -406,9 +410,8 @@ namespace Microsoft.AspNetCore.Components
{
private readonly object _dummy;
private readonly int _dummyPrimitive;
public Microsoft.AspNetCore.Components.Dispatcher Dispatcher { get { throw null; } }
public bool IsInitialized { get { throw null; } }
public System.Threading.Tasks.Task InvokeAsync(System.Action workItem) { throw null; }
public System.Threading.Tasks.Task InvokeAsync(System.Func<System.Threading.Tasks.Task> workItem) { throw null; }
public void Render(Microsoft.AspNetCore.Components.RenderFragment renderFragment) { }
}
[System.AttributeUsageAttribute(System.AttributeTargets.Class, AllowMultiple=true, Inherited=false)]
@ -656,7 +659,8 @@ namespace Microsoft.AspNetCore.Components.Rendering
}
public partial class HtmlRenderer : Microsoft.AspNetCore.Components.Rendering.Renderer
{
public HtmlRenderer(System.IServiceProvider serviceProvider, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory, Microsoft.AspNetCore.Components.IDispatcher dispatcher, System.Func<string, string> htmlEncoder) : base (default(System.IServiceProvider), default(Microsoft.Extensions.Logging.ILoggerFactory)) { }
public HtmlRenderer(System.IServiceProvider serviceProvider, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory, System.Func<string, string> htmlEncoder) : base (default(System.IServiceProvider), default(Microsoft.Extensions.Logging.ILoggerFactory)) { }
public override Microsoft.AspNetCore.Components.Dispatcher Dispatcher { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
protected override void HandleException(System.Exception exception) { }
[System.Diagnostics.DebuggerStepThroughAttribute]
public System.Threading.Tasks.Task<Microsoft.AspNetCore.Components.Rendering.ComponentRenderedText> RenderComponentAsync(System.Type componentType, Microsoft.AspNetCore.Components.ParameterCollection initialParameters) { throw null; }
@ -675,18 +679,15 @@ namespace Microsoft.AspNetCore.Components.Rendering
public abstract partial class Renderer : System.IDisposable
{
public Renderer(System.IServiceProvider serviceProvider, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory) { }
public Renderer(System.IServiceProvider serviceProvider, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory, Microsoft.AspNetCore.Components.IDispatcher dispatcher) { }
public abstract Microsoft.AspNetCore.Components.Dispatcher Dispatcher { get; }
public event System.UnhandledExceptionEventHandler UnhandledSynchronizationException { add { } remove { } }
protected internal virtual void AddToRenderQueue(int componentId, Microsoft.AspNetCore.Components.RenderFragment renderFragment) { }
protected internal int AssignRootComponentId(Microsoft.AspNetCore.Components.IComponent component) { throw null; }
public static Microsoft.AspNetCore.Components.IDispatcher CreateDefaultDispatcher() { throw null; }
public virtual System.Threading.Tasks.Task DispatchEventAsync(int eventHandlerId, Microsoft.AspNetCore.Components.Rendering.EventFieldInfo fieldInfo, Microsoft.AspNetCore.Components.UIEventArgs eventArgs) { throw null; }
public void Dispose() { }
protected virtual void Dispose(bool disposing) { }
protected abstract void HandleException(System.Exception exception);
protected Microsoft.AspNetCore.Components.IComponent InstantiateComponent(System.Type componentType) { throw null; }
public virtual System.Threading.Tasks.Task InvokeAsync(System.Action workItem) { throw null; }
public virtual System.Threading.Tasks.Task InvokeAsync(System.Func<System.Threading.Tasks.Task> workItem) { throw null; }
protected System.Threading.Tasks.Task RenderRootComponentAsync(int componentId) { throw null; }
[System.Diagnostics.DebuggerStepThroughAttribute]
protected System.Threading.Tasks.Task RenderRootComponentAsync(int componentId, Microsoft.AspNetCore.Components.ParameterCollection initialParameters) { throw null; }

View File

@ -148,7 +148,7 @@ namespace Microsoft.AspNetCore.Components
/// </summary>
/// <param name="workItem">The work item to execute.</param>
protected Task InvokeAsync(Action workItem)
=> _renderHandle.InvokeAsync(workItem);
=> _renderHandle.Dispatcher.InvokeAsync(workItem);
/// <summary>
/// Executes the supplied work item on the associated renderer's
@ -156,7 +156,7 @@ namespace Microsoft.AspNetCore.Components
/// </summary>
/// <param name="workItem">The work item to execute.</param>
protected Task InvokeAsync(Func<Task> workItem)
=> _renderHandle.InvokeAsync(workItem);
=> _renderHandle.Dispatcher.InvokeAsync(workItem);
void IComponent.Configure(RenderHandle renderHandle)
{

View File

@ -10,34 +10,66 @@ namespace Microsoft.AspNetCore.Components
/// <summary>
/// Dispatches external actions to be executed on the context of a <see cref="Renderer"/>.
/// </summary>
public interface IDispatcher
public abstract class Dispatcher
{
/// <summary>
/// Creates a default instance of <see cref="Dispatcher"/>.
/// </summary>
/// <returns>A <see cref="Dispatcher"/> instance.</returns>
public static Dispatcher CreateDefault() => new RendererSynchronizationContextDispatcher();
/// <summary>
/// Provides notifications of unhandled exceptions that occur within the dispatcher.
/// </summary>
internal event UnhandledExceptionEventHandler UnhandledException;
/// <summary>
/// Returns a value that determines whether using the dispatcher to invoke a work item is required
/// from the current context.
/// </summary>
/// <returns><c>true</c> if invoking is required, otherwise <c>false</c>.</returns>
public abstract bool CheckAccess();
/// <summary>
/// Invokes the given <see cref="Action"/> in the context of the associated <see cref="Renderer"/>.
/// </summary>
/// <param name="workItem">The action to execute.</param>
/// <returns>A <see cref="Task"/> that will be completed when the action has finished executing.</returns>
Task InvokeAsync(Action workItem);
public abstract Task InvokeAsync(Action workItem);
/// <summary>
/// Invokes the given <see cref="Func{TResult}"/> in the context of the associated <see cref="Renderer"/>.
/// </summary>
/// <param name="workItem">The asynchronous action to execute.</param>
/// <returns>A <see cref="Task"/> that will be completed when the action has finished executing.</returns>
Task InvokeAsync(Func<Task> workItem);
public abstract Task InvokeAsync(Func<Task> workItem);
/// <summary>
/// Invokes the given <see cref="Func{TResult}"/> in the context of the associated <see cref="Renderer"/>.
/// </summary>
/// <param name="workItem">The function to execute.</param>
/// <returns>A <see cref="Task{TResult}"/> that will be completed when the function has finished executing.</returns>
Task<TResult> InvokeAsync<TResult>(Func<TResult> workItem);
public abstract Task<TResult> InvokeAsync<TResult>(Func<TResult> workItem);
/// <summary>
/// Invokes the given <see cref="Func{TResult}"/> in the context of the associated <see cref="Renderer"/>.
/// </summary>
/// <param name="workItem">The asynchronous function to execute.</param>
/// <returns>A <see cref="Task{TResult}"/> that will be completed when the function has finished executing.</returns>
Task<TResult> InvokeAsync<TResult>(Func<Task<TResult>> workItem);
public abstract Task<TResult> InvokeAsync<TResult>(Func<Task<TResult>> workItem);
/// <summary>
/// Called to notify listeners of an unhandled exception.
/// </summary>
/// <param name="e">The <see cref="UnhandledExceptionEventArgs"/>.</param>
protected void OnUnhandledException(UnhandledExceptionEventArgs e)
{
if (e is null)
{
throw new ArgumentNullException(nameof(e));
}
UnhandledException?.Invoke(this, e);
}
}
}

View File

@ -2,13 +2,12 @@
// 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 Microsoft.AspNetCore.Components.Rendering;
namespace Microsoft.AspNetCore.Components
{
/// <summary>
/// Allows a component to notify the renderer that it should be rendered.
/// Allows a component to interact with its renderer.
/// </summary>
public readonly struct RenderHandle
{
@ -21,9 +20,25 @@ namespace Microsoft.AspNetCore.Components
_componentId = componentId;
}
/// <summary>
/// Gets the <see cref="Microsoft.AspNetCore.Components.Dispatcher" /> associated with the component.
/// </summary>
public Dispatcher Dispatcher
{
get
{
if (_renderer == null)
{
ThrowNotInitialized();
}
return _renderer.Dispatcher;
}
}
/// <summary>
/// Gets a value that indicates whether the <see cref="RenderHandle"/> has been
/// initialised and is ready to use.
/// initialized and is ready to use.
/// </summary>
public bool IsInitialized
=> _renderer != null;
@ -36,38 +51,15 @@ namespace Microsoft.AspNetCore.Components
{
if (_renderer == null)
{
throw new InvalidOperationException("The render handle is not yet assigned.");
ThrowNotInitialized();
}
_renderer.AddToRenderQueue(_componentId, renderFragment);
}
/// <summary>
/// Executes the supplied work item on the renderer's
/// synchronization context.
/// </summary>
/// <param name="workItem">The work item to execute.</param>
public Task InvokeAsync(Action workItem)
private static void ThrowNotInitialized()
{
if (_renderer == null)
{
throw new InvalidOperationException("The render handle is not yet assigned.");
}
return _renderer.InvokeAsync(workItem);
}
/// <summary>
/// Executes the supplied work item on the renderer's
/// synchronization context.
/// </summary>
/// <param name="workItem">The work item to execute.</param>
public Task InvokeAsync(Func<Task> workItem)
{
if (_renderer == null)
{
throw new InvalidOperationException("The render handle is not yet assigned.");
}
return _renderer.InvokeAsync(workItem);
throw new InvalidOperationException("The render handle is not yet assigned.");
}
}
}

View File

@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.ExceptionServices;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.RenderTree;
@ -28,14 +29,15 @@ namespace Microsoft.AspNetCore.Components.Rendering
/// </summary>
/// <param name="serviceProvider">The <see cref="IServiceProvider"/> to use to instantiate components.</param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
/// <param name="dispatcher">The <see cref="IDispatcher"/> to be for invoking user actions into the <see cref="Renderer"/> context.</param>
/// <param name="htmlEncoder">A <see cref="Func{T, TResult}"/> that will HTML encode the given string.</param>
public HtmlRenderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory, IDispatcher dispatcher, Func<string, string> htmlEncoder)
: base(serviceProvider, loggerFactory, dispatcher)
public HtmlRenderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory, Func<string, string> htmlEncoder)
: base(serviceProvider, loggerFactory)
{
_htmlEncoder = htmlEncoder;
}
public override Dispatcher Dispatcher { get; } = Dispatcher.CreateDefault();
/// <inheritdoc />
protected override Task UpdateDisplayAsync(in RenderBatch renderBatch)
{

View File

@ -22,7 +22,6 @@ namespace Microsoft.AspNetCore.Components.Rendering
private readonly RenderBatchBuilder _batchBuilder = new RenderBatchBuilder();
private readonly Dictionary<int, EventCallback> _eventBindings = new Dictionary<int, EventCallback>();
private readonly Dictionary<int, int> _eventHandlerIdReplacements = new Dictionary<int, int>();
private readonly IDispatcher _dispatcher;
private readonly ILogger<Renderer> _logger;
private int _nextComponentId = 0; // TODO: change to 'long' when Mono .NET->JS interop supports it
@ -37,19 +36,11 @@ namespace Microsoft.AspNetCore.Components.Rendering
{
add
{
if (!(_dispatcher is RendererSynchronizationContext rendererSynchronizationContext))
{
return;
}
rendererSynchronizationContext.UnhandledException += value;
Dispatcher.UnhandledException += value;
}
remove
{
if (!(_dispatcher is RendererSynchronizationContext rendererSynchronizationContext))
{
return;
}
rendererSynchronizationContext.UnhandledException -= value;
Dispatcher.UnhandledException -= value;
}
}
@ -72,25 +63,13 @@ namespace Microsoft.AspNetCore.Components.Rendering
_componentFactory = new ComponentFactory(serviceProvider);
_logger = loggerFactory.CreateLogger<Renderer>();
_componentFactory = new ComponentFactory(serviceProvider);
}
/// <summary>
/// Constructs an instance of <see cref="Renderer"/>.
/// Gets the <see cref="Microsoft.AspNetCore.Components.Dispatcher" /> associated with this <see cref="Renderer" />.
/// </summary>
/// <param name="serviceProvider">The <see cref="IServiceProvider"/> to be used when initializing components.</param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
/// <param name="dispatcher">The <see cref="IDispatcher"/> to be for invoking user actions into the <see cref="Renderer"/> context.</param>
public Renderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory, IDispatcher dispatcher)
: this(serviceProvider, loggerFactory)
{
_dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
}
/// <summary>
/// Creates an <see cref="IDispatcher"/> that can be used with one or more <see cref="Renderer"/>.
/// </summary>
/// <returns>The <see cref="IDispatcher"/>.</returns>
public static IDispatcher CreateDefaultDispatcher() => new RendererSynchronizationContext();
public abstract Dispatcher Dispatcher { get; }
/// <summary>
/// Constructs a new component of the specified type.
@ -272,58 +251,6 @@ namespace Microsoft.AspNetCore.Components.Rendering
return GetErrorHandledTask(task);
}
/// <summary>
/// Executes the supplied work item on the renderer's
/// synchronization context.
/// </summary>
/// <param name="workItem">The work item to execute.</param>
public virtual Task InvokeAsync(Action workItem)
{
// This is for example when we run on a system with a single thread, like WebAssembly.
if (_dispatcher == null)
{
workItem();
return Task.CompletedTask;
}
if (SynchronizationContext.Current == _dispatcher)
{
// This is an optimization for when the dispatcher is also a syncronization context, like in the default case.
// No need to dispatch. Avoid deadlock by invoking directly.
workItem();
return Task.CompletedTask;
}
else
{
return _dispatcher.InvokeAsync(workItem);
}
}
/// <summary>
/// Executes the supplied work item on the renderer's
/// synchronization context.
/// </summary>
/// <param name="workItem">The work item to execute.</param>
public virtual Task InvokeAsync(Func<Task> workItem)
{
// This is for example when we run on a system with a single thread, like WebAssembly.
if (_dispatcher == null)
{
return workItem();
}
if (SynchronizationContext.Current == _dispatcher)
{
// This is an optimization for when the dispatcher is also a syncronization context, like in the default case.
// No need to dispatch. Avoid deadlock by invoking directly.
return workItem();
}
else
{
return _dispatcher.InvokeAsync(workItem);
}
}
internal void InstantiateChildComponentOnFrame(ref RenderTreeFrame frame, int parentComponentId)
{
if (frame.FrameType != RenderTreeFrameType.Component)
@ -448,17 +375,16 @@ namespace Microsoft.AspNetCore.Components.Rendering
private void EnsureSynchronizationContext()
{
// When the IDispatcher is a synchronization context
// Render operations are not thread-safe, so they need to be serialized.
// Render operations are not thread-safe, so they need to be serialized by the dispatcher.
// Plus, any other logic that mutates state accessed during rendering also
// needs not to run concurrently with rendering so should be dispatched to
// the renderer's sync context.
if (_dispatcher is SynchronizationContext synchronizationContext && SynchronizationContext.Current != synchronizationContext)
if (!Dispatcher.CheckAccess())
{
throw new InvalidOperationException(
"The current thread is not associated with the renderer's synchronization context. " +
"Use Invoke() or InvokeAsync() to switch execution to the renderer's synchronization " +
"context when triggering rendering or modifying any state accessed during rendering.");
"The current thread is not associated with the Dispatcher. " +
"Use Invoke() or InvokeAsync() to switch execution to the Dispatcher when " +
"triggering rendering or modifying any state accessed during rendering.");
}
}

View File

@ -9,7 +9,7 @@ using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Components.Rendering
{
[DebuggerDisplay("{_state,nq}")]
internal class RendererSynchronizationContext : SynchronizationContext, IDispatcher
internal class RendererSynchronizationContext : SynchronizationContext
{
private static readonly ContextCallback ExecutionContextThunk = (object state) =>
{

View File

@ -0,0 +1,66 @@
// 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;
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Components.Rendering
{
internal class RendererSynchronizationContextDispatcher : Dispatcher
{
private readonly RendererSynchronizationContext _context;
public RendererSynchronizationContextDispatcher()
{
_context = new RendererSynchronizationContext();
_context.UnhandledException += (sender, e) =>
{
OnUnhandledException(e);
};
}
public override bool CheckAccess() => SynchronizationContext.Current == _context;
public override Task InvokeAsync(Action workItem)
{
if (CheckAccess())
{
workItem();
return Task.CompletedTask;
}
return _context.InvokeAsync(workItem);
}
public override Task InvokeAsync(Func<Task> workItem)
{
if (CheckAccess())
{
return workItem();
}
return _context.InvokeAsync(workItem);
}
public override Task<TResult> InvokeAsync<TResult>(Func<TResult> workItem)
{
if (CheckAccess())
{
return Task.FromResult(workItem());
}
return _context.InvokeAsync<TResult>(workItem);
}
public override Task<TResult> InvokeAsync<TResult>(Func<Task<TResult>> workItem)
{
if (CheckAccess())
{
return workItem();
}
return _context.InvokeAsync<TResult>(workItem);
}
}
}

View File

@ -386,7 +386,7 @@ namespace Microsoft.AspNetCore.Components.Test
supplierParams.Add("Name", name);
}
renderer.InvokeAsync((Action)(() => supplier.SetParametersAsync(ParameterCollection.FromDictionary(supplierParams))));
renderer.Dispatcher.InvokeAsync((Action)(() => supplier.SetParametersAsync(ParameterCollection.FromDictionary(supplierParams))));
return supplier;
}

View File

@ -27,7 +27,7 @@ namespace Microsoft.AspNetCore.Components.Test
public void DisplaysComponentInsideLayout()
{
// Arrange/Act
_renderer.InvokeAsync(() => _pageDisplayComponent.SetParametersAsync(ParameterCollection.FromDictionary(new Dictionary<string, object>
_renderer.Dispatcher.InvokeAsync(() => _pageDisplayComponent.SetParametersAsync(ParameterCollection.FromDictionary(new Dictionary<string, object>
{
{ nameof(PageDisplay.Page), typeof(ComponentWithLayout) }
})));
@ -84,7 +84,7 @@ namespace Microsoft.AspNetCore.Components.Test
public void DisplaysComponentInsideNestedLayout()
{
// Arrange/Act
_renderer.InvokeAsync(() => _pageDisplayComponent.SetParametersAsync(ParameterCollection.FromDictionary(new Dictionary<string, object>
_renderer.Dispatcher.InvokeAsync(() => _pageDisplayComponent.SetParametersAsync(ParameterCollection.FromDictionary(new Dictionary<string, object>
{
{ nameof(PageDisplay.Page), typeof(ComponentWithNestedLayout) }
})));
@ -111,13 +111,13 @@ namespace Microsoft.AspNetCore.Components.Test
public void CanChangeDisplayedPageWithSameLayout()
{
// Arrange
_renderer.InvokeAsync(() => _pageDisplayComponent.SetParametersAsync(ParameterCollection.FromDictionary(new Dictionary<string, object>
_renderer.Dispatcher.InvokeAsync(() => _pageDisplayComponent.SetParametersAsync(ParameterCollection.FromDictionary(new Dictionary<string, object>
{
{ nameof(PageDisplay.Page), typeof(ComponentWithLayout) }
})));
// Act
_renderer.InvokeAsync(() => _pageDisplayComponent.SetParametersAsync(ParameterCollection.FromDictionary(new Dictionary<string, object>
_renderer.Dispatcher.InvokeAsync(() => _pageDisplayComponent.SetParametersAsync(ParameterCollection.FromDictionary(new Dictionary<string, object>
{
{ nameof(PageDisplay.Page), typeof(DifferentComponentWithLayout) }
})));
@ -162,13 +162,13 @@ namespace Microsoft.AspNetCore.Components.Test
public void CanChangeDisplayedPageWithDifferentLayout()
{
// Arrange
_renderer.InvokeAsync(() => _pageDisplayComponent.SetParametersAsync(ParameterCollection.FromDictionary(new Dictionary<string, object>
_renderer.Dispatcher.InvokeAsync(() => _pageDisplayComponent.SetParametersAsync(ParameterCollection.FromDictionary(new Dictionary<string, object>
{
{ nameof(PageDisplay.Page), typeof(ComponentWithLayout) }
})));
// Act
_renderer.InvokeAsync(() => _pageDisplayComponent.SetParametersAsync(ParameterCollection.FromDictionary(new Dictionary<string, object>
_renderer.Dispatcher.InvokeAsync(() => _pageDisplayComponent.SetParametersAsync(ParameterCollection.FromDictionary(new Dictionary<string, object>
{
{ nameof(PageDisplay.Page), typeof(ComponentWithNestedLayout) }
})));

View File

@ -1795,10 +1795,12 @@ namespace Microsoft.AspNetCore.Components.Test
private class TestRenderer : Renderer
{
public TestRenderer() : base(new TestServiceProvider(), NullLoggerFactory.Instance, new RendererSynchronizationContext())
public TestRenderer() : base(new TestServiceProvider(), NullLoggerFactory.Instance)
{
}
public override Dispatcher Dispatcher { get; } = Dispatcher.CreateDefault();
protected override void HandleException(Exception exception)
=> throw new NotImplementedException();

View File

@ -2208,10 +2208,12 @@ namespace Microsoft.AspNetCore.Components.Test
private class FakeRenderer : Renderer
{
public FakeRenderer() : base(new TestServiceProvider(), NullLoggerFactory.Instance, new RendererSynchronizationContext())
public FakeRenderer() : base(new TestServiceProvider(), NullLoggerFactory.Instance)
{
}
public override Dispatcher Dispatcher { get; } = Dispatcher.CreateDefault();
protected override void HandleException(Exception exception)
=> throw new NotImplementedException();

View File

@ -186,7 +186,7 @@ namespace Microsoft.AspNetCore.Components.Test
// Act
var componentId = renderer.AssignRootComponentId(component);
var renderTask = renderer.InvokeAsync(() => renderer.RenderRootComponentAsync(componentId));
var renderTask = renderer.Dispatcher.InvokeAsync(() => renderer.RenderRootComponentAsync(componentId));
// Assert
Assert.False(renderTask.IsCompleted);
@ -239,7 +239,7 @@ namespace Microsoft.AspNetCore.Components.Test
// Act/Assert
var componentId = renderer.AssignRootComponentId(component);
var log = new ConcurrentQueue<(int id, NestedAsyncComponent.EventType @event)>();
await renderer.InvokeAsync(() => renderer.RenderRootComponentAsync(componentId, ParameterCollection.FromDictionary(new Dictionary<string, object>
await renderer.Dispatcher.InvokeAsync(() => renderer.RenderRootComponentAsync(componentId, ParameterCollection.FromDictionary(new Dictionary<string, object>
{
[EventActionsName] = new Dictionary<int, IList<NestedAsyncComponent.ExecutionAction>>
{
@ -283,7 +283,7 @@ namespace Microsoft.AspNetCore.Components.Test
// Act/Assert
var componentId = renderer.AssignRootComponentId(component);
var log = new ConcurrentQueue<(int id, NestedAsyncComponent.EventType @event)>();
await renderer.InvokeAsync(() => renderer.RenderRootComponentAsync(componentId, ParameterCollection.FromDictionary(new Dictionary<string, object>
await renderer.Dispatcher.InvokeAsync(() => renderer.RenderRootComponentAsync(componentId, ParameterCollection.FromDictionary(new Dictionary<string, object>
{
[EventActionsName] = new Dictionary<int, IList<NestedAsyncComponent.ExecutionAction>>
{
@ -327,7 +327,7 @@ namespace Microsoft.AspNetCore.Components.Test
// Act/Assert
var componentId = renderer.AssignRootComponentId(component);
var log = new ConcurrentQueue<(int id, NestedAsyncComponent.EventType @event)>();
await renderer.InvokeAsync(() => renderer.RenderRootComponentAsync(componentId, ParameterCollection.FromDictionary(new Dictionary<string, object>
await renderer.Dispatcher.InvokeAsync(() => renderer.RenderRootComponentAsync(componentId, ParameterCollection.FromDictionary(new Dictionary<string, object>
{
[EventActionsName] = new Dictionary<int, IList<NestedAsyncComponent.ExecutionAction>>
{
@ -371,7 +371,7 @@ namespace Microsoft.AspNetCore.Components.Test
// Act/Assert
var componentId = renderer.AssignRootComponentId(component);
var log = new ConcurrentQueue<(int id, NestedAsyncComponent.EventType @event)>();
await renderer.InvokeAsync(() => renderer.RenderRootComponentAsync(componentId, ParameterCollection.FromDictionary(new Dictionary<string, object>
await renderer.Dispatcher.InvokeAsync(() => renderer.RenderRootComponentAsync(componentId, ParameterCollection.FromDictionary(new Dictionary<string, object>
{
[EventActionsName] = new Dictionary<int, IList<NestedAsyncComponent.ExecutionAction>>
{
@ -2017,7 +2017,7 @@ namespace Microsoft.AspNetCore.Components.Test
// Act/Assert: If a disposed component requests a render, it's a no-op
var renderHandle = ((FakeComponent)childComponent3).RenderHandle;
renderHandle.InvokeAsync(() => renderHandle.Render(builder
renderHandle.Dispatcher.InvokeAsync(() => renderHandle.Render(builder
=> throw new NotImplementedException("Should not be invoked")));
Assert.Equal(2, renderer.Batches.Count);
}
@ -2906,7 +2906,7 @@ namespace Microsoft.AspNetCore.Components.Test
var asyncExceptionTcs = new TaskCompletionSource<object>();
taskToAwait = asyncExceptionTcs.Task;
await renderer.InvokeAsync(component.TriggerRender);
await renderer.Dispatcher.InvokeAsync(component.TriggerRender);
// Act
var exception = new InvalidOperationException();
@ -3373,10 +3373,12 @@ namespace Microsoft.AspNetCore.Components.Test
private class NoOpRenderer : Renderer
{
public NoOpRenderer() : base(new TestServiceProvider(), NullLoggerFactory.Instance, new RendererSynchronizationContext())
public NoOpRenderer() : base(new TestServiceProvider(), NullLoggerFactory.Instance)
{
}
public override Dispatcher Dispatcher { get; } = Dispatcher.CreateDefault();
public new int AssignRootComponentId(IComponent component)
=> base.AssignRootComponentId(component);
@ -3410,7 +3412,7 @@ namespace Microsoft.AspNetCore.Components.Test
public void TriggerRender()
{
var t = _renderHandle.InvokeAsync(() => _renderHandle.Render(_renderFragment));
var t = _renderHandle.Dispatcher.InvokeAsync(() => _renderHandle.Render(_renderFragment));
// This should always be run synchronously
Assert.True(t.IsCompleted);
if (t.IsFaulted)
@ -3660,7 +3662,7 @@ namespace Microsoft.AspNetCore.Components.Test
{
foreach (var renderHandle in _renderHandles)
{
renderHandle.InvokeAsync(() => renderHandle.Render(builder =>
renderHandle.Dispatcher.InvokeAsync(() => renderHandle.Render(builder =>
{
builder.AddContent(0, $"Hello from {nameof(MultiRendererComponent)}");
}));
@ -3821,7 +3823,7 @@ namespace Microsoft.AspNetCore.Components.Test
return TriggerRenderAsync();
}
public Task TriggerRenderAsync() => _renderHandle.InvokeAsync(() => _renderHandle.Render(RenderFragment));
public Task TriggerRenderAsync() => _renderHandle.Dispatcher.InvokeAsync(() => _renderHandle.Render(RenderFragment));
}
private void AssertStream(int expectedId, (int id, NestedAsyncComponent.EventType @event)[] logStream)

View File

@ -15,7 +15,6 @@ namespace Microsoft.AspNetCore.Components.Rendering
public abstract class HtmlRendererTestBase
{
protected readonly Func<string, string> _encoder = (string t) => HtmlEncoder.Default.Encode(t);
protected readonly IDispatcher Dispatcher = Renderer.CreateDefaultDispatcher();
protected abstract HtmlRenderer GetHtmlRenderer(IServiceProvider serviceProvider);
@ -33,7 +32,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
var htmlRenderer = GetHtmlRenderer(serviceProvider);
// Act
var result = GetResult(Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<TestComponent>(ParameterCollection.Empty)));
var result = GetResult(htmlRenderer.Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<TestComponent>(ParameterCollection.Empty)));
// Assert
Assert.Equal(expectedHtml, result);
@ -43,7 +42,6 @@ namespace Microsoft.AspNetCore.Components.Rendering
public void RenderComponentAsync_CanRenderSimpleComponent()
{
// Arrange
var dispatcher = Renderer.CreateDefaultDispatcher();
var expectedHtml = new[] { "<", "p", ">", "Hello world!", "</", "p", ">" };
var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb =>
{
@ -54,7 +52,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
var htmlRenderer = GetHtmlRenderer(serviceProvider);
// Act
var result = GetResult(Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<TestComponent>(ParameterCollection.Empty)));
var result = GetResult(htmlRenderer.Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<TestComponent>(ParameterCollection.Empty)));
// Assert
Assert.Equal(expectedHtml, result);
@ -64,7 +62,6 @@ namespace Microsoft.AspNetCore.Components.Rendering
public void RenderComponentAsync_HtmlEncodesContent()
{
// Arrange
var dispatcher = Renderer.CreateDefaultDispatcher();
var expectedHtml = new[] { "<", "p", ">", "&lt;Hello world!&gt;", "</", "p", ">" };
var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb =>
{
@ -75,7 +72,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
var htmlRenderer = GetHtmlRenderer(serviceProvider);
// Act
var result = GetResult(Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<TestComponent>(ParameterCollection.Empty)));
var result = GetResult(htmlRenderer.Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<TestComponent>(ParameterCollection.Empty)));
// Assert
Assert.Equal(expectedHtml, result);
@ -96,7 +93,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
var htmlRenderer = GetHtmlRenderer(serviceProvider);
// Act
var result = GetResult(Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<TestComponent>(ParameterCollection.Empty)));
var result = GetResult(htmlRenderer.Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<TestComponent>(ParameterCollection.Empty)));
// Assert
Assert.Equal(expectedHtml, result);
@ -119,7 +116,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
var htmlRenderer = GetHtmlRenderer(serviceProvider);
// Act
var result = GetResult(Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<TestComponent>(ParameterCollection.Empty)));
var result = GetResult(htmlRenderer.Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<TestComponent>(ParameterCollection.Empty)));
// Assert
Assert.Equal(expectedHtml, result);
@ -153,7 +150,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
var htmlRenderer = GetHtmlRenderer(serviceProvider);
// Act
var result = GetResult(Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<TestComponent>(ParameterCollection.Empty)));
var result = GetResult(htmlRenderer.Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<TestComponent>(ParameterCollection.Empty)));
// Assert
Assert.Equal(expectedHtml, result);
@ -175,7 +172,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
var htmlRenderer = GetHtmlRenderer(serviceProvider);
// Act
var result = GetResult(Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<TestComponent>(ParameterCollection.Empty)));
var result = GetResult(htmlRenderer.Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<TestComponent>(ParameterCollection.Empty)));
// Assert
Assert.Equal(expectedHtml, result);
@ -196,7 +193,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
var htmlRenderer = GetHtmlRenderer(serviceProvider);
// Act
var result = GetResult(Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<TestComponent>(ParameterCollection.Empty)));
var result = GetResult(htmlRenderer.Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<TestComponent>(ParameterCollection.Empty)));
// Assert
Assert.Equal(expectedHtml, result);
@ -217,7 +214,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
var htmlRenderer = GetHtmlRenderer(serviceProvider);
// Act
var result = GetResult(Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<TestComponent>(ParameterCollection.Empty)));
var result = GetResult(htmlRenderer.Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<TestComponent>(ParameterCollection.Empty)));
// Assert
Assert.Equal(expectedHtml, result);
@ -240,7 +237,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
var htmlRenderer = GetHtmlRenderer(serviceProvider);
// Act
var result = GetResult(Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<TestComponent>(ParameterCollection.Empty)));
var result = GetResult(htmlRenderer.Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<TestComponent>(ParameterCollection.Empty)));
// Assert
Assert.Equal(expectedHtml, result);
@ -270,7 +267,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
var htmlRenderer = GetHtmlRenderer(serviceProvider);
// Act
var result = GetResult(Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<TestComponent>(ParameterCollection.Empty)));
var result = GetResult(htmlRenderer.Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<TestComponent>(ParameterCollection.Empty)));
// Assert
Assert.Equal(expectedHtml, result);
@ -299,7 +296,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
var htmlRenderer = GetHtmlRenderer(serviceProvider);
// Act
var result = GetResult(Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<TestComponent>(ParameterCollection.Empty)));
var result = GetResult(htmlRenderer.Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<TestComponent>(ParameterCollection.Empty)));
// Assert
Assert.Equal(expectedHtml, result);
@ -329,7 +326,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
var htmlRenderer = GetHtmlRenderer(serviceProvider);
// Act
var result = GetResult(Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<TestComponent>(ParameterCollection.Empty)));
var result = GetResult(htmlRenderer.Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<TestComponent>(ParameterCollection.Empty)));
// Assert
Assert.Equal(expectedHtml, result);
@ -360,7 +357,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
Action<UIChangeEventArgs> change = (UIChangeEventArgs changeArgs) => throw new InvalidOperationException();
// Act
var result = GetResult(Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<ComponentWithParameters>(
var result = GetResult(htmlRenderer.Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<ComponentWithParameters>(
new ParameterCollection(new[] {
RenderTreeFrame.Element(0,string.Empty),
RenderTreeFrame.Attribute(1,"update",change),
@ -391,7 +388,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
var htmlRenderer = GetHtmlRenderer(serviceProvider);
// Act
var result = GetResult(Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<TestComponent>(ParameterCollection.Empty)));
var result = GetResult(htmlRenderer.Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<TestComponent>(ParameterCollection.Empty)));
// Assert
Assert.Equal(expectedHtml, result);
@ -418,7 +415,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
var htmlRenderer = GetHtmlRenderer(serviceProvider);
// Act
var result = GetResult(Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<TestComponent>(ParameterCollection.Empty)));
var result = GetResult(htmlRenderer.Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<TestComponent>(ParameterCollection.Empty)));
// Assert
Assert.Equal(expectedHtml, result);
@ -468,7 +465,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
var htmlRenderer = GetHtmlRenderer(serviceProvider);
// Act
var result = await Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<AsyncComponent>(ParameterCollection.FromDictionary(new Dictionary<string, object>
var result = await htmlRenderer.Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<AsyncComponent>(ParameterCollection.FromDictionary(new Dictionary<string, object>
{
["Value"] = 10
})));
@ -481,8 +478,8 @@ namespace Microsoft.AspNetCore.Components.Rendering
public async Task CanRender_NestedAsyncComponents()
{
// Arrange
var dispatcher = Renderer.CreateDefaultDispatcher();
var expectedHtml = new[] {
var expectedHtml = new[]
{
"<", "p", ">", "20", "</", "p", ">",
"<", "p", ">", "80", "</", "p", ">"
};
@ -492,7 +489,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
var htmlRenderer = GetHtmlRenderer(serviceProvider);
// Act
var result = await Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<NestedAsyncComponent>(ParameterCollection.FromDictionary(new Dictionary<string, object>
var result = await htmlRenderer.Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<NestedAsyncComponent>(ParameterCollection.FromDictionary(new Dictionary<string, object>
{
["Nested"] = false,
["Value"] = 10

View File

@ -10,7 +10,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
{
protected override HtmlRenderer GetHtmlRenderer(IServiceProvider serviceProvider)
{
return new HtmlRenderer(serviceProvider, NullLoggerFactory.Instance, Dispatcher, _encoder);
return new HtmlRenderer(serviceProvider, NullLoggerFactory.Instance, _encoder);
}
}
}

View File

@ -56,14 +56,12 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
RendererRegistry rendererRegistry,
RemoteRenderer renderer,
IList<ComponentDescriptor> descriptors,
IDispatcher dispatcher,
RemoteJSRuntime jsRuntime,
CircuitHandler[] circuitHandlers,
ILogger logger)
{
CircuitId = circuitId;
_scope = scope ?? throw new ArgumentNullException(nameof(scope));
Dispatcher = dispatcher;
Client = client;
RendererRegistry = rendererRegistry ?? throw new ArgumentNullException(nameof(rendererRegistry));
Descriptors = descriptors ?? throw new ArgumentNullException(nameof(descriptors));
@ -96,11 +94,9 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
public IServiceProvider Services { get; }
public IDispatcher Dispatcher { get; }
public Task<ComponentRenderedText> PrerenderComponentAsync(Type componentType, ParameterCollection parameters)
{
return Dispatcher.InvokeAsync(async () =>
return Renderer.Dispatcher.InvokeAsync(async () =>
{
var result = await Renderer.RenderComponentAsync(componentType, parameters);
@ -137,12 +133,12 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
// Dispatch any buffered renders we accumulated during a disconnect.
// Note that while the rendering is async, we cannot await it here. The Task returned by ProcessBufferedRenderBatches relies on
// OnRenderCompleted to be invoked to complete, and SignalR does not allow concurrent hub method invocations.
var _ = Renderer.InvokeAsync(() => Renderer.ProcessBufferedRenderBatches());
var _ = Renderer.Dispatcher.InvokeAsync(() => Renderer.ProcessBufferedRenderBatches());
}
public async Task InitializeAsync(CancellationToken cancellationToken)
{
await Renderer.InvokeAsync(async () =>
await Renderer.Dispatcher.InvokeAsync(async () =>
{
try
{
@ -180,7 +176,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
try
{
await Renderer.InvokeAsync(() =>
await Renderer.Dispatcher.InvokeAsync(() =>
{
SetCurrentCircuitHost(this);
DotNetDispatcher.BeginInvoke(callId, assemblyName, methodIdentifier, dotNetObjectId, argsJson);
@ -291,7 +287,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
{
Log.DisposingCircuit(_logger, CircuitId);
await Renderer.InvokeAsync(async () =>
await Renderer.Dispatcher.InvokeAsync(async () =>
{
try
{

View File

@ -90,7 +90,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
{
if (DisconnectCore(circuitHost, connectionId))
{
circuitHandlerTask = circuitHost.Dispatcher.InvokeAsync(() => circuitHost.OnConnectionDownAsync(default));
circuitHandlerTask = circuitHost.Renderer.Dispatcher.InvokeAsync(() => circuitHost.OnConnectionDownAsync(default));
}
else
{
@ -189,7 +189,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
// Dispatch the circuit handlers inside the sync context to ensure the order of execution. CircuitHost executes circuit handlers inside of
//
circuitHandlerTask = circuitHost.Dispatcher.InvokeAsync(async () =>
circuitHandlerTask = circuitHost.Renderer.Dispatcher.InvokeAsync(async () =>
{
if (previouslyConnected)
{

View File

@ -71,14 +71,12 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
}
var rendererRegistry = new RendererRegistry();
var dispatcher = Renderer.CreateDefaultDispatcher();
var renderer = new RemoteRenderer(
scope.ServiceProvider,
_loggerFactory,
rendererRegistry,
jsRuntime,
client,
dispatcher,
encoder,
_loggerFactory.CreateLogger<RemoteRenderer>());
@ -93,7 +91,6 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
rendererRegistry,
renderer,
components,
dispatcher,
jsRuntime,
circuitHandlers,
_loggerFactory.CreateLogger<CircuitHost>());

View File

@ -41,10 +41,9 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
RendererRegistry rendererRegistry,
IJSRuntime jsRuntime,
CircuitClientProxy client,
IDispatcher dispatcher,
HtmlEncoder encoder,
ILogger logger)
: base(serviceProvider, loggerFactory, dispatcher, encoder.Encode)
: base(serviceProvider, loggerFactory, encoder.Encode)
{
_rendererRegistry = rendererRegistry;
_jsRuntime = jsRuntime;
@ -56,6 +55,8 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
internal ConcurrentQueue<PendingRender> PendingRenderBatches = new ConcurrentQueue<PendingRender>();
public override Dispatcher Dispatcher { get; } = Dispatcher.CreateDefault();
public int Id { get; }
/// <summary>

View File

@ -10,7 +10,6 @@ using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.Web.Rendering;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
@ -27,7 +26,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
{
// Arrange
var serviceScope = new Mock<IServiceScope>();
var remoteRenderer = GetRemoteRenderer(Renderer.CreateDefaultDispatcher());
var remoteRenderer = GetRemoteRenderer();
var circuitHost = TestCircuitHost.Create(
Guid.NewGuid().ToString(),
serviceScope.Object,
@ -50,7 +49,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
handler
.Setup(h => h.OnCircuitClosedAsync(It.IsAny<Circuit>(), It.IsAny<CancellationToken>()))
.Throws<InvalidTimeZoneException>();
var remoteRenderer = GetRemoteRenderer(Renderer.CreateDefaultDispatcher());
var remoteRenderer = GetRemoteRenderer();
var circuitHost = TestCircuitHost.Create(
Guid.NewGuid().ToString(),
serviceScope.Object,
@ -77,13 +76,13 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
{
// Arrange
var serviceScope = new Mock<IServiceScope>();
var remoteRenderer = GetRemoteRenderer(Renderer.CreateDefaultDispatcher());
var remoteRenderer = GetRemoteRenderer();
var circuitHost = TestCircuitHost.Create(
Guid.NewGuid().ToString(),
serviceScope.Object,
remoteRenderer);
var component = new DispatcherComponent(circuitHost.Dispatcher);
var component = new DispatcherComponent(circuitHost.Renderer.Dispatcher);
circuitHost.Renderer.AssignRootComponentId(component);
var original = SynchronizationContext.Current;
SynchronizationContext.SetSynchronizationContext(null);
@ -229,20 +228,19 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
handler2.VerifyAll();
}
private static TestRemoteRenderer GetRemoteRenderer(IDispatcher dispatcher)
private static TestRemoteRenderer GetRemoteRenderer()
{
return new TestRemoteRenderer(
Mock.Of<IServiceProvider>(),
new RendererRegistry(),
dispatcher,
Mock.Of<IJSRuntime>(),
Mock.Of<IClientProxy>());
}
private class TestRemoteRenderer : RemoteRenderer
{
public TestRemoteRenderer(IServiceProvider serviceProvider, RendererRegistry rendererRegistry, IDispatcher dispatcher, IJSRuntime jsRuntime, IClientProxy client)
: base(serviceProvider, NullLoggerFactory.Instance, rendererRegistry, jsRuntime, new CircuitClientProxy(client, "connection"), dispatcher, HtmlEncoder.Default, NullLogger.Instance)
public TestRemoteRenderer(IServiceProvider serviceProvider, RendererRegistry rendererRegistry, IJSRuntime jsRuntime, IClientProxy client)
: base(serviceProvider, NullLoggerFactory.Instance, rendererRegistry, jsRuntime, new CircuitClientProxy(client, "connection"), HtmlEncoder.Default, NullLogger.Instance)
{
}
@ -257,12 +255,12 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
private class DispatcherComponent : ComponentBase, IDisposable
{
public DispatcherComponent(IDispatcher dispatcher)
public DispatcherComponent(Dispatcher dispatcher)
{
Dispatcher = dispatcher;
}
public IDispatcher Dispatcher { get; }
public Dispatcher Dispatcher { get; }
public bool Called { get; private set; }
public void Dispose()

View File

@ -268,7 +268,6 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
new RendererRegistry(),
jsRuntime.Object,
circuitClientProxy,
Dispatcher,
HtmlEncoder.Default,
NullLogger.Instance);
}
@ -313,7 +312,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
public void TriggerRender()
{
var task = _renderHandle.InvokeAsync(() => _renderHandle.Render(_renderFragment));
var task = _renderHandle.Dispatcher.InvokeAsync(() => _renderHandle.Render(_renderFragment));
Assert.True(task.IsCompletedSuccessfully);
}
}
@ -342,7 +341,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
public void TriggerRender()
{
var task = _renderHandle.InvokeAsync(() => _renderHandle.Render(Content));
var task = _renderHandle.Dispatcher.InvokeAsync(() => _renderHandle.Render(Content));
Assert.True(task.IsCompletedSuccessfully);
}
}

View File

@ -55,7 +55,7 @@ namespace Microsoft.AspNetCore.Components.Server
// Assert
AssertBinaryContents(bytes, /* startIndex */ 0,
0, // Length of UpdatedComponents
0, // Length of ReferenceFrames
0, // Length of ReferenceFrames
3, 123, int.MaxValue, int.MinValue, // DisposedComponentIds as length-prefixed array
0, // Length of DisposedEventHandlerIds
@ -122,7 +122,7 @@ namespace Microsoft.AspNetCore.Components.Server
2, // Length of UpdatedComponents
0, // Index of UpdatedComponents[0]
8, // Index of UpdatedComponents[1]
0, // Length of ReferenceFrames
0, // Length of DisposedComponentIds
0, // Length of DisposedEventHandlerIds
@ -159,7 +159,7 @@ namespace Microsoft.AspNetCore.Components.Server
var bytes = Serialize(new RenderBatch(
new ArrayRange<RenderTreeDiff>(new[]
{
new RenderTreeDiff(123, editsSegment)
new RenderTreeDiff(123, editsSegment)
}, 1),
default,
default,
@ -360,7 +360,7 @@ namespace Microsoft.AspNetCore.Components.Server
shift += 7;
numBytesRead++;
}
return result;
}
@ -376,10 +376,12 @@ namespace Microsoft.AspNetCore.Components.Server
class FakeRenderer : Renderer
{
public FakeRenderer()
: base(new ServiceCollection().BuildServiceProvider(), NullLoggerFactory.Instance, new RendererSynchronizationContext())
: base(new ServiceCollection().BuildServiceProvider(), NullLoggerFactory.Instance)
{
}
public override Dispatcher Dispatcher { get; } = Dispatcher.CreateDefault();
protected override void HandleException(Exception exception)
{
throw new NotImplementedException();

View File

@ -7,20 +7,19 @@ using System.Runtime.ExceptionServices;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.Web.Rendering;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Microsoft.Extensions.Options;
using Moq;
namespace Microsoft.AspNetCore.Components.Server.Circuits
{
internal class TestCircuitHost : CircuitHost
{
private TestCircuitHost(string circuitId, IServiceScope scope, CircuitClientProxy client, RendererRegistry rendererRegistry, RemoteRenderer renderer, IList<ComponentDescriptor> descriptors, IDispatcher dispatcher, RemoteJSRuntime jsRuntime, CircuitHandler[] circuitHandlers, ILogger logger)
: base(circuitId, scope, client, rendererRegistry, renderer, descriptors, dispatcher, jsRuntime, circuitHandlers, logger)
private TestCircuitHost(string circuitId, IServiceScope scope, CircuitClientProxy client, RendererRegistry rendererRegistry, RemoteRenderer renderer, IList<ComponentDescriptor> descriptors, RemoteJSRuntime jsRuntime, CircuitHandler[] circuitHandlers, ILogger logger)
: base(circuitId, scope, client, rendererRegistry, renderer, descriptors, jsRuntime, circuitHandlers, logger)
{
}
@ -40,7 +39,6 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
clientProxy = clientProxy ?? new CircuitClientProxy(Mock.Of<IClientProxy>(), Guid.NewGuid().ToString());
var renderRegistry = new RendererRegistry();
var jsRuntime = new RemoteJSRuntime(Options.Create(new CircuitOptions()));
var dispatcher = Rendering.Renderer.CreateDefaultDispatcher();
if (remoteRenderer == null)
{
@ -50,7 +48,6 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
new RendererRegistry(),
jsRuntime,
clientProxy,
dispatcher,
HtmlEncoder.Default,
NullLogger.Instance);
}
@ -63,7 +60,6 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
renderRegistry,
remoteRenderer,
new List<ComponentDescriptor>(),
dispatcher,
jsRuntime,
handlers,
NullLogger<CircuitHost>.Instance);

View File

@ -4,7 +4,6 @@
using System;
using System.Runtime.ExceptionServices;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.RenderTree;
using Xunit;
@ -29,7 +28,7 @@ namespace Microsoft.AspNetCore.Components.Test.Helpers
// We do it this way so that we don't have to be doing renderer.Invoke on each and every test.
public void TriggerRender()
{
var t = _renderHandle.InvokeAsync(() => _renderHandle.Render(BuildRenderTree));
var t = _renderHandle.Dispatcher.InvokeAsync(() => _renderHandle.Render(BuildRenderTree));
// This should always be run synchronously
Assert.True(t.IsCompleted);
if (t.IsFaulted)

View File

@ -18,14 +18,18 @@ namespace Microsoft.AspNetCore.Components.Test.Helpers
{
}
public TestRenderer(IDispatcher dispatcher) : base(new TestServiceProvider(), NullLoggerFactory.Instance, dispatcher)
public TestRenderer(Dispatcher dispatcher) : base(new TestServiceProvider(), NullLoggerFactory.Instance)
{
Dispatcher = dispatcher;
}
public TestRenderer(IServiceProvider serviceProvider) : base(serviceProvider, NullLoggerFactory.Instance, new RendererSynchronizationContext())
public TestRenderer(IServiceProvider serviceProvider) : base(serviceProvider, NullLoggerFactory.Instance)
{
Dispatcher = Dispatcher.CreateDefault();
}
public override Dispatcher Dispatcher { get; }
public Action OnExceptionHandled { get; set; }
public Action<RenderBatch> OnUpdateDisplay { get; set; }
@ -46,21 +50,21 @@ namespace Microsoft.AspNetCore.Components.Test.Helpers
public void RenderRootComponent(int componentId, ParameterCollection? parameters = default)
{
var task = InvokeAsync(() => base.RenderRootComponentAsync(componentId, parameters ?? ParameterCollection.Empty));
var task = Dispatcher.InvokeAsync(() => base.RenderRootComponentAsync(componentId, parameters ?? ParameterCollection.Empty));
UnwrapTask(task);
}
public new Task RenderRootComponentAsync(int componentId)
=> InvokeAsync(() => base.RenderRootComponentAsync(componentId));
=> Dispatcher.InvokeAsync(() => base.RenderRootComponentAsync(componentId));
public new Task RenderRootComponentAsync(int componentId, ParameterCollection parameters)
=> InvokeAsync(() => base.RenderRootComponentAsync(componentId, parameters));
=> Dispatcher.InvokeAsync(() => base.RenderRootComponentAsync(componentId, parameters));
public Task DispatchEventAsync(int eventHandlerId, UIEventArgs args)
=> InvokeAsync(() => base.DispatchEventAsync(eventHandlerId, null, args));
=> Dispatcher.InvokeAsync(() => base.DispatchEventAsync(eventHandlerId, null, args));
public new Task DispatchEventAsync(int eventHandlerId, EventFieldInfo eventFieldInfo, UIEventArgs args)
=> InvokeAsync(() => base.DispatchEventAsync(eventHandlerId, eventFieldInfo, args));
=> Dispatcher.InvokeAsync(() => base.DispatchEventAsync(eventHandlerId, eventFieldInfo, args));
private static Task UnwrapTask(Task task)
{

View File

@ -33,7 +33,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
appElement.FindElement(By.Id("run-without-dispatch")).Click();
Browser.Contains(
$"{typeof(InvalidOperationException).FullName}: The current thread is not associated with the renderer's synchronization context",
$"{typeof(InvalidOperationException).FullName}: The current thread is not associated with the Dispatcher. Use Invoke() or InvokeAsync() to switch execution to the Dispatcher when triggering rendering or modifying any state accessed during rendering.",
() => result.Text);
}
}

View File

@ -335,10 +335,12 @@ namespace Ignitor
class FakeRenderer : Renderer
{
public FakeRenderer()
: base(new ServiceCollection().BuildServiceProvider(), NullLoggerFactory.Instance, new RendererSynchronizationContext())
: base(new ServiceCollection().BuildServiceProvider(), NullLoggerFactory.Instance)
{
}
public override Dispatcher Dispatcher { get; } = Dispatcher.CreateDefault();
protected override void HandleException(Exception exception)
{
throw new NotImplementedException();

View File

@ -289,10 +289,12 @@ namespace Ignitor
public class FakeRenderer : Renderer
{
public FakeRenderer()
: base(new ServiceCollection().BuildServiceProvider(), NullLoggerFactory.Instance, new RendererSynchronizationContext())
: base(new ServiceCollection().BuildServiceProvider(), NullLoggerFactory.Instance)
{
}
public override Dispatcher Dispatcher { get; } = Dispatcher.CreateDefault();
protected override void HandleException(Exception exception)
{
throw new NotImplementedException();

View File

@ -30,16 +30,14 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponents
HttpContext httpContext,
Type componentType)
{
var dispatcher = Renderer.CreateDefaultDispatcher();
InitializeUriHelper(httpContext);
var loggerFactory = (ILoggerFactory)httpContext.RequestServices.GetService(typeof (ILoggerFactory));
using (var htmlRenderer = new HtmlRenderer(httpContext.RequestServices, loggerFactory, dispatcher, _encoder.Encode))
using (var htmlRenderer = new HtmlRenderer(httpContext.RequestServices, loggerFactory, _encoder.Encode))
{
ComponentRenderedText result = default;
try
{
result = await dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync(
result = await htmlRenderer.Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync(
componentType,
parameters));
}