* Moves the Synchronization context from the remote renderer to the base renderer. * Removes all the locking from the base renderer.
This commit is contained in:
parent
2d73f7ad2b
commit
f456e3d153
|
|
@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Components.Rendering;
|
|||
using Microsoft.JSInterop;
|
||||
using Mono.WebAssembly.Interop;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Rendering
|
||||
|
|
@ -31,9 +32,6 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
|
|||
_webAssemblyRendererId = RendererRegistry.Current.Add(this);
|
||||
}
|
||||
|
||||
internal void DispatchBrowserEvent(int componentId, int eventHandlerId, UIEventArgs eventArgs)
|
||||
=> DispatchEvent(componentId, eventHandlerId, eventArgs);
|
||||
|
||||
/// <summary>
|
||||
/// Attaches a new root component to the renderer,
|
||||
/// causing it to be displayed in the specified DOM element.
|
||||
|
|
|
|||
|
|
@ -377,7 +377,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
|
|||
{
|
||||
var renderer = new TestRenderer();
|
||||
renderer.AttachComponent(component);
|
||||
var task = component.SetParametersAsync(ParameterCollection.Empty);
|
||||
var task = renderer.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)
|
||||
|
|
@ -434,7 +434,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
|
|||
|
||||
private class TestRenderer : Renderer
|
||||
{
|
||||
public TestRenderer() : base(new TestServiceProvider())
|
||||
public TestRenderer() : base(new TestServiceProvider(), CreateDefaultDispatcher())
|
||||
{
|
||||
}
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -87,7 +87,7 @@ namespace Microsoft.AspNetCore.Components.Performance
|
|||
private class FakeRenderer : Renderer
|
||||
{
|
||||
public FakeRenderer()
|
||||
: base(new TestServiceProvider())
|
||||
: base(new TestServiceProvider(), new RendererSynchronizationContext())
|
||||
{
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// 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.Components.Rendering;
|
||||
|
|
@ -49,7 +49,13 @@ namespace Microsoft.AspNetCore.Components
|
|||
/// </summary>
|
||||
/// <param name="workItem">The work item to execute.</param>
|
||||
public Task Invoke(Action workItem)
|
||||
=> _renderer.Invoke(workItem);
|
||||
{
|
||||
if (_renderer == null)
|
||||
{
|
||||
throw new InvalidOperationException("The render handle is not yet assigned.");
|
||||
}
|
||||
return _renderer.Invoke(workItem);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes the supplied work item on the renderer's
|
||||
|
|
@ -57,6 +63,12 @@ namespace Microsoft.AspNetCore.Components
|
|||
/// </summary>
|
||||
/// <param name="workItem">The work item to execute.</param>
|
||||
public Task InvokeAsync(Func<Task> workItem)
|
||||
=> _renderer.InvokeAsync(workItem);
|
||||
{
|
||||
if (_renderer == null)
|
||||
{
|
||||
throw new InvalidOperationException("The render handle is not yet assigned.");
|
||||
}
|
||||
return _renderer.InvokeAsync(workItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,8 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
/// </summary>
|
||||
/// <param name="serviceProvider">The <see cref="IServiceProvider"/> to use to instantiate components.</param>
|
||||
/// <param name="htmlEncoder">A <see cref="Func{T, TResult}"/> that will HTML encode the given string.</param>
|
||||
public HtmlRenderer(IServiceProvider serviceProvider, Func<string, string> htmlEncoder) : base(serviceProvider)
|
||||
public HtmlRenderer(IServiceProvider serviceProvider, Func<string, string> htmlEncoder, IDispatcher dispatcher)
|
||||
: base(serviceProvider, dispatcher)
|
||||
{
|
||||
_htmlEncoder = htmlEncoder;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
// 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;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Rendering
|
||||
{
|
||||
/// <summary>
|
||||
/// Dispatches external actions to be executed on the context of a <see cref="Renderer"/>.
|
||||
/// </summary>
|
||||
public interface IDispatcher
|
||||
{
|
||||
/// <summary>
|
||||
/// Invokes the given <see cref="Action"/> in the context of the associated <see cref="Renderer"/>.
|
||||
/// </summary>
|
||||
/// <param name="action">The action to execute.</param>
|
||||
/// <returns>A <see cref="Task"/> that will be completed when the action has finished executing.</returns>
|
||||
Task Invoke(Action action);
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the given <see cref="Func{TResult}"/> in the context of the associated <see cref="Renderer"/>.
|
||||
/// </summary>
|
||||
/// <param name="asyncAction">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> asyncAction);
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the given <see cref="Func{TResult}"/> in the context of the associated <see cref="Renderer"/>.
|
||||
/// </summary>
|
||||
/// <param name="function">The function to execute.</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> that will be completed when the function has finished executing.</returns>
|
||||
Task<TResult> Invoke<TResult>(Func<TResult> function);
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the given <see cref="Func{TResult}"/> in the context of the associated <see cref="Renderer"/>.
|
||||
/// </summary>
|
||||
/// <param name="asyncAction">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>> asyncFunction);
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components.RenderTree;
|
||||
|
||||
|
|
@ -20,28 +21,61 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
private readonly Dictionary<int, ComponentState> _componentStateById = new Dictionary<int, ComponentState>();
|
||||
private readonly RenderBatchBuilder _batchBuilder = new RenderBatchBuilder();
|
||||
private readonly Dictionary<int, EventHandlerInvoker> _eventBindings = new Dictionary<int, EventHandlerInvoker>();
|
||||
private IDispatcher _dispatcher;
|
||||
|
||||
private int _nextComponentId = 0; // TODO: change to 'long' when Mono .NET->JS interop supports it
|
||||
private bool _isBatchInProgress;
|
||||
private int _lastEventHandlerId = 0;
|
||||
private List<Task> _pendingTasks;
|
||||
|
||||
// We need to introduce locking as we don't know if we are executing
|
||||
// under a synchronization context that limits the ammount of concurrency
|
||||
// that can happen when async callbacks are executed.
|
||||
// As a result, we have to protect the _pendingTask list and the
|
||||
// _batchBuilder render queue from concurrent modifications.
|
||||
private object _asyncWorkLock = new object();
|
||||
/// <summary>
|
||||
/// Allows the caller to handle exceptions from the SynchronizationContext when one is available.
|
||||
/// </summary>
|
||||
public event UnhandledExceptionEventHandler UnhandledSynchronizationException
|
||||
{
|
||||
add
|
||||
{
|
||||
if (!(_dispatcher is RendererSynchronizationContext rendererSynchronizationContext))
|
||||
{
|
||||
return;
|
||||
}
|
||||
rendererSynchronizationContext.UnhandledException += value;
|
||||
}
|
||||
remove
|
||||
{
|
||||
if (!(_dispatcher is RendererSynchronizationContext rendererSynchronizationContext))
|
||||
{
|
||||
return;
|
||||
}
|
||||
rendererSynchronizationContext.UnhandledException -= value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructs an instance of <see cref="Renderer"/>.
|
||||
/// </summary>
|
||||
/// <param name="serviceProvider">The <see cref="IServiceProvider"/> to be used when initialising components.</param>
|
||||
/// <param name="serviceProvider">The <see cref="IServiceProvider"/> to be used when initializing components.</param>
|
||||
public Renderer(IServiceProvider serviceProvider)
|
||||
{
|
||||
_componentFactory = new ComponentFactory(serviceProvider);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructs an instance of <see cref="Renderer"/>.
|
||||
/// </summary>
|
||||
/// <param name="serviceProvider">The <see cref="IServiceProvider"/> to be used when initializing components.</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, IDispatcher dispatcher) : this(serviceProvider)
|
||||
{
|
||||
_dispatcher = 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();
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new component of the specified type.
|
||||
/// </summary>
|
||||
|
|
@ -198,14 +232,11 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
while (_pendingTasks.Count > 0)
|
||||
{
|
||||
Task pendingWork;
|
||||
lock (_asyncWorkLock)
|
||||
{
|
||||
// Create a Task that represents the remaining ongoing work for the rendering process
|
||||
pendingWork = Task.WhenAll(_pendingTasks);
|
||||
// Create a Task that represents the remaining ongoing work for the rendering process
|
||||
pendingWork = Task.WhenAll(_pendingTasks);
|
||||
|
||||
// Clear all pending work.
|
||||
_pendingTasks.Clear();
|
||||
}
|
||||
// Clear all pending work.
|
||||
_pendingTasks.Clear();
|
||||
|
||||
// new work might be added before we check again as a result of waiting for all
|
||||
// the child components to finish executing SetParametersAsync
|
||||
|
|
@ -238,6 +269,8 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
/// <param name="eventArgs">Arguments to be passed to the event handler.</param>
|
||||
public void DispatchEvent(int componentId, int eventHandlerId, UIEventArgs eventArgs)
|
||||
{
|
||||
EnsureSynchronizationContext();
|
||||
|
||||
if (_eventBindings.TryGetValue(eventHandlerId, out var binding))
|
||||
{
|
||||
// The event handler might request multiple renders in sequence. Capture them
|
||||
|
|
@ -266,9 +299,24 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
/// <param name="workItem">The work item to execute.</param>
|
||||
public virtual Task Invoke(Action workItem)
|
||||
{
|
||||
// Base renderer has nothing to dispatch to, so execute directly
|
||||
workItem();
|
||||
return Task.CompletedTask;
|
||||
// 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.Invoke(workItem);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -278,8 +326,23 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
/// <param name="workItem">The work item to execute.</param>
|
||||
public virtual Task InvokeAsync(Func<Task> workItem)
|
||||
{
|
||||
// Base renderer has nothing to dispatch to, so execute directly
|
||||
return 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.
|
||||
return workItem();
|
||||
}
|
||||
else
|
||||
{
|
||||
return _dispatcher.InvokeAsync(workItem);
|
||||
}
|
||||
}
|
||||
|
||||
internal void InstantiateChildComponentOnFrame(ref RenderTreeFrame frame, int parentComponentId)
|
||||
|
|
@ -323,10 +386,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
{
|
||||
return;
|
||||
}
|
||||
lock (_asyncWorkLock)
|
||||
{
|
||||
_pendingTasks.Add(task);
|
||||
}
|
||||
_pendingTasks.Add(task);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -351,6 +411,8 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
/// <param name="renderFragment">A <see cref="RenderFragment"/> that will supply the updated UI contents.</param>
|
||||
protected internal virtual void AddToRenderQueue(int componentId, RenderFragment renderFragment)
|
||||
{
|
||||
EnsureSynchronizationContext();
|
||||
|
||||
var componentState = GetOptionalComponentState(componentId);
|
||||
if (componentState == null)
|
||||
{
|
||||
|
|
@ -359,11 +421,8 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
return;
|
||||
}
|
||||
|
||||
lock (_asyncWorkLock)
|
||||
{
|
||||
_batchBuilder.ComponentRenderQueue.Enqueue(
|
||||
new RenderQueueEntry(componentState, renderFragment));
|
||||
}
|
||||
_batchBuilder.ComponentRenderQueue.Enqueue(
|
||||
new RenderQueueEntry(componentState, renderFragment));
|
||||
|
||||
if (!_isBatchInProgress)
|
||||
{
|
||||
|
|
@ -371,6 +430,22 @@ 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.
|
||||
// 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)
|
||||
{
|
||||
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.");
|
||||
}
|
||||
}
|
||||
|
||||
private ComponentState GetRequiredComponentState(int componentId)
|
||||
=> _componentStateById.TryGetValue(componentId, out var componentState)
|
||||
? componentState
|
||||
|
|
@ -389,8 +464,9 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
try
|
||||
{
|
||||
// Process render queue until empty
|
||||
while (TryDequeueRenderQueueEntry(out var nextToRender))
|
||||
while (_batchBuilder.ComponentRenderQueue.Count > 0)
|
||||
{
|
||||
var nextToRender = _batchBuilder.ComponentRenderQueue.Dequeue();
|
||||
RenderInExistingBatch(nextToRender);
|
||||
}
|
||||
|
||||
|
|
@ -406,23 +482,6 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
}
|
||||
}
|
||||
|
||||
private bool TryDequeueRenderQueueEntry(out RenderQueueEntry entry)
|
||||
{
|
||||
lock (_asyncWorkLock)
|
||||
{
|
||||
if (_batchBuilder.ComponentRenderQueue.Count > 0)
|
||||
{
|
||||
entry = _batchBuilder.ComponentRenderQueue.Dequeue();
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
entry = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void InvokeRenderCompletedCalls(ArrayRange<RenderTreeDiff> updatedComponents)
|
||||
{
|
||||
var array = updatedComponents.Array;
|
||||
|
|
|
|||
|
|
@ -6,10 +6,10 @@ using System.Diagnostics;
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Server.Circuits
|
||||
namespace Microsoft.AspNetCore.Components.Rendering
|
||||
{
|
||||
[DebuggerDisplay("{_state,nq}")]
|
||||
internal class CircuitSynchronizationContext : SynchronizationContext
|
||||
internal class RendererSynchronizationContext : SynchronizationContext, IDispatcher
|
||||
{
|
||||
private static readonly ContextCallback ExecutionContextThunk = (object state) =>
|
||||
{
|
||||
|
|
@ -27,12 +27,12 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
|
||||
public event UnhandledExceptionEventHandler UnhandledException;
|
||||
|
||||
public CircuitSynchronizationContext()
|
||||
public RendererSynchronizationContext()
|
||||
: this(new State())
|
||||
{
|
||||
}
|
||||
|
||||
private CircuitSynchronizationContext(State state)
|
||||
private RendererSynchronizationContext(State state)
|
||||
{
|
||||
_state = state;
|
||||
}
|
||||
|
|
@ -158,7 +158,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
// shallow copy
|
||||
public override SynchronizationContext CreateCopy()
|
||||
{
|
||||
return new CircuitSynchronizationContext(_state);
|
||||
return new RendererSynchronizationContext(_state);
|
||||
}
|
||||
|
||||
private Task Enqueue(Task antecedant, SendOrPostCallback d, object state)
|
||||
|
|
@ -259,7 +259,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
|
||||
private class WorkItem
|
||||
{
|
||||
public CircuitSynchronizationContext SynchronizationContext;
|
||||
public RendererSynchronizationContext SynchronizationContext;
|
||||
public ExecutionContext ExecutionContext;
|
||||
public SendOrPostCallback Callback;
|
||||
public object State;
|
||||
|
|
@ -373,7 +373,8 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
static CascadingValue<T> CreateCascadingValueComponent<T>(T value, string name = null)
|
||||
{
|
||||
var supplier = new CascadingValue<T>();
|
||||
supplier.Configure(new RenderHandle(new TestRenderer(), 0));
|
||||
var renderer = new TestRenderer();
|
||||
supplier.Configure(new RenderHandle(renderer, 0));
|
||||
|
||||
var supplierParams = new Dictionary<string, object>
|
||||
{
|
||||
|
|
@ -385,7 +386,7 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
supplierParams.Add("Name", name);
|
||||
}
|
||||
|
||||
supplier.SetParameters(supplierParams);
|
||||
renderer.Invoke(() => supplier.SetParametersAsync(ParameterCollection.FromDictionary(supplierParams)));
|
||||
return supplier;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
// 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.Components;
|
||||
using Microsoft.AspNetCore.Components.RenderTree;
|
||||
using Microsoft.AspNetCore.Components.Test.Helpers;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components.RenderTree;
|
||||
using Microsoft.AspNetCore.Components.Test.Helpers;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Test
|
||||
|
|
|
|||
|
|
@ -28,10 +28,10 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
public void DisplaysComponentInsideLayout()
|
||||
{
|
||||
// Arrange/Act
|
||||
_layoutDisplayComponent.SetParameters(new Dictionary<string, object>
|
||||
_renderer.Invoke(() => _layoutDisplayComponent.SetParametersAsync(ParameterCollection.FromDictionary(new Dictionary<string, object>
|
||||
{
|
||||
{ LayoutDisplay.NameOfPage, typeof(ComponentWithLayout) }
|
||||
});
|
||||
})));
|
||||
|
||||
// Assert
|
||||
var batch = _renderer.Batches.Single();
|
||||
|
|
@ -85,10 +85,10 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
public void DisplaysComponentInsideNestedLayout()
|
||||
{
|
||||
// Arrange/Act
|
||||
_layoutDisplayComponent.SetParameters(new Dictionary<string, object>
|
||||
_renderer.Invoke(() => _layoutDisplayComponent.SetParametersAsync(ParameterCollection.FromDictionary(new Dictionary<string, object>
|
||||
{
|
||||
{ LayoutDisplay.NameOfPage, typeof(ComponentWithNestedLayout) }
|
||||
});
|
||||
})));
|
||||
|
||||
// Assert
|
||||
var batch = _renderer.Batches.Single();
|
||||
|
|
@ -112,16 +112,16 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
public void CanChangeDisplayedPageWithSameLayout()
|
||||
{
|
||||
// Arrange
|
||||
_layoutDisplayComponent.SetParameters(new Dictionary<string, object>
|
||||
_renderer.Invoke(() => _layoutDisplayComponent.SetParametersAsync(ParameterCollection.FromDictionary(new Dictionary<string, object>
|
||||
{
|
||||
{ LayoutDisplay.NameOfPage, typeof(ComponentWithLayout) }
|
||||
});
|
||||
})));
|
||||
|
||||
// Act
|
||||
_layoutDisplayComponent.SetParameters(new Dictionary<string, object>
|
||||
_renderer.Invoke(() => _layoutDisplayComponent.SetParametersAsync(ParameterCollection.FromDictionary(new Dictionary<string, object>
|
||||
{
|
||||
{ LayoutDisplay.NameOfPage, typeof(DifferentComponentWithLayout) }
|
||||
});
|
||||
})));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, _renderer.Batches.Count);
|
||||
|
|
@ -163,16 +163,16 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
public void CanChangeDisplayedPageWithDifferentLayout()
|
||||
{
|
||||
// Arrange
|
||||
_layoutDisplayComponent.SetParameters(new Dictionary<string, object>
|
||||
_renderer.Invoke(() => _layoutDisplayComponent.SetParametersAsync(ParameterCollection.FromDictionary(new Dictionary<string, object>
|
||||
{
|
||||
{ LayoutDisplay.NameOfPage, typeof(ComponentWithLayout) }
|
||||
});
|
||||
})));
|
||||
|
||||
// Act
|
||||
_layoutDisplayComponent.SetParameters(new Dictionary<string, object>
|
||||
_renderer.Invoke(() => _layoutDisplayComponent.SetParametersAsync(ParameterCollection.FromDictionary(new Dictionary<string, object>
|
||||
{
|
||||
{ LayoutDisplay.NameOfPage, typeof(ComponentWithNestedLayout) }
|
||||
});
|
||||
})));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, _renderer.Batches.Count);
|
||||
|
|
|
|||
|
|
@ -1057,7 +1057,7 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
|
||||
private class TestRenderer : Renderer
|
||||
{
|
||||
public TestRenderer() : base(new TestServiceProvider())
|
||||
public TestRenderer() : base(new TestServiceProvider(), new RendererSynchronizationContext())
|
||||
{
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1527,7 +1527,7 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
|
||||
private class FakeRenderer : Renderer
|
||||
{
|
||||
public FakeRenderer() : base(new TestServiceProvider())
|
||||
public FakeRenderer() : base(new TestServiceProvider(), new RendererSynchronizationContext())
|
||||
{
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using System;
|
|||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components.Rendering;
|
||||
using Microsoft.AspNetCore.Components.RenderTree;
|
||||
|
|
@ -15,6 +16,10 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
{
|
||||
public class RendererTest
|
||||
{
|
||||
private const string EventActionsName = nameof(NestedAsyncComponent.EventActions);
|
||||
private const string WhatToRenderName = nameof(NestedAsyncComponent.WhatToRender);
|
||||
private const string LogName = nameof(NestedAsyncComponent.Log);
|
||||
|
||||
[Fact]
|
||||
public void CanRenderTopLevelComponents()
|
||||
{
|
||||
|
|
@ -171,7 +176,7 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
|
||||
// Act
|
||||
var componentId = renderer.AssignRootComponentId(component);
|
||||
await renderer.RenderRootComponentAsync(componentId);
|
||||
await renderer.InvokeAsync(() => renderer.RenderRootComponentAsync(componentId));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(5, renderer.Batches.Count);
|
||||
|
|
@ -221,9 +226,9 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
// Act/Assert
|
||||
var componentId = renderer.AssignRootComponentId(component);
|
||||
var log = new ConcurrentQueue<(int id, NestedAsyncComponent.EventType @event)>();
|
||||
await renderer.RenderRootComponentAsync(componentId, ParameterCollection.FromDictionary(new Dictionary<string, object>
|
||||
await renderer.InvokeAsync(() => renderer.RenderRootComponentAsync(componentId, ParameterCollection.FromDictionary(new Dictionary<string, object>
|
||||
{
|
||||
[nameof(NestedAsyncComponent.EventActions)] = new Dictionary<int, IList<NestedAsyncComponent.ExecutionAction>>
|
||||
[EventActionsName] = new Dictionary<int, IList<NestedAsyncComponent.ExecutionAction>>
|
||||
{
|
||||
[0] = new List<NestedAsyncComponent.ExecutionAction>
|
||||
{
|
||||
|
|
@ -240,13 +245,13 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
NestedAsyncComponent.ExecutionAction.On(1, NestedAsyncComponent.EventType.OnParametersSetAsyncAsync, async: true),
|
||||
}
|
||||
},
|
||||
[nameof(NestedAsyncComponent.WhatToRender)] = new Dictionary<int, Func<NestedAsyncComponent, RenderFragment>>
|
||||
[WhatToRenderName] = new Dictionary<int, Func<NestedAsyncComponent, RenderFragment>>
|
||||
{
|
||||
[0] = CreateRenderFactory(new[] { 1 }),
|
||||
[1] = CreateRenderFactory(Array.Empty<int>())
|
||||
},
|
||||
[nameof(NestedAsyncComponent.Log)] = log
|
||||
}));
|
||||
[LogName] = log
|
||||
})));
|
||||
|
||||
var logForParent = log.Where(l => l.id == 0).ToArray();
|
||||
var logForChild = log.Where(l => l.id == 1).ToArray();
|
||||
|
|
@ -265,9 +270,9 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
// Act/Assert
|
||||
var componentId = renderer.AssignRootComponentId(component);
|
||||
var log = new ConcurrentQueue<(int id, NestedAsyncComponent.EventType @event)>();
|
||||
await renderer.RenderRootComponentAsync(componentId, ParameterCollection.FromDictionary(new Dictionary<string, object>
|
||||
await renderer.InvokeAsync(() => renderer.RenderRootComponentAsync(componentId, ParameterCollection.FromDictionary(new Dictionary<string, object>
|
||||
{
|
||||
[nameof(NestedAsyncComponent.EventActions)] = new Dictionary<int, IList<NestedAsyncComponent.ExecutionAction>>
|
||||
[EventActionsName] = new Dictionary<int, IList<NestedAsyncComponent.ExecutionAction>>
|
||||
{
|
||||
[0] = new List<NestedAsyncComponent.ExecutionAction>
|
||||
{
|
||||
|
|
@ -284,13 +289,13 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
NestedAsyncComponent.ExecutionAction.On(1, NestedAsyncComponent.EventType.OnParametersSetAsyncAsync),
|
||||
}
|
||||
},
|
||||
[nameof(NestedAsyncComponent.WhatToRender)] = new Dictionary<int, Func<NestedAsyncComponent, RenderFragment>>
|
||||
[WhatToRenderName] = new Dictionary<int, Func<NestedAsyncComponent, RenderFragment>>
|
||||
{
|
||||
[0] = CreateRenderFactory(new[] { 1 }),
|
||||
[1] = CreateRenderFactory(Array.Empty<int>())
|
||||
},
|
||||
[nameof(NestedAsyncComponent.Log)] = log
|
||||
}));
|
||||
[LogName] = log
|
||||
})));
|
||||
|
||||
var logForParent = log.Where(l => l.id == 0).ToArray();
|
||||
var logForChild = log.Where(l => l.id == 1).ToArray();
|
||||
|
|
@ -309,9 +314,9 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
// Act/Assert
|
||||
var componentId = renderer.AssignRootComponentId(component);
|
||||
var log = new ConcurrentQueue<(int id, NestedAsyncComponent.EventType @event)>();
|
||||
await renderer.RenderRootComponentAsync(componentId, ParameterCollection.FromDictionary(new Dictionary<string, object>
|
||||
await renderer.InvokeAsync(() => renderer.RenderRootComponentAsync(componentId, ParameterCollection.FromDictionary(new Dictionary<string, object>
|
||||
{
|
||||
[nameof(NestedAsyncComponent.EventActions)] = new Dictionary<int, IList<NestedAsyncComponent.ExecutionAction>>
|
||||
[EventActionsName] = new Dictionary<int, IList<NestedAsyncComponent.ExecutionAction>>
|
||||
{
|
||||
[0] = new List<NestedAsyncComponent.ExecutionAction>
|
||||
{
|
||||
|
|
@ -328,13 +333,13 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
NestedAsyncComponent.ExecutionAction.On(1, NestedAsyncComponent.EventType.OnParametersSetAsyncAsync),
|
||||
}
|
||||
},
|
||||
[nameof(NestedAsyncComponent.WhatToRender)] = new Dictionary<int, Func<NestedAsyncComponent, RenderFragment>>
|
||||
[WhatToRenderName] = new Dictionary<int, Func<NestedAsyncComponent, RenderFragment>>
|
||||
{
|
||||
[0] = CreateRenderFactory(new[] { 1 }),
|
||||
[1] = CreateRenderFactory(Array.Empty<int>())
|
||||
},
|
||||
[nameof(NestedAsyncComponent.Log)] = log
|
||||
}));
|
||||
[LogName] = log
|
||||
})));
|
||||
|
||||
var logForParent = log.Where(l => l.id == 0).ToArray();
|
||||
var logForChild = log.Where(l => l.id == 1).ToArray();
|
||||
|
|
@ -353,9 +358,9 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
// Act/Assert
|
||||
var componentId = renderer.AssignRootComponentId(component);
|
||||
var log = new ConcurrentQueue<(int id, NestedAsyncComponent.EventType @event)>();
|
||||
await renderer.RenderRootComponentAsync(componentId, ParameterCollection.FromDictionary(new Dictionary<string, object>
|
||||
await renderer.InvokeAsync(() => renderer.RenderRootComponentAsync(componentId, ParameterCollection.FromDictionary(new Dictionary<string, object>
|
||||
{
|
||||
[nameof(NestedAsyncComponent.EventActions)] = new Dictionary<int, IList<NestedAsyncComponent.ExecutionAction>>
|
||||
[EventActionsName] = new Dictionary<int, IList<NestedAsyncComponent.ExecutionAction>>
|
||||
{
|
||||
[0] = new List<NestedAsyncComponent.ExecutionAction>
|
||||
{
|
||||
|
|
@ -386,15 +391,15 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
NestedAsyncComponent.ExecutionAction.On(3, NestedAsyncComponent.EventType.OnParametersSetAsyncAsync, async:true),
|
||||
}
|
||||
},
|
||||
[nameof(NestedAsyncComponent.WhatToRender)] = new Dictionary<int, Func<NestedAsyncComponent, RenderFragment>>
|
||||
[WhatToRenderName] = new Dictionary<int, Func<NestedAsyncComponent, RenderFragment>>
|
||||
{
|
||||
[0] = CreateRenderFactory(new[] { 1, 2 }),
|
||||
[1] = CreateRenderFactory(new[] { 3 }),
|
||||
[2] = CreateRenderFactory(Array.Empty<int>()),
|
||||
[3] = CreateRenderFactory(Array.Empty<int>())
|
||||
},
|
||||
[nameof(NestedAsyncComponent.Log)] = log
|
||||
}));
|
||||
[LogName] = log
|
||||
})));
|
||||
|
||||
var logForParent = log.Where(l => l.id == 0).ToArray();
|
||||
var logForFirstChild = log.Where(l => l.id == 1).ToArray();
|
||||
|
|
@ -553,10 +558,7 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
var eventArgs = new UIEventArgs();
|
||||
|
||||
// Act/Assert
|
||||
var ex = Assert.Throws<InvalidOperationException>(() =>
|
||||
{
|
||||
renderer.DispatchEvent(componentId, eventHandlerId, eventArgs);
|
||||
});
|
||||
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);
|
||||
}
|
||||
|
|
@ -568,10 +570,7 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
var renderer = new TestRenderer();
|
||||
|
||||
// Act/Assert
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
{
|
||||
renderer.DispatchEvent(123, 0, new UIEventArgs());
|
||||
});
|
||||
Assert.Throws<ArgumentException>(() => renderer.DispatchEvent(123, 0, new UIEventArgs()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -766,8 +765,9 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
Assert.Equal(new[] { 1, 3 }, renderer.Batches[1].DisposedComponentIDs);
|
||||
|
||||
// Act/Assert: If a disposed component requests a render, it's a no-op
|
||||
((FakeComponent)childComponent3).RenderHandle.Render(builder
|
||||
=> throw new NotImplementedException("Should not be invoked"));
|
||||
var renderHandle = ((FakeComponent)childComponent3).RenderHandle;
|
||||
renderHandle.Invoke(() => renderHandle.Render(builder
|
||||
=> throw new NotImplementedException("Should not be invoked")));
|
||||
Assert.Equal(2, renderer.Batches.Count);
|
||||
}
|
||||
|
||||
|
|
@ -798,10 +798,7 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
component.TriggerRender();
|
||||
|
||||
// Act/Assert 2: Can no longer fire the original event, but can fire the new event
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
{
|
||||
renderer.DispatchEvent(componentId, origEventHandlerId, args: null);
|
||||
});
|
||||
Assert.Throws<ArgumentException>(() => renderer.DispatchEvent(componentId, origEventHandlerId, args: null));
|
||||
Assert.Equal(1, eventCount);
|
||||
Assert.Equal(0, newEventCount);
|
||||
renderer.DispatchEvent(componentId, origEventHandlerId + 1, args: null);
|
||||
|
|
@ -834,10 +831,7 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
component.TriggerRender();
|
||||
|
||||
// Act/Assert 2: Can no longer fire the original event
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
{
|
||||
renderer.DispatchEvent(componentId, origEventHandlerId, args: null);
|
||||
});
|
||||
Assert.Throws<ArgumentException>(() => renderer.DispatchEvent(componentId, origEventHandlerId, args: null));
|
||||
Assert.Equal(1, eventCount);
|
||||
}
|
||||
|
||||
|
|
@ -883,10 +877,7 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
component.TriggerRender();
|
||||
|
||||
// Act/Assert 2: Can no longer fire the original event
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
{
|
||||
renderer.DispatchEvent(eventHandlerId, eventHandlerId, args: null);
|
||||
});
|
||||
Assert.Throws<ArgumentException>(() => renderer.DispatchEvent(eventHandlerId, eventHandlerId, args: null));
|
||||
Assert.Equal(1, eventCount);
|
||||
}
|
||||
|
||||
|
|
@ -916,10 +907,7 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
component.TriggerRender();
|
||||
|
||||
// Act/Assert 2: Can no longer fire the original event
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
{
|
||||
renderer.DispatchEvent(componentId, origEventHandlerId, args: null);
|
||||
});
|
||||
Assert.Throws<ArgumentException>(() => renderer.DispatchEvent(componentId, origEventHandlerId, args: null));
|
||||
Assert.Equal(1, eventCount);
|
||||
}
|
||||
|
||||
|
|
@ -1009,10 +997,7 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
var component = new TestComponent(builder => { });
|
||||
|
||||
// Act/Assert
|
||||
var ex = Assert.Throws<InvalidOperationException>(() =>
|
||||
{
|
||||
component.TriggerRender();
|
||||
});
|
||||
var ex = Assert.Throws<InvalidOperationException>(() => component.TriggerRender());
|
||||
Assert.Equal("The render handle is not yet assigned.", ex.Message);
|
||||
}
|
||||
|
||||
|
|
@ -1317,7 +1302,7 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanTriggerEventHandlerDisposedInEarlierPendingBatch()
|
||||
public async Task CanTriggerEventHandlerDisposedInEarlierPendingBatchAsync()
|
||||
{
|
||||
// This represents the scenario where the same event handler is being triggered
|
||||
// rapidly, such as an input event while typing. It only applies to asynchronous
|
||||
|
|
@ -1458,7 +1443,7 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
|
||||
private class NoOpRenderer : Renderer
|
||||
{
|
||||
public NoOpRenderer() : base(new TestServiceProvider())
|
||||
public NoOpRenderer() : base(new TestServiceProvider(), new RendererSynchronizationContext())
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -1491,7 +1476,20 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
}
|
||||
|
||||
public void TriggerRender()
|
||||
=> _renderHandle.Render(_renderFragment);
|
||||
{
|
||||
var t = _renderHandle.Invoke(() => _renderHandle.Render(_renderFragment));
|
||||
// This should always be run synchronously
|
||||
Assert.True(t.IsCompleted);
|
||||
if (t.IsFaulted)
|
||||
{
|
||||
var exception = t.Exception.Flatten().InnerException;
|
||||
while (exception is AggregateException e)
|
||||
{
|
||||
exception = e.InnerException;
|
||||
}
|
||||
ExceptionDispatchInfo.Capture(exception).Throw();
|
||||
}
|
||||
}
|
||||
|
||||
public bool Disposed { get; private set; }
|
||||
|
||||
|
|
@ -1674,10 +1672,10 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
{
|
||||
foreach (var renderHandle in _renderHandles)
|
||||
{
|
||||
renderHandle.Render(builder =>
|
||||
renderHandle.Invoke(() => renderHandle.Render(builder =>
|
||||
{
|
||||
builder.AddContent(0, $"Hello from {nameof(MultiRendererComponent)}");
|
||||
});
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components.RenderTree;
|
||||
|
|
@ -19,16 +20,17 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
public void RenderComponent_CanRenderEmptyElement()
|
||||
{
|
||||
// Arrange
|
||||
var dispatcher = Renderer.CreateDefaultDispatcher();
|
||||
var expectedHtml = new[] { "<", "p", ">", "</", "p", ">" };
|
||||
var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb =>
|
||||
{
|
||||
rtb.OpenElement(0, "p");
|
||||
rtb.CloseElement();
|
||||
})).BuildServiceProvider();
|
||||
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
|
||||
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder, dispatcher);
|
||||
|
||||
// Act
|
||||
var result = htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty);
|
||||
var result = GetResult(dispatcher.Invoke(() => htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty)));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedHtml, result);
|
||||
|
|
@ -38,6 +40,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
public void RenderComponent_CanRenderSimpleComponent()
|
||||
{
|
||||
// Arrange
|
||||
var dispatcher = Renderer.CreateDefaultDispatcher();
|
||||
var expectedHtml = new[] { "<", "p", ">", "Hello world!", "</", "p", ">" };
|
||||
var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb =>
|
||||
{
|
||||
|
|
@ -45,10 +48,10 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
rtb.AddContent(1, "Hello world!");
|
||||
rtb.CloseElement();
|
||||
})).BuildServiceProvider();
|
||||
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
|
||||
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder, dispatcher);
|
||||
|
||||
// Act
|
||||
var result = htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty);
|
||||
var result = GetResult(dispatcher.Invoke(() => htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty)));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedHtml, result);
|
||||
|
|
@ -58,6 +61,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
public void RenderComponent_HtmlEncodesContent()
|
||||
{
|
||||
// Arrange
|
||||
var dispatcher = Renderer.CreateDefaultDispatcher();
|
||||
var expectedHtml = new[] { "<", "p", ">", "<Hello world!>", "</", "p", ">" };
|
||||
var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb =>
|
||||
{
|
||||
|
|
@ -65,10 +69,10 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
rtb.AddContent(1, "<Hello world!>");
|
||||
rtb.CloseElement();
|
||||
})).BuildServiceProvider();
|
||||
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
|
||||
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder, dispatcher);
|
||||
|
||||
// Act
|
||||
var result = htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty);
|
||||
var result = GetResult(dispatcher.Invoke(() => htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty)));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedHtml, result);
|
||||
|
|
@ -79,6 +83,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
public void RenderComponent_DoesNotEncodeMarkup()
|
||||
{
|
||||
// Arrange
|
||||
var dispatcher = Renderer.CreateDefaultDispatcher();
|
||||
var expectedHtml = new[] { "<", "p", ">", "<span>Hello world!</span>", "</", "p", ">" };
|
||||
var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb =>
|
||||
{
|
||||
|
|
@ -86,10 +91,10 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
rtb.AddMarkupContent(1, "<span>Hello world!</span>");
|
||||
rtb.CloseElement();
|
||||
})).BuildServiceProvider();
|
||||
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
|
||||
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder, dispatcher);
|
||||
|
||||
// Act
|
||||
var result = htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty);
|
||||
var result = GetResult(dispatcher.Invoke(() => htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty)));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedHtml, result);
|
||||
|
|
@ -100,6 +105,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
public void RenderComponent_CanRenderWithAttributes()
|
||||
{
|
||||
// Arrange
|
||||
var dispatcher = Renderer.CreateDefaultDispatcher();
|
||||
var expectedHtml = new[] { "<", "p", " ", "class", "=", "\"", "lead", "\"", ">", "Hello world!", "</", "p", ">" };
|
||||
var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb =>
|
||||
{
|
||||
|
|
@ -109,10 +115,10 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
rtb.CloseElement();
|
||||
})).BuildServiceProvider();
|
||||
|
||||
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
|
||||
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder, dispatcher);
|
||||
|
||||
// Act
|
||||
var result = htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty);
|
||||
var result = GetResult(dispatcher.Invoke(() => htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty)));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedHtml, result);
|
||||
|
|
@ -122,6 +128,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
public void RenderComponent_HtmlEncodesAttributeValues()
|
||||
{
|
||||
// Arrange
|
||||
var dispatcher = Renderer.CreateDefaultDispatcher();
|
||||
var expectedHtml = new[] { "<", "p", " ", "class", "=", "\"", "<lead", "\"", ">", "Hello world!", "</", "p", ">" };
|
||||
var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb =>
|
||||
{
|
||||
|
|
@ -131,10 +138,10 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
rtb.CloseElement();
|
||||
})).BuildServiceProvider();
|
||||
|
||||
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
|
||||
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder, dispatcher);
|
||||
|
||||
// Act
|
||||
var result = htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty);
|
||||
var result = GetResult(dispatcher.Invoke(() => htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty)));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedHtml, result);
|
||||
|
|
@ -144,6 +151,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
public void RenderComponent_CanRenderBooleanAttributes()
|
||||
{
|
||||
// Arrange
|
||||
var dispatcher = Renderer.CreateDefaultDispatcher();
|
||||
var expectedHtml = new[] { "<", "input", " ", "disabled", " />" };
|
||||
var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb =>
|
||||
{
|
||||
|
|
@ -152,10 +160,10 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
rtb.CloseElement();
|
||||
})).BuildServiceProvider();
|
||||
|
||||
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
|
||||
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder, dispatcher);
|
||||
|
||||
// Act
|
||||
var result = htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty);
|
||||
var result = GetResult(dispatcher.Invoke(() => htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty)));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedHtml, result);
|
||||
|
|
@ -165,6 +173,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
public void RenderComponent_DoesNotRenderBooleanAttributesWhenValueIsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var dispatcher = Renderer.CreateDefaultDispatcher();
|
||||
var expectedHtml = new[] { "<", "input", " />" };
|
||||
var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb =>
|
||||
{
|
||||
|
|
@ -173,10 +182,10 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
rtb.CloseElement();
|
||||
})).BuildServiceProvider();
|
||||
|
||||
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
|
||||
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder, dispatcher);
|
||||
|
||||
// Act
|
||||
var result = htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty);
|
||||
var result = GetResult(dispatcher.Invoke(() => htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty)));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedHtml, result);
|
||||
|
|
@ -186,6 +195,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
public void RenderComponent_CanRenderWithChildren()
|
||||
{
|
||||
// Arrange
|
||||
var dispatcher = Renderer.CreateDefaultDispatcher();
|
||||
var expectedHtml = new[] { "<", "p", ">", "<", "span", ">", "Hello world!", "</", "span", ">", "</", "p", ">" };
|
||||
var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb =>
|
||||
{
|
||||
|
|
@ -196,10 +206,10 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
rtb.CloseElement();
|
||||
})).BuildServiceProvider();
|
||||
|
||||
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
|
||||
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder, dispatcher);
|
||||
|
||||
// Act
|
||||
var result = htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty);
|
||||
var result = GetResult(dispatcher.Invoke(() => htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty)));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedHtml, result);
|
||||
|
|
@ -209,6 +219,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
public void RenderComponent_CanRenderWithMultipleChildren()
|
||||
{
|
||||
// Arrange
|
||||
var dispatcher = Renderer.CreateDefaultDispatcher();
|
||||
var expectedHtml = new[] { "<", "p", ">",
|
||||
"<", "span", ">", "Hello world!", "</", "span", ">",
|
||||
"<", "span", ">", "Bye Bye world!", "</", "span", ">",
|
||||
|
|
@ -226,10 +237,10 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
rtb.CloseElement();
|
||||
})).BuildServiceProvider();
|
||||
|
||||
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
|
||||
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder, dispatcher);
|
||||
|
||||
// Act
|
||||
var result = htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty);
|
||||
var result = GetResult(dispatcher.Invoke(() => htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty)));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedHtml, result);
|
||||
|
|
@ -239,6 +250,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
public void RenderComponent_CanRenderComponentWithChildrenComponents()
|
||||
{
|
||||
// Arrange
|
||||
var dispatcher = Renderer.CreateDefaultDispatcher();
|
||||
var expectedHtml = new[] {
|
||||
"<", "p", ">", "<", "span", ">", "Hello world!", "</", "span", ">", "</", "p", ">",
|
||||
"<", "span", ">", "Child content!", "</", "span", ">"
|
||||
|
|
@ -255,10 +267,10 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
rtb.CloseComponent();
|
||||
})).BuildServiceProvider();
|
||||
|
||||
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
|
||||
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder, dispatcher);
|
||||
|
||||
// Act
|
||||
var result = htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty);
|
||||
var result = GetResult(dispatcher.Invoke(() => htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty)));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedHtml, result);
|
||||
|
|
@ -268,6 +280,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
public void RenderComponent_ComponentReferenceNoops()
|
||||
{
|
||||
// Arrange
|
||||
var dispatcher = Renderer.CreateDefaultDispatcher();
|
||||
var expectedHtml = new[] {
|
||||
"<", "p", ">", "<", "span", ">", "Hello world!", "</", "span", ">", "</", "p", ">",
|
||||
"<", "span", ">", "Child content!", "</", "span", ">"
|
||||
|
|
@ -285,10 +298,10 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
rtb.CloseComponent();
|
||||
})).BuildServiceProvider();
|
||||
|
||||
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
|
||||
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder, dispatcher);
|
||||
|
||||
// Act
|
||||
var result = htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty);
|
||||
var result = GetResult(dispatcher.Invoke(() => htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty)));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedHtml, result);
|
||||
|
|
@ -298,6 +311,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
public void RenderComponent_CanPassParameters()
|
||||
{
|
||||
// Arrange
|
||||
var dispatcher = Renderer.CreateDefaultDispatcher();
|
||||
var expectedHtml = new[] {
|
||||
"<", "p", ">", "<", "input", " ", "value", "=", "\"", "5", "\"", " />", "</", "p", ">" };
|
||||
|
||||
|
|
@ -315,16 +329,16 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
.AddSingleton(new Func<ParameterCollection, RenderFragment>(Content))
|
||||
.BuildServiceProvider();
|
||||
|
||||
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
|
||||
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder, dispatcher);
|
||||
Action<UIChangeEventArgs> change = (UIChangeEventArgs changeArgs) => throw new InvalidOperationException();
|
||||
|
||||
// Act
|
||||
var result = htmlRenderer.RenderComponent<ComponentWithParameters>(
|
||||
var result = GetResult(dispatcher.Invoke(() => htmlRenderer.RenderComponent<ComponentWithParameters>(
|
||||
new ParameterCollection(new[] {
|
||||
RenderTreeFrame.Element(0,string.Empty),
|
||||
RenderTreeFrame.Attribute(1,"update",change),
|
||||
RenderTreeFrame.Attribute(2,"value",5)
|
||||
}, 0));
|
||||
}, 0))));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedHtml, result);
|
||||
|
|
@ -334,6 +348,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
public void RenderComponent_CanRenderComponentWithRenderFragmentContent()
|
||||
{
|
||||
// Arrange
|
||||
var dispatcher = Renderer.CreateDefaultDispatcher();
|
||||
var expectedHtml = new[] {
|
||||
"<", "p", ">", "<", "span", ">", "Hello world!", "</", "span", ">", "</", "p", ">" };
|
||||
var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb =>
|
||||
|
|
@ -347,10 +362,10 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
rtb.CloseElement();
|
||||
})).BuildServiceProvider();
|
||||
|
||||
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
|
||||
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder, dispatcher);
|
||||
|
||||
// Act
|
||||
var result = htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty);
|
||||
var result = GetResult(dispatcher.Invoke(() => htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty)));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedHtml, result);
|
||||
|
|
@ -360,6 +375,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
public void RenderComponent_ElementRefsNoops()
|
||||
{
|
||||
// Arrange
|
||||
var dispatcher = Renderer.CreateDefaultDispatcher();
|
||||
var expectedHtml = new[] {
|
||||
"<", "p", ">", "<", "span", ">", "Hello world!", "</", "span", ">", "</", "p", ">" };
|
||||
var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb =>
|
||||
|
|
@ -374,15 +390,29 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
rtb.CloseElement();
|
||||
})).BuildServiceProvider();
|
||||
|
||||
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
|
||||
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder, dispatcher);
|
||||
|
||||
// Act
|
||||
var result = htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty);
|
||||
var result = GetResult(dispatcher.Invoke(() => htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty)));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedHtml, result);
|
||||
}
|
||||
|
||||
private IEnumerable<string> GetResult(Task<IEnumerable<string>> task)
|
||||
{
|
||||
Assert.True(task.IsCompleted);
|
||||
if (task.IsCompletedSuccessfully)
|
||||
{
|
||||
return task.Result;
|
||||
}
|
||||
else
|
||||
{
|
||||
ExceptionDispatchInfo.Capture(task.Exception).Throw();
|
||||
throw new InvalidOperationException("We will never hit this line");
|
||||
}
|
||||
}
|
||||
|
||||
private class ComponentWithParameters : IComponent
|
||||
{
|
||||
public RenderHandle RenderHandle { get; private set; }
|
||||
|
|
@ -406,17 +436,18 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
public async Task CanRender_AsyncComponent()
|
||||
{
|
||||
// Arrange
|
||||
var dispatcher = Renderer.CreateDefaultDispatcher();
|
||||
var expectedHtml = new[] {
|
||||
"<", "p", ">", "20", "</", "p", ">" };
|
||||
var serviceProvider = new ServiceCollection().AddSingleton<AsyncComponent>().BuildServiceProvider();
|
||||
|
||||
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
|
||||
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder, dispatcher);
|
||||
|
||||
// Act
|
||||
var result = await htmlRenderer.RenderComponentAsync<AsyncComponent>(ParameterCollection.FromDictionary(new Dictionary<string, object>
|
||||
var result = await dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<AsyncComponent>(ParameterCollection.FromDictionary(new Dictionary<string, object>
|
||||
{
|
||||
["Value"] = 10
|
||||
}));
|
||||
})));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedHtml, result);
|
||||
|
|
@ -426,6 +457,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
public async Task CanRender_NestedAsyncComponents()
|
||||
{
|
||||
// Arrange
|
||||
var dispatcher = Renderer.CreateDefaultDispatcher();
|
||||
var expectedHtml = new[] {
|
||||
"<", "p", ">", "20", "</", "p", ">",
|
||||
"<", "p", ">", "80", "</", "p", ">"
|
||||
|
|
@ -433,14 +465,14 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
|
||||
var serviceProvider = new ServiceCollection().AddSingleton<AsyncComponent>().BuildServiceProvider();
|
||||
|
||||
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
|
||||
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder, dispatcher);
|
||||
|
||||
// Act
|
||||
var result = await htmlRenderer.RenderComponentAsync<NestedAsyncComponent>(ParameterCollection.FromDictionary(new Dictionary<string, object>
|
||||
var result = await dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<NestedAsyncComponent>(ParameterCollection.FromDictionary(new Dictionary<string, object>
|
||||
{
|
||||
["Nested"] = false,
|
||||
["Value"] = 10
|
||||
}));
|
||||
})));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedHtml, result);
|
||||
|
|
|
|||
|
|
@ -1,17 +1,15 @@
|
|||
// 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.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components.Server.Circuits;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Server
|
||||
namespace Microsoft.AspNetCore.Components.Rendering
|
||||
{
|
||||
public class CircuitSynchronizationContextTest
|
||||
public class RendererSynchronizationContextTest
|
||||
{
|
||||
// Nothing should exceed the timeout in a successful run of the the tests, this is just here to catch
|
||||
// failures.
|
||||
|
|
@ -21,7 +19,7 @@ namespace Microsoft.AspNetCore.Components.Server
|
|||
public void Post_CanRunSynchronously_WhenNotBusy()
|
||||
{
|
||||
// Arrange
|
||||
var context = new CircuitSynchronizationContext();
|
||||
var context = new RendererSynchronizationContext();
|
||||
var thread = Thread.CurrentThread;
|
||||
Thread capturedThread = null;
|
||||
|
||||
|
|
@ -39,7 +37,7 @@ namespace Microsoft.AspNetCore.Components.Server
|
|||
public void Post_CanRunSynchronously_WhenNotBusy_Exception()
|
||||
{
|
||||
// Arrange
|
||||
var context = new CircuitSynchronizationContext();
|
||||
var context = new RendererSynchronizationContext();
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<InvalidTimeZoneException>(() => context.Post((_) =>
|
||||
|
|
@ -52,7 +50,7 @@ namespace Microsoft.AspNetCore.Components.Server
|
|||
public async Task Post_CanRunAsynchronously_WhenBusy()
|
||||
{
|
||||
// Arrange
|
||||
var context = new CircuitSynchronizationContext();
|
||||
var context = new RendererSynchronizationContext();
|
||||
var thread = Thread.CurrentThread;
|
||||
Thread capturedThread = null;
|
||||
|
||||
|
|
@ -92,7 +90,7 @@ namespace Microsoft.AspNetCore.Components.Server
|
|||
public async Task Post_CanRunAsynchronously_CaptureExecutionContext()
|
||||
{
|
||||
// Arrange
|
||||
var context = new CircuitSynchronizationContext();
|
||||
var context = new RendererSynchronizationContext();
|
||||
|
||||
// CultureInfo uses the execution context.
|
||||
CultureInfo.CurrentCulture = new CultureInfo("en-GB");
|
||||
|
|
@ -147,7 +145,7 @@ namespace Microsoft.AspNetCore.Components.Server
|
|||
public async Task Post_CanRunAsynchronously_WhenBusy_Exception()
|
||||
{
|
||||
// Arrange
|
||||
var context = new CircuitSynchronizationContext();
|
||||
var context = new RendererSynchronizationContext();
|
||||
|
||||
Exception exception = null;
|
||||
context.UnhandledException += (sender, e) =>
|
||||
|
|
@ -189,7 +187,7 @@ namespace Microsoft.AspNetCore.Components.Server
|
|||
public async Task Post_BackgroundWorkItem_CanProcessMoreItemsInline()
|
||||
{
|
||||
// Arrange
|
||||
var context = new CircuitSynchronizationContext();
|
||||
var context = new RendererSynchronizationContext();
|
||||
Thread capturedThread = null;
|
||||
|
||||
var e1 = new ManualResetEventSlim();
|
||||
|
|
@ -251,7 +249,7 @@ namespace Microsoft.AspNetCore.Components.Server
|
|||
public void Post_CapturesContext()
|
||||
{
|
||||
// Arrange
|
||||
var context = new CircuitSynchronizationContext();
|
||||
var context = new RendererSynchronizationContext();
|
||||
|
||||
var e1 = new ManualResetEventSlim();
|
||||
|
||||
|
|
@ -281,7 +279,7 @@ namespace Microsoft.AspNetCore.Components.Server
|
|||
public void Send_CanRunSynchronously()
|
||||
{
|
||||
// Arrange
|
||||
var context = new CircuitSynchronizationContext();
|
||||
var context = new RendererSynchronizationContext();
|
||||
var thread = Thread.CurrentThread;
|
||||
Thread capturedThread = null;
|
||||
|
||||
|
|
@ -299,7 +297,7 @@ namespace Microsoft.AspNetCore.Components.Server
|
|||
public void Send_CanRunSynchronously_Exception()
|
||||
{
|
||||
// Arrange
|
||||
var context = new CircuitSynchronizationContext();
|
||||
var context = new RendererSynchronizationContext();
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<InvalidTimeZoneException>(() => context.Send((_) =>
|
||||
|
|
@ -312,7 +310,7 @@ namespace Microsoft.AspNetCore.Components.Server
|
|||
public async Task Send_BlocksWhenOtherWorkRunning()
|
||||
{
|
||||
// Arrange
|
||||
var context = new CircuitSynchronizationContext();
|
||||
var context = new RendererSynchronizationContext();
|
||||
|
||||
var e1 = new ManualResetEventSlim();
|
||||
var e2 = new ManualResetEventSlim();
|
||||
|
|
@ -359,7 +357,7 @@ namespace Microsoft.AspNetCore.Components.Server
|
|||
public void Send_CapturesContext()
|
||||
{
|
||||
// Arrange
|
||||
var context = new CircuitSynchronizationContext();
|
||||
var context = new RendererSynchronizationContext();
|
||||
|
||||
var e1 = new ManualResetEventSlim();
|
||||
|
||||
|
|
@ -390,7 +388,7 @@ namespace Microsoft.AspNetCore.Components.Server
|
|||
public async Task Invoke_Void_CanRunSynchronously_WhenNotBusy()
|
||||
{
|
||||
// Arrange
|
||||
var context = new CircuitSynchronizationContext();
|
||||
var context = new RendererSynchronizationContext();
|
||||
var thread = Thread.CurrentThread;
|
||||
Thread capturedThread = null;
|
||||
|
||||
|
|
@ -409,7 +407,7 @@ namespace Microsoft.AspNetCore.Components.Server
|
|||
public async Task Invoke_Void_CanRunAsynchronously_WhenBusy()
|
||||
{
|
||||
// Arrange
|
||||
var context = new CircuitSynchronizationContext();
|
||||
var context = new RendererSynchronizationContext();
|
||||
var thread = Thread.CurrentThread;
|
||||
Thread capturedThread = null;
|
||||
|
||||
|
|
@ -449,7 +447,7 @@ namespace Microsoft.AspNetCore.Components.Server
|
|||
public async Task Invoke_Void_CanRethrowExceptions()
|
||||
{
|
||||
// Arrange
|
||||
var context = new CircuitSynchronizationContext();
|
||||
var context = new RendererSynchronizationContext();
|
||||
|
||||
// Act
|
||||
var task = context.Invoke(() =>
|
||||
|
|
@ -465,7 +463,7 @@ namespace Microsoft.AspNetCore.Components.Server
|
|||
public async Task Invoke_T_CanRunSynchronously_WhenNotBusy()
|
||||
{
|
||||
// Arrange
|
||||
var context = new CircuitSynchronizationContext();
|
||||
var context = new RendererSynchronizationContext();
|
||||
var thread = Thread.CurrentThread;
|
||||
|
||||
// Act
|
||||
|
|
@ -482,7 +480,7 @@ namespace Microsoft.AspNetCore.Components.Server
|
|||
public async Task Invoke_T_CanRunAsynchronously_WhenBusy()
|
||||
{
|
||||
// Arrange
|
||||
var context = new CircuitSynchronizationContext();
|
||||
var context = new RendererSynchronizationContext();
|
||||
var thread = Thread.CurrentThread;
|
||||
|
||||
var e1 = new ManualResetEventSlim();
|
||||
|
|
@ -520,7 +518,7 @@ namespace Microsoft.AspNetCore.Components.Server
|
|||
public async Task Invoke_T_CanRethrowExceptions()
|
||||
{
|
||||
// Arrange
|
||||
var context = new CircuitSynchronizationContext();
|
||||
var context = new RendererSynchronizationContext();
|
||||
|
||||
// Act
|
||||
var task = context.Invoke<string>(() =>
|
||||
|
|
@ -536,7 +534,7 @@ namespace Microsoft.AspNetCore.Components.Server
|
|||
public async Task InvokeAsync_Void_CanRunSynchronously_WhenNotBusy()
|
||||
{
|
||||
// Arrange
|
||||
var context = new CircuitSynchronizationContext();
|
||||
var context = new RendererSynchronizationContext();
|
||||
var thread = Thread.CurrentThread;
|
||||
Thread capturedThread = null;
|
||||
|
||||
|
|
@ -556,7 +554,7 @@ namespace Microsoft.AspNetCore.Components.Server
|
|||
public async Task InvokeAsync_Void_CanRunAsynchronously_WhenBusy()
|
||||
{
|
||||
// Arrange
|
||||
var context = new CircuitSynchronizationContext();
|
||||
var context = new RendererSynchronizationContext();
|
||||
var thread = Thread.CurrentThread;
|
||||
Thread capturedThread = null;
|
||||
|
||||
|
|
@ -597,7 +595,7 @@ namespace Microsoft.AspNetCore.Components.Server
|
|||
public async Task InvokeAsync_Void_CanRethrowExceptions()
|
||||
{
|
||||
// Arrange
|
||||
var context = new CircuitSynchronizationContext();
|
||||
var context = new RendererSynchronizationContext();
|
||||
|
||||
// Act
|
||||
var task = context.InvokeAsync(() =>
|
||||
|
|
@ -613,7 +611,7 @@ namespace Microsoft.AspNetCore.Components.Server
|
|||
public async Task InvokeAsync_T_CanRunSynchronously_WhenNotBusy()
|
||||
{
|
||||
// Arrange
|
||||
var context = new CircuitSynchronizationContext();
|
||||
var context = new RendererSynchronizationContext();
|
||||
var thread = Thread.CurrentThread;
|
||||
|
||||
// Act
|
||||
|
|
@ -630,7 +628,7 @@ namespace Microsoft.AspNetCore.Components.Server
|
|||
public async Task InvokeAsync_T_CanRunAsynchronously_WhenBusy()
|
||||
{
|
||||
// Arrange
|
||||
var context = new CircuitSynchronizationContext();
|
||||
var context = new RendererSynchronizationContext();
|
||||
var thread = Thread.CurrentThread;
|
||||
|
||||
var e1 = new ManualResetEventSlim();
|
||||
|
|
@ -668,7 +666,7 @@ namespace Microsoft.AspNetCore.Components.Server
|
|||
public async Task InvokeAsync_T_CanRethrowExceptions()
|
||||
{
|
||||
// Arrange
|
||||
var context = new CircuitSynchronizationContext();
|
||||
var context = new RendererSynchronizationContext();
|
||||
|
||||
// Act
|
||||
var task = context.InvokeAsync<string>(() =>
|
||||
|
|
@ -55,7 +55,6 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
RemoteRenderer renderer,
|
||||
Action<IComponentsApplicationBuilder> configure,
|
||||
IJSRuntime jsRuntime,
|
||||
CircuitSynchronizationContext synchronizationContext,
|
||||
CircuitHandler[] circuitHandlers)
|
||||
{
|
||||
_scope = scope ?? throw new ArgumentNullException(nameof(scope));
|
||||
|
|
@ -64,7 +63,6 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
Renderer = renderer ?? throw new ArgumentNullException(nameof(renderer));
|
||||
_configure = configure ?? throw new ArgumentNullException(nameof(configure));
|
||||
JSRuntime = jsRuntime ?? throw new ArgumentNullException(nameof(jsRuntime));
|
||||
SynchronizationContext = synchronizationContext ?? throw new ArgumentNullException(nameof(synchronizationContext));
|
||||
|
||||
Services = scope.ServiceProvider;
|
||||
|
||||
|
|
@ -72,7 +70,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
_circuitHandlers = circuitHandlers;
|
||||
|
||||
Renderer.UnhandledException += Renderer_UnhandledException;
|
||||
SynchronizationContext.UnhandledException += SynchronizationContext_UnhandledException;
|
||||
Renderer.UnhandledSynchronizationException += SynchronizationContext_UnhandledException;
|
||||
}
|
||||
|
||||
public string CircuitId { get; } = Guid.NewGuid().ToString();
|
||||
|
|
@ -89,11 +87,9 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
|
||||
public IServiceProvider Services { get; }
|
||||
|
||||
public CircuitSynchronizationContext SynchronizationContext { get; }
|
||||
|
||||
public async Task InitializeAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await SynchronizationContext.InvokeAsync(async () =>
|
||||
await Renderer.InvokeAsync(async () =>
|
||||
{
|
||||
SetCurrentCircuitHost(this);
|
||||
|
||||
|
|
@ -127,10 +123,9 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
|
||||
try
|
||||
{
|
||||
await SynchronizationContext.Invoke(() =>
|
||||
await Renderer.Invoke(() =>
|
||||
{
|
||||
SetCurrentCircuitHost(this);
|
||||
|
||||
DotNetDispatcher.BeginInvoke(callId, assemblyName, methodIdentifier, dotNetObjectId, argsJson);
|
||||
});
|
||||
}
|
||||
|
|
@ -142,7 +137,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await SynchronizationContext.InvokeAsync(async () =>
|
||||
await Renderer.InvokeAsync(async () =>
|
||||
{
|
||||
for (var i = 0; i < _circuitHandlers.Length; i++)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using System;
|
|||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Components.Browser;
|
||||
using Microsoft.AspNetCore.Components.Browser.Rendering;
|
||||
using Microsoft.AspNetCore.Components.Rendering;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
|
@ -41,8 +42,13 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
var scope = _scopeFactory.CreateScope();
|
||||
var jsRuntime = new RemoteJSRuntime(client);
|
||||
var rendererRegistry = new RendererRegistry();
|
||||
var synchronizationContext = new CircuitSynchronizationContext();
|
||||
var renderer = new RemoteRenderer(scope.ServiceProvider, rendererRegistry, jsRuntime, client, synchronizationContext);
|
||||
var dispatcher = Renderer.CreateDefaultDispatcher();
|
||||
var renderer = new RemoteRenderer(
|
||||
scope.ServiceProvider,
|
||||
rendererRegistry,
|
||||
jsRuntime,
|
||||
client,
|
||||
dispatcher);
|
||||
|
||||
var circuitHandlers = scope.ServiceProvider.GetServices<CircuitHandler>()
|
||||
.OrderBy(h => h.Order)
|
||||
|
|
@ -55,7 +61,6 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
renderer,
|
||||
config,
|
||||
jsRuntime,
|
||||
synchronizationContext,
|
||||
circuitHandlers);
|
||||
|
||||
// Initialize per-circuit data that services need
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
|
|||
private readonly IClientProxy _client;
|
||||
private readonly IJSRuntime _jsRuntime;
|
||||
private readonly RendererRegistry _rendererRegistry;
|
||||
private readonly SynchronizationContext _syncContext;
|
||||
private readonly ConcurrentDictionary<long, AutoCancelTaskCompletionSource<object>> _pendingRenders
|
||||
= new ConcurrentDictionary<long, AutoCancelTaskCompletionSource<object>>();
|
||||
private long _nextRenderId = 1;
|
||||
|
|
@ -46,13 +45,12 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
|
|||
RendererRegistry rendererRegistry,
|
||||
IJSRuntime jsRuntime,
|
||||
IClientProxy client,
|
||||
SynchronizationContext syncContext)
|
||||
: base(serviceProvider)
|
||||
IDispatcher dispatcher)
|
||||
: base(serviceProvider, dispatcher)
|
||||
{
|
||||
_rendererRegistry = rendererRegistry;
|
||||
_jsRuntime = jsRuntime;
|
||||
_client = client;
|
||||
_syncContext = syncContext ?? throw new ArgumentNullException(nameof(syncContext));
|
||||
|
||||
_id = _rendererRegistry.Add(this);
|
||||
}
|
||||
|
|
@ -64,7 +62,7 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
|
|||
/// <typeparam name="TComponent">The type of the component.</typeparam>
|
||||
/// <param name="domElementSelector">A CSS selector that uniquely identifies a DOM element.</param>
|
||||
public void AddComponent<TComponent>(string domElementSelector)
|
||||
where TComponent: IComponent
|
||||
where TComponent : IComponent
|
||||
{
|
||||
AddComponent(typeof(TComponent), domElementSelector);
|
||||
}
|
||||
|
|
@ -90,36 +88,6 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
|
|||
RenderRootComponent(componentId);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task Invoke(Action workItem)
|
||||
{
|
||||
if (SynchronizationContext.Current == _syncContext)
|
||||
{
|
||||
// No need to dispatch. Avoid deadlock by invoking directly.
|
||||
return base.Invoke(workItem);
|
||||
}
|
||||
else
|
||||
{
|
||||
var syncContext = (CircuitSynchronizationContext)_syncContext;
|
||||
return syncContext.Invoke(workItem);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task InvokeAsync(Func<Task> workItem)
|
||||
{
|
||||
if (SynchronizationContext.Current == _syncContext)
|
||||
{
|
||||
// No need to dispatch. Avoid deadlock by invoking directly.
|
||||
return base.InvokeAsync(workItem);
|
||||
}
|
||||
else
|
||||
{
|
||||
var syncContext = (CircuitSynchronizationContext)_syncContext;
|
||||
return syncContext.InvokeAsync(workItem);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
|
|
@ -127,23 +95,6 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
|
|||
_rendererRegistry.TryRemove(_id);
|
||||
}
|
||||
|
||||
protected override void AddToRenderQueue(int componentId, RenderFragment renderFragment)
|
||||
{
|
||||
// Render operations are not thread-safe, so they need to be serialized.
|
||||
// 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 (SynchronizationContext.Current != _syncContext)
|
||||
{
|
||||
throw new RemoteRendererException(
|
||||
"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.");
|
||||
}
|
||||
|
||||
base.AddToRenderQueue(componentId, renderFragment);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Task UpdateDisplayAsync(in RenderBatch batch)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -129,7 +129,6 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
var clientProxy = Mock.Of<IClientProxy>();
|
||||
var renderRegistry = new RendererRegistry();
|
||||
var jsRuntime = Mock.Of<IJSRuntime>();
|
||||
var syncContext = new CircuitSynchronizationContext();
|
||||
|
||||
remoteRenderer = remoteRenderer ?? GetRemoteRenderer();
|
||||
handlers = handlers ?? Array.Empty<CircuitHandler>();
|
||||
|
|
@ -141,8 +140,6 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
remoteRenderer,
|
||||
configure: _ => { },
|
||||
jsRuntime: jsRuntime,
|
||||
synchronizationContext:
|
||||
syncContext,
|
||||
handlers);
|
||||
}
|
||||
|
||||
|
|
@ -152,14 +149,13 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
Mock.Of<IServiceProvider>(),
|
||||
new RendererRegistry(),
|
||||
Mock.Of<IJSRuntime>(),
|
||||
Mock.Of<IClientProxy>(),
|
||||
new CircuitSynchronizationContext());
|
||||
Mock.Of<IClientProxy>());
|
||||
}
|
||||
|
||||
private class TestRemoteRenderer : RemoteRenderer
|
||||
{
|
||||
public TestRemoteRenderer(IServiceProvider serviceProvider, RendererRegistry rendererRegistry, IJSRuntime jsRuntime, IClientProxy client, SynchronizationContext syncContext)
|
||||
: base(serviceProvider, rendererRegistry, jsRuntime, client, syncContext)
|
||||
public TestRemoteRenderer(IServiceProvider serviceProvider, RendererRegistry rendererRegistry, IJSRuntime jsRuntime, IClientProxy client)
|
||||
: base(serviceProvider, rendererRegistry, jsRuntime, client, CreateDefaultDispatcher())
|
||||
{
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -373,7 +373,7 @@ namespace Microsoft.AspNetCore.Components.Server
|
|||
class FakeRenderer : Renderer
|
||||
{
|
||||
public FakeRenderer()
|
||||
: base(new ServiceCollection().BuildServiceProvider())
|
||||
: base(new ServiceCollection().BuildServiceProvider(), new RendererSynchronizationContext())
|
||||
{
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
// 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.Runtime.ExceptionServices;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.RenderTree;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Test.Helpers
|
||||
{
|
||||
|
|
@ -23,8 +26,22 @@ namespace Microsoft.AspNetCore.Components.Test.Helpers
|
|||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// We do it this way so that we don't have to be doing renderer.Invoke on each and every test.
|
||||
public void TriggerRender()
|
||||
=> _renderHandle.Render(BuildRenderTree);
|
||||
{
|
||||
var t = _renderHandle.Invoke(() => _renderHandle.Render(BuildRenderTree));
|
||||
// This should always be run synchronously
|
||||
Assert.True(t.IsCompleted);
|
||||
if (t.IsFaulted)
|
||||
{
|
||||
var exception = t.Exception.Flatten().InnerException;
|
||||
while (exception is AggregateException e)
|
||||
{
|
||||
exception = e.InnerException;
|
||||
}
|
||||
ExceptionDispatchInfo.Capture(exception).Throw();
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void BuildRenderTree(RenderTreeBuilder builder);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,19 +4,25 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Rendering;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Test.Helpers
|
||||
{
|
||||
public class TestRenderer : Renderer
|
||||
{
|
||||
public TestRenderer(): this(new TestServiceProvider())
|
||||
public TestRenderer() : this(new TestServiceProvider())
|
||||
{
|
||||
}
|
||||
|
||||
public TestRenderer(IServiceProvider serviceProvider) : base(serviceProvider)
|
||||
public TestRenderer(IDispatcher dispatcher) : base(new TestServiceProvider(), dispatcher)
|
||||
{
|
||||
}
|
||||
|
||||
public TestRenderer(IServiceProvider serviceProvider) : base(serviceProvider, new RendererSynchronizationContext())
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -29,16 +35,29 @@ namespace Microsoft.AspNetCore.Components.Test.Helpers
|
|||
=> base.AssignRootComponentId(component);
|
||||
|
||||
public new void RenderRootComponent(int componentId)
|
||||
=> base.RenderRootComponent(componentId);
|
||||
=> Invoke(() => base.RenderRootComponent(componentId));
|
||||
|
||||
public new Task RenderRootComponentAsync(int componentId)
|
||||
=> base.RenderRootComponentAsync(componentId);
|
||||
=> InvokeAsync(() => base.RenderRootComponentAsync(componentId));
|
||||
|
||||
public new Task RenderRootComponentAsync(int componentId, ParameterCollection parameters)
|
||||
=> base.RenderRootComponentAsync(componentId, parameters);
|
||||
=> InvokeAsync(() => base.RenderRootComponentAsync(componentId, parameters));
|
||||
|
||||
public new void DispatchEvent(int componentId, int eventHandlerId, UIEventArgs args)
|
||||
=> base.DispatchEvent(componentId, eventHandlerId, args);
|
||||
{
|
||||
var t = Invoke(() => base.DispatchEvent(componentId, eventHandlerId, args));
|
||||
// This should always be run synchronously
|
||||
Assert.True(t.IsCompleted);
|
||||
if (t.IsFaulted)
|
||||
{
|
||||
var exception = t.Exception.Flatten().InnerException;
|
||||
while (exception is AggregateException e)
|
||||
{
|
||||
exception = e.InnerException;
|
||||
}
|
||||
ExceptionDispatchInfo.Capture(exception).Throw();
|
||||
}
|
||||
}
|
||||
|
||||
public T InstantiateComponent<T>() where T : IComponent
|
||||
=> (T)InstantiateComponent(typeof(T));
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
|
|||
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
|
||||
using Microsoft.AspNetCore.Components.E2ETest.Tests;
|
||||
using OpenQA.Selenium;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
|
@ -32,7 +33,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
appElement.FindElement(By.Id("run-without-dispatch")).Click();
|
||||
|
||||
WaitAssert.Contains(
|
||||
$"{typeof(RemoteRendererException).FullName}: The current thread is not associated with the renderer's synchronization context",
|
||||
$"{typeof(InvalidOperationException).FullName}: The current thread is not associated with the renderer's synchronization context",
|
||||
() => result.Text);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,12 +51,13 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
|||
|
||||
var serviceProvider = htmlHelper.ViewContext.HttpContext.RequestServices;
|
||||
var encoder = serviceProvider.GetRequiredService<HtmlEncoder>();
|
||||
using (var htmlRenderer = new HtmlRenderer(serviceProvider, encoder.Encode))
|
||||
var dispatcher = Renderer.CreateDefaultDispatcher();
|
||||
using (var htmlRenderer = new HtmlRenderer(serviceProvider, encoder.Encode, dispatcher))
|
||||
{
|
||||
var result = await htmlRenderer.RenderComponentAsync<TComponent>(
|
||||
var result = await dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<TComponent>(
|
||||
parameters == null ?
|
||||
ParameterCollection.Empty :
|
||||
ParameterCollection.FromDictionary(HtmlHelper.ObjectToDictionary(parameters)));
|
||||
ParameterCollection.FromDictionary(HtmlHelper.ObjectToDictionary(parameters))));
|
||||
|
||||
return new ComponentHtmlContent(result);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue