Skip to content

Commit 32e9188

Browse files
author
Ryan Nowak
committed
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.
1 parent 6978590 commit 32e9188

28 files changed

+275
-185
lines changed

src/Components/Blazor/Build/test/RazorIntegrationTestBase.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,7 @@ protected RenderTreeFrame[] GetRenderTree(IComponent component)
385385
protected private RenderTreeFrame[] GetRenderTree(TestRenderer renderer, IComponent component)
386386
{
387387
renderer.AttachComponent(component);
388-
var task = renderer.InvokeAsync(() => component.SetParametersAsync(ParameterCollection.Empty));
388+
var task = renderer.Dispatcher.InvokeAsync(() => component.SetParametersAsync(ParameterCollection.Empty));
389389
// we will have to change this method if we add a test that does actual async work.
390390
Assert.True(task.Status.HasFlag(TaskStatus.RanToCompletion) || task.Status.HasFlag(TaskStatus.Faulted));
391391
if (task.IsFaulted)

src/Components/Blazor/Build/test/RenderingRazorIntegrationTest.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,7 @@ public async Task SupportsTwoWayBindingForTextboxes()
332332
// Trigger the change event to show it updates the property
333333
//
334334
// This should always complete synchronously.
335-
var task = renderer.InvokeAsync(() => setter.InvokeAsync(new UIChangeEventArgs { Value = "Modified value", }));
335+
var task = renderer.Dispatcher.InvokeAsync(() => setter.InvokeAsync(new UIChangeEventArgs { Value = "Modified value", }));
336336
Assert.Equal(TaskStatus.RanToCompletion, task.Status);
337337
await task;
338338

@@ -367,7 +367,7 @@ public async Task SupportsTwoWayBindingForTextareas()
367367
// Trigger the change event to show it updates the property
368368
//
369369
// This should always complete synchronously.
370-
var task = renderer.InvokeAsync(() => setter.InvokeAsync(new UIChangeEventArgs { Value = "Modified value", }));
370+
var task = renderer.Dispatcher.InvokeAsync(() => setter.InvokeAsync(new UIChangeEventArgs { Value = "Modified value", }));
371371
Assert.Equal(TaskStatus.RanToCompletion, task.Status);
372372
await task;
373373

@@ -404,7 +404,7 @@ public async Task SupportsTwoWayBindingForDateValues()
404404
//
405405
// This should always complete synchronously.
406406
var newDateValue = new DateTime(2018, 3, 5, 4, 5, 6);
407-
var task = renderer.InvokeAsync(() => setter.InvokeAsync(new UIChangeEventArgs { Value = newDateValue.ToString(), }));
407+
var task = renderer.Dispatcher.InvokeAsync(() => setter.InvokeAsync(new UIChangeEventArgs { Value = newDateValue.ToString(), }));
408408
Assert.Equal(TaskStatus.RanToCompletion, task.Status);
409409
await task;
410410

@@ -440,7 +440,7 @@ public async Task SupportsTwoWayBindingForDateValuesWithFormatString()
440440
// Trigger the change event to show it updates the property
441441
//
442442
// This should always complete synchronously.
443-
var task = renderer.InvokeAsync(() => setter.InvokeAsync(new UIChangeEventArgs { Value = new DateTime(2018, 3, 5).ToString(testDateFormat), }));
443+
var task = renderer.Dispatcher.InvokeAsync(() => setter.InvokeAsync(new UIChangeEventArgs { Value = new DateTime(2018, 3, 5).ToString(testDateFormat), }));
444444
Assert.Equal(TaskStatus.RanToCompletion, task.Status);
445445
await task;
446446

@@ -559,7 +559,7 @@ public async Task SupportsTwoWayBindingForBoolValues()
559559
// Trigger the change event to show it updates the property
560560
//
561561
// This should always complete synchronously.
562-
var task = renderer.InvokeAsync(() => setter.InvokeAsync(new UIChangeEventArgs() { Value = false, }));
562+
var task = renderer.Dispatcher.InvokeAsync(() => setter.InvokeAsync(new UIChangeEventArgs() { Value = false, }));
563563
Assert.Equal(TaskStatus.RanToCompletion, task.Status);
564564
await task;
565565

@@ -595,7 +595,7 @@ public async Task SupportsTwoWayBindingForEnumValues()
595595
// Trigger the change event to show it updates the property
596596
//
597597
// This should always complete synchronously.
598-
var task = renderer.InvokeAsync(() => setter.InvokeAsync(new UIChangeEventArgs { Value = MyEnum.SecondValue.ToString(), }));
598+
var task = renderer.Dispatcher.InvokeAsync(() => setter.InvokeAsync(new UIChangeEventArgs { Value = MyEnum.SecondValue.ToString(), }));
599599
Assert.Equal(TaskStatus.RanToCompletion, task.Status);
600600
await task;
601601

src/Components/Components/perf/RenderTreeDiffBuilderBenchmark.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ public void ComputeDiff_SingleFormField()
8888
private class FakeRenderer : Renderer
8989
{
9090
public FakeRenderer()
91-
: base(new TestServiceProvider(), NullLoggerFactory.Instance, new RendererSynchronizationContext())
91+
: base(new TestServiceProvider(), NullLoggerFactory.Instance, new RendererSynchronizationContextDispatcher())
9292
{
9393
}
9494

src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.cs

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,16 @@ public DataTransfer() { }
8282
public Microsoft.AspNetCore.Components.UIDataTransferItem[] Items { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
8383
public string[] Types { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
8484
}
85+
public abstract partial class Dispatcher
86+
{
87+
protected Dispatcher() { }
88+
public abstract event System.UnhandledExceptionEventHandler UnhandledException;
89+
public abstract bool CheckAccess();
90+
public abstract System.Threading.Tasks.Task InvokeAsync(System.Action workItem);
91+
public abstract System.Threading.Tasks.Task InvokeAsync(System.Func<System.Threading.Tasks.Task> workItem);
92+
public abstract System.Threading.Tasks.Task<TResult> InvokeAsync<TResult>(System.Func<System.Threading.Tasks.Task<TResult>> workItem);
93+
public abstract System.Threading.Tasks.Task<TResult> InvokeAsync<TResult>(System.Func<TResult> workItem);
94+
}
8595
[System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)]
8696
public readonly partial struct ElementRef
8797
{
@@ -295,13 +305,6 @@ public partial interface IComponentContext
295305
{
296306
bool IsConnected { get; }
297307
}
298-
public partial interface IDispatcher
299-
{
300-
System.Threading.Tasks.Task InvokeAsync(System.Action workItem);
301-
System.Threading.Tasks.Task InvokeAsync(System.Func<System.Threading.Tasks.Task> workItem);
302-
System.Threading.Tasks.Task<TResult> InvokeAsync<TResult>(System.Func<System.Threading.Tasks.Task<TResult>> workItem);
303-
System.Threading.Tasks.Task<TResult> InvokeAsync<TResult>(System.Func<TResult> workItem);
304-
}
305308
public partial interface IHandleAfterRender
306309
{
307310
System.Threading.Tasks.Task OnAfterRenderAsync();
@@ -386,9 +389,8 @@ public readonly partial struct RenderHandle
386389
{
387390
private readonly object _dummy;
388391
private readonly int _dummyPrimitive;
392+
public Microsoft.AspNetCore.Components.Dispatcher Dispatcher { get { throw null; } }
389393
public bool IsInitialized { get { throw null; } }
390-
public System.Threading.Tasks.Task InvokeAsync(System.Action workItem) { throw null; }
391-
public System.Threading.Tasks.Task InvokeAsync(System.Func<System.Threading.Tasks.Task> workItem) { throw null; }
392394
public void Render(Microsoft.AspNetCore.Components.RenderFragment renderFragment) { }
393395
}
394396
[System.AttributeUsageAttribute(System.AttributeTargets.Class, AllowMultiple=true, Inherited=false)]
@@ -649,7 +651,7 @@ public EventFieldInfo() { }
649651
}
650652
public partial class HtmlRenderer : Microsoft.AspNetCore.Components.Rendering.Renderer
651653
{
652-
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)) { }
654+
public HtmlRenderer(System.IServiceProvider serviceProvider, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory, Microsoft.AspNetCore.Components.Dispatcher dispatcher, System.Func<string, string> htmlEncoder) : base (default(System.IServiceProvider), default(Microsoft.Extensions.Logging.ILoggerFactory)) { }
653655
protected override void HandleException(System.Exception exception) { }
654656
[System.Diagnostics.DebuggerStepThroughAttribute]
655657
public System.Threading.Tasks.Task<Microsoft.AspNetCore.Components.Rendering.ComponentRenderedText> RenderComponentAsync(System.Type componentType, Microsoft.AspNetCore.Components.ParameterCollection initialParameters) { throw null; }
@@ -668,18 +670,17 @@ public readonly partial struct RenderBatch
668670
public abstract partial class Renderer : System.IDisposable
669671
{
670672
public Renderer(System.IServiceProvider serviceProvider, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory) { }
671-
public Renderer(System.IServiceProvider serviceProvider, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory, Microsoft.AspNetCore.Components.IDispatcher dispatcher) { }
673+
public Renderer(System.IServiceProvider serviceProvider, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory, Microsoft.AspNetCore.Components.Dispatcher dispatcher) { }
674+
public Microsoft.AspNetCore.Components.Dispatcher Dispatcher { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
672675
public event System.UnhandledExceptionEventHandler UnhandledSynchronizationException { add { } remove { } }
673676
protected internal virtual void AddToRenderQueue(int componentId, Microsoft.AspNetCore.Components.RenderFragment renderFragment) { }
674677
protected internal int AssignRootComponentId(Microsoft.AspNetCore.Components.IComponent component) { throw null; }
675-
public static Microsoft.AspNetCore.Components.IDispatcher CreateDefaultDispatcher() { throw null; }
678+
public static Microsoft.AspNetCore.Components.Dispatcher CreateDefaultDispatcher() { throw null; }
676679
public virtual System.Threading.Tasks.Task DispatchEventAsync(int eventHandlerId, Microsoft.AspNetCore.Components.Rendering.EventFieldInfo fieldInfo, Microsoft.AspNetCore.Components.UIEventArgs eventArgs) { throw null; }
677680
public void Dispose() { }
678681
protected virtual void Dispose(bool disposing) { }
679682
protected abstract void HandleException(System.Exception exception);
680683
protected Microsoft.AspNetCore.Components.IComponent InstantiateComponent(System.Type componentType) { throw null; }
681-
public virtual System.Threading.Tasks.Task InvokeAsync(System.Action workItem) { throw null; }
682-
public virtual System.Threading.Tasks.Task InvokeAsync(System.Func<System.Threading.Tasks.Task> workItem) { throw null; }
683684
protected System.Threading.Tasks.Task RenderRootComponentAsync(int componentId) { throw null; }
684685
[System.Diagnostics.DebuggerStepThroughAttribute]
685686
protected System.Threading.Tasks.Task RenderRootComponentAsync(int componentId, Microsoft.AspNetCore.Components.ParameterCollection initialParameters) { throw null; }

src/Components/Components/src/ComponentBase.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,15 +148,15 @@ protected virtual Task OnAfterRenderAsync()
148148
/// </summary>
149149
/// <param name="workItem">The work item to execute.</param>
150150
protected Task InvokeAsync(Action workItem)
151-
=> _renderHandle.InvokeAsync(workItem);
151+
=> _renderHandle.Dispatcher.InvokeAsync(workItem);
152152

153153
/// <summary>
154154
/// Executes the supplied work item on the associated renderer's
155155
/// synchronization context.
156156
/// </summary>
157157
/// <param name="workItem">The work item to execute.</param>
158158
protected Task InvokeAsync(Func<Task> workItem)
159-
=> _renderHandle.InvokeAsync(workItem);
159+
=> _renderHandle.Dispatcher.InvokeAsync(workItem);
160160

161161
void IComponent.Configure(RenderHandle renderHandle)
162162
{

src/Components/Components/src/IDispatcher.cs renamed to src/Components/Components/src/Dispatcher.cs

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,34 +10,46 @@ namespace Microsoft.AspNetCore.Components
1010
/// <summary>
1111
/// Dispatches external actions to be executed on the context of a <see cref="Renderer"/>.
1212
/// </summary>
13-
public interface IDispatcher
13+
public abstract class Dispatcher
1414
{
15+
/// <summary>
16+
/// Provides notifications of unhandled exceptions that occur within the dispatcher.
17+
/// </summary>
18+
public abstract event UnhandledExceptionEventHandler UnhandledException;
19+
20+
/// <summary>
21+
/// Returns a value that determines whether using the dispatcher to invoke a work item is required
22+
/// from the current context.
23+
/// </summary>
24+
/// <returns><c>true</c> if invoking is required, otherwise <c>false</c>.</returns>
25+
public abstract bool CheckAccess();
26+
1527
/// <summary>
1628
/// Invokes the given <see cref="Action"/> in the context of the associated <see cref="Renderer"/>.
1729
/// </summary>
1830
/// <param name="workItem">The action to execute.</param>
1931
/// <returns>A <see cref="Task"/> that will be completed when the action has finished executing.</returns>
20-
Task InvokeAsync(Action workItem);
32+
public abstract Task InvokeAsync(Action workItem);
2133

2234
/// <summary>
2335
/// Invokes the given <see cref="Func{TResult}"/> in the context of the associated <see cref="Renderer"/>.
2436
/// </summary>
2537
/// <param name="workItem">The asynchronous action to execute.</param>
2638
/// <returns>A <see cref="Task"/> that will be completed when the action has finished executing.</returns>
27-
Task InvokeAsync(Func<Task> workItem);
39+
public abstract Task InvokeAsync(Func<Task> workItem);
2840

2941
/// <summary>
3042
/// Invokes the given <see cref="Func{TResult}"/> in the context of the associated <see cref="Renderer"/>.
3143
/// </summary>
3244
/// <param name="workItem">The function to execute.</param>
3345
/// <returns>A <see cref="Task{TResult}"/> that will be completed when the function has finished executing.</returns>
34-
Task<TResult> InvokeAsync<TResult>(Func<TResult> workItem);
46+
public abstract Task<TResult> InvokeAsync<TResult>(Func<TResult> workItem);
3547

3648
/// <summary>
3749
/// Invokes the given <see cref="Func{TResult}"/> in the context of the associated <see cref="Renderer"/>.
3850
/// </summary>
3951
/// <param name="workItem">The asynchronous function to execute.</param>
4052
/// <returns>A <see cref="Task{TResult}"/> that will be completed when the function has finished executing.</returns>
41-
Task<TResult> InvokeAsync<TResult>(Func<Task<TResult>> workItem);
53+
public abstract Task<TResult> InvokeAsync<TResult>(Func<Task<TResult>> workItem);
4254
}
4355
}

src/Components/Components/src/RenderHandle.cs

Lines changed: 21 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,12 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5-
using System.Threading.Tasks;
65
using Microsoft.AspNetCore.Components.Rendering;
76

87
namespace Microsoft.AspNetCore.Components
98
{
109
/// <summary>
11-
/// Allows a component to notify the renderer that it should be rendered.
10+
/// Allows a component to interact with its renderer.
1211
/// </summary>
1312
public readonly struct RenderHandle
1413
{
@@ -21,9 +20,25 @@ internal RenderHandle(Renderer renderer, int componentId)
2120
_componentId = componentId;
2221
}
2322

23+
/// <summary>
24+
/// Gets the <see cref="Microsoft.AspNetCore.Components.Dispatcher" /> associated with the component.
25+
/// </summary>
26+
public Dispatcher Dispatcher
27+
{
28+
get
29+
{
30+
if (_renderer == null)
31+
{
32+
ThrowNotInitialized();
33+
}
34+
35+
return _renderer.Dispatcher;
36+
}
37+
}
38+
2439
/// <summary>
2540
/// Gets a value that indicates whether the <see cref="RenderHandle"/> has been
26-
/// initialised and is ready to use.
41+
/// initialized and is ready to use.
2742
/// </summary>
2843
public bool IsInitialized
2944
=> _renderer != null;
@@ -36,38 +51,15 @@ public void Render(RenderFragment renderFragment)
3651
{
3752
if (_renderer == null)
3853
{
39-
throw new InvalidOperationException("The render handle is not yet assigned.");
54+
ThrowNotInitialized();
4055
}
4156

4257
_renderer.AddToRenderQueue(_componentId, renderFragment);
4358
}
4459

45-
/// <summary>
46-
/// Executes the supplied work item on the renderer's
47-
/// synchronization context.
48-
/// </summary>
49-
/// <param name="workItem">The work item to execute.</param>
50-
public Task InvokeAsync(Action workItem)
60+
private static void ThrowNotInitialized()
5161
{
52-
if (_renderer == null)
53-
{
54-
throw new InvalidOperationException("The render handle is not yet assigned.");
55-
}
56-
return _renderer.InvokeAsync(workItem);
57-
}
58-
59-
/// <summary>
60-
/// Executes the supplied work item on the renderer's
61-
/// synchronization context.
62-
/// </summary>
63-
/// <param name="workItem">The work item to execute.</param>
64-
public Task InvokeAsync(Func<Task> workItem)
65-
{
66-
if (_renderer == null)
67-
{
68-
throw new InvalidOperationException("The render handle is not yet assigned.");
69-
}
70-
return _renderer.InvokeAsync(workItem);
62+
throw new InvalidOperationException("The render handle is not yet assigned.");
7163
}
7264
}
7365
}

src/Components/Components/src/Rendering/HtmlRenderer.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ public class HtmlRenderer : Renderer
2828
/// </summary>
2929
/// <param name="serviceProvider">The <see cref="IServiceProvider"/> to use to instantiate components.</param>
3030
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
31-
/// <param name="dispatcher">The <see cref="IDispatcher"/> to be for invoking user actions into the <see cref="Renderer"/> context.</param>
31+
/// <param name="dispatcher">The <see cref="Dispatcher"/> to be for invoking user actions into the <see cref="Renderer"/> context.</param>
3232
/// <param name="htmlEncoder">A <see cref="Func{T, TResult}"/> that will HTML encode the given string.</param>
33-
public HtmlRenderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory, IDispatcher dispatcher, Func<string, string> htmlEncoder)
33+
public HtmlRenderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory, Dispatcher dispatcher, Func<string, string> htmlEncoder)
3434
: base(serviceProvider, loggerFactory, dispatcher)
3535
{
3636
_htmlEncoder = htmlEncoder;

0 commit comments

Comments
 (0)