Skip to content

Commit a27b9fc

Browse files
authored
Blazor API Review: Design concept for Dispatcher (#11930)
* Design concept for Dispatcher Part of: #11610 This change brings forward the Dispatcher as a more primary and more expandable concept. - Dispatcher shows up in more places - Dispatcher is an abstract class for horizontal scalability - Dispatcher has parallels with S.Windows.Threading.Dispatcher where possible Looking for feedback on this approach. I feel pretty strongly that making this an abstract class is the right choice, I want to see opinions on how much to push it into people's faces. * WIP * PR feedback
1 parent 62f00a7 commit a27b9fc

35 files changed

+333
-249
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
5959
public partial class WebAssemblyRenderer : Microsoft.AspNetCore.Components.Rendering.Renderer
6060
{
6161
public WebAssemblyRenderer(System.IServiceProvider serviceProvider, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory) : base (default(System.IServiceProvider), default(Microsoft.Extensions.Logging.ILoggerFactory)) { }
62+
public override Microsoft.AspNetCore.Components.Dispatcher Dispatcher { get { throw null; } }
6263
public System.Threading.Tasks.Task AddComponentAsync(System.Type componentType, string domElementSelector) { throw null; }
6364
public System.Threading.Tasks.Task AddComponentAsync<TComponent>(string domElementSelector) where TComponent : Microsoft.AspNetCore.Components.IComponent { throw null; }
6465
public override System.Threading.Tasks.Task DispatchEventAsync(int eventHandlerId, Microsoft.AspNetCore.Components.Rendering.EventFieldInfo eventFieldInfo, Microsoft.AspNetCore.Components.UIEventArgs eventArgs) { throw null; }
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Threading.Tasks;
6+
using Microsoft.AspNetCore.Components;
7+
8+
namespace Microsoft.AspNetCore.Blazor.Rendering
9+
{
10+
internal class NullDispatcher : Dispatcher
11+
{
12+
public static readonly Dispatcher Instance = new NullDispatcher();
13+
14+
private NullDispatcher()
15+
{
16+
}
17+
18+
public override bool CheckAccess() => true;
19+
20+
public override Task InvokeAsync(Action workItem)
21+
{
22+
if (workItem is null)
23+
{
24+
throw new ArgumentNullException(nameof(workItem));
25+
}
26+
27+
workItem();
28+
return Task.CompletedTask;
29+
}
30+
31+
public override Task InvokeAsync(Func<Task> workItem)
32+
{
33+
if (workItem is null)
34+
{
35+
throw new ArgumentNullException(nameof(workItem));
36+
}
37+
38+
return workItem();
39+
}
40+
41+
public override Task<TResult> InvokeAsync<TResult>(Func<TResult> workItem)
42+
{
43+
if (workItem is null)
44+
{
45+
throw new ArgumentNullException(nameof(workItem));
46+
}
47+
48+
return Task.FromResult(workItem());
49+
}
50+
51+
public override Task<TResult> InvokeAsync<TResult>(Func<Task<TResult>> workItem)
52+
{
53+
if (workItem is null)
54+
{
55+
throw new ArgumentNullException(nameof(workItem));
56+
}
57+
58+
return workItem();
59+
}
60+
}
61+
}

src/Components/Blazor/Blazor/src/Rendering/WebAssemblyRenderer.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ public WebAssemblyRenderer(IServiceProvider serviceProvider, ILoggerFactory logg
3737
_webAssemblyRendererId = RendererRegistry.Current.Add(this);
3838
}
3939

40+
public override Dispatcher Dispatcher => NullDispatcher.Instance;
41+
4042
/// <summary>
4143
/// Attaches a new root component to the renderer,
4244
/// causing it to be displayed in the specified DOM element.

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

Lines changed: 4 additions & 2 deletions
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)
@@ -442,10 +442,12 @@ protected class CompileToAssemblyResult
442442

443443
protected class TestRenderer : Renderer
444444
{
445-
public TestRenderer() : base(new TestServiceProvider(), NullLoggerFactory.Instance, CreateDefaultDispatcher())
445+
public TestRenderer() : base(new TestServiceProvider(), NullLoggerFactory.Instance)
446446
{
447447
}
448448

449+
public override Dispatcher Dispatcher { get; } = Dispatcher.CreateDefault();
450+
449451
public RenderTreeFrame[] LatestBatchReferenceFrames { get; private set; }
450452

451453
public void AttachComponent(IComponent component)

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: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,10 +88,12 @@ 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)
9292
{
9393
}
9494

95+
public override Dispatcher Dispatcher { get; } = Dispatcher.CreateDefault();
96+
9597
protected override void HandleException(Exception exception)
9698
{
9799
throw new NotImplementedException();

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

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,17 @@ public DataTransfer() { }
8585
public Microsoft.AspNetCore.Components.UIDataTransferItem[] Items { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
8686
public string[] Types { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
8787
}
88+
public abstract partial class Dispatcher
89+
{
90+
protected Dispatcher() { }
91+
public abstract bool CheckAccess();
92+
public static Microsoft.AspNetCore.Components.Dispatcher CreateDefault() { throw null; }
93+
public abstract System.Threading.Tasks.Task InvokeAsync(System.Action workItem);
94+
public abstract System.Threading.Tasks.Task InvokeAsync(System.Func<System.Threading.Tasks.Task> workItem);
95+
public abstract System.Threading.Tasks.Task<TResult> InvokeAsync<TResult>(System.Func<System.Threading.Tasks.Task<TResult>> workItem);
96+
public abstract System.Threading.Tasks.Task<TResult> InvokeAsync<TResult>(System.Func<TResult> workItem);
97+
protected void OnUnhandledException(System.UnhandledExceptionEventArgs e) { }
98+
}
8899
[System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)]
89100
public readonly partial struct ElementRef
90101
{
@@ -303,13 +314,6 @@ public partial interface IComponentContext
303314
{
304315
bool IsConnected { get; }
305316
}
306-
public partial interface IDispatcher
307-
{
308-
System.Threading.Tasks.Task InvokeAsync(System.Action workItem);
309-
System.Threading.Tasks.Task InvokeAsync(System.Func<System.Threading.Tasks.Task> workItem);
310-
System.Threading.Tasks.Task<TResult> InvokeAsync<TResult>(System.Func<System.Threading.Tasks.Task<TResult>> workItem);
311-
System.Threading.Tasks.Task<TResult> InvokeAsync<TResult>(System.Func<TResult> workItem);
312-
}
313317
public partial interface IHandleAfterRender
314318
{
315319
System.Threading.Tasks.Task OnAfterRenderAsync();
@@ -406,9 +410,8 @@ public readonly partial struct RenderHandle
406410
{
407411
private readonly object _dummy;
408412
private readonly int _dummyPrimitive;
413+
public Microsoft.AspNetCore.Components.Dispatcher Dispatcher { get { throw null; } }
409414
public bool IsInitialized { get { throw null; } }
410-
public System.Threading.Tasks.Task InvokeAsync(System.Action workItem) { throw null; }
411-
public System.Threading.Tasks.Task InvokeAsync(System.Func<System.Threading.Tasks.Task> workItem) { throw null; }
412415
public void Render(Microsoft.AspNetCore.Components.RenderFragment renderFragment) { }
413416
}
414417
[System.AttributeUsageAttribute(System.AttributeTargets.Class, AllowMultiple=true, Inherited=false)]
@@ -656,7 +659,8 @@ public EventFieldInfo() { }
656659
}
657660
public partial class HtmlRenderer : Microsoft.AspNetCore.Components.Rendering.Renderer
658661
{
659-
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)) { }
662+
public HtmlRenderer(System.IServiceProvider serviceProvider, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory, System.Func<string, string> htmlEncoder) : base (default(System.IServiceProvider), default(Microsoft.Extensions.Logging.ILoggerFactory)) { }
663+
public override Microsoft.AspNetCore.Components.Dispatcher Dispatcher { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
660664
protected override void HandleException(System.Exception exception) { }
661665
[System.Diagnostics.DebuggerStepThroughAttribute]
662666
public System.Threading.Tasks.Task<Microsoft.AspNetCore.Components.Rendering.ComponentRenderedText> RenderComponentAsync(System.Type componentType, Microsoft.AspNetCore.Components.ParameterCollection initialParameters) { throw null; }
@@ -675,18 +679,15 @@ public readonly partial struct RenderBatch
675679
public abstract partial class Renderer : System.IDisposable
676680
{
677681
public Renderer(System.IServiceProvider serviceProvider, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory) { }
678-
public Renderer(System.IServiceProvider serviceProvider, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory, Microsoft.AspNetCore.Components.IDispatcher dispatcher) { }
682+
public abstract Microsoft.AspNetCore.Components.Dispatcher Dispatcher { get; }
679683
public event System.UnhandledExceptionEventHandler UnhandledSynchronizationException { add { } remove { } }
680684
protected internal virtual void AddToRenderQueue(int componentId, Microsoft.AspNetCore.Components.RenderFragment renderFragment) { }
681685
protected internal int AssignRootComponentId(Microsoft.AspNetCore.Components.IComponent component) { throw null; }
682-
public static Microsoft.AspNetCore.Components.IDispatcher CreateDefaultDispatcher() { throw null; }
683686
public virtual System.Threading.Tasks.Task DispatchEventAsync(int eventHandlerId, Microsoft.AspNetCore.Components.Rendering.EventFieldInfo fieldInfo, Microsoft.AspNetCore.Components.UIEventArgs eventArgs) { throw null; }
684687
public void Dispose() { }
685688
protected virtual void Dispose(bool disposing) { }
686689
protected abstract void HandleException(System.Exception exception);
687690
protected Microsoft.AspNetCore.Components.IComponent InstantiateComponent(System.Type componentType) { throw null; }
688-
public virtual System.Threading.Tasks.Task InvokeAsync(System.Action workItem) { throw null; }
689-
public virtual System.Threading.Tasks.Task InvokeAsync(System.Func<System.Threading.Tasks.Task> workItem) { throw null; }
690691
protected System.Threading.Tasks.Task RenderRootComponentAsync(int componentId) { throw null; }
691692
[System.Diagnostics.DebuggerStepThroughAttribute]
692693
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: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,34 +10,66 @@ 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+
/// Creates a default instance of <see cref="Dispatcher"/>.
17+
/// </summary>
18+
/// <returns>A <see cref="Dispatcher"/> instance.</returns>
19+
public static Dispatcher CreateDefault() => new RendererSynchronizationContextDispatcher();
20+
21+
/// <summary>
22+
/// Provides notifications of unhandled exceptions that occur within the dispatcher.
23+
/// </summary>
24+
internal event UnhandledExceptionEventHandler UnhandledException;
25+
26+
/// <summary>
27+
/// Returns a value that determines whether using the dispatcher to invoke a work item is required
28+
/// from the current context.
29+
/// </summary>
30+
/// <returns><c>true</c> if invoking is required, otherwise <c>false</c>.</returns>
31+
public abstract bool CheckAccess();
32+
1533
/// <summary>
1634
/// Invokes the given <see cref="Action"/> in the context of the associated <see cref="Renderer"/>.
1735
/// </summary>
1836
/// <param name="workItem">The action to execute.</param>
1937
/// <returns>A <see cref="Task"/> that will be completed when the action has finished executing.</returns>
20-
Task InvokeAsync(Action workItem);
38+
public abstract Task InvokeAsync(Action workItem);
2139

2240
/// <summary>
2341
/// Invokes the given <see cref="Func{TResult}"/> in the context of the associated <see cref="Renderer"/>.
2442
/// </summary>
2543
/// <param name="workItem">The asynchronous action to execute.</param>
2644
/// <returns>A <see cref="Task"/> that will be completed when the action has finished executing.</returns>
27-
Task InvokeAsync(Func<Task> workItem);
45+
public abstract Task InvokeAsync(Func<Task> workItem);
2846

2947
/// <summary>
3048
/// Invokes the given <see cref="Func{TResult}"/> in the context of the associated <see cref="Renderer"/>.
3149
/// </summary>
3250
/// <param name="workItem">The function to execute.</param>
3351
/// <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);
52+
public abstract Task<TResult> InvokeAsync<TResult>(Func<TResult> workItem);
3553

3654
/// <summary>
3755
/// Invokes the given <see cref="Func{TResult}"/> in the context of the associated <see cref="Renderer"/>.
3856
/// </summary>
3957
/// <param name="workItem">The asynchronous function to execute.</param>
4058
/// <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);
59+
public abstract Task<TResult> InvokeAsync<TResult>(Func<Task<TResult>> workItem);
60+
61+
/// <summary>
62+
/// Called to notify listeners of an unhandled exception.
63+
/// </summary>
64+
/// <param name="e">The <see cref="UnhandledExceptionEventArgs"/>.</param>
65+
protected void OnUnhandledException(UnhandledExceptionEventArgs e)
66+
{
67+
if (e is null)
68+
{
69+
throw new ArgumentNullException(nameof(e));
70+
}
71+
72+
UnhandledException?.Invoke(this, e);
73+
}
4274
}
4375
}

0 commit comments

Comments
 (0)