-
Notifications
You must be signed in to change notification settings - Fork 10.3k
[blazor][wasm] Dispatch rendering to main thread #48991
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 14 commits
51c7008
6eb9ee8
db1c1c5
4845353
401bf2c
e7c87d5
fe393bf
ffee1e7
068091f
5e56dff
46fd9b7
ae88c7d
2b79ac1
82d97d9
a44a60f
1c7c442
e4e38e0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
namespace Microsoft.AspNetCore.Components.WebAssembly.Rendering; | ||
|
||
internal sealed class WebAssemblyDispatcher : Dispatcher | ||
{ | ||
public static readonly Dispatcher Instance = new WebAssemblyDispatcher(); | ||
private readonly SynchronizationContext? _context; | ||
|
||
private WebAssemblyDispatcher() | ||
{ | ||
// capture the JSSynchronizationContext from the main thread. | ||
_context = SynchronizationContext.Current; | ||
} | ||
|
||
public override bool CheckAccess() => SynchronizationContext.Current == _context || _context == null; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you explain the I would have thought that if |
||
|
||
public override Task InvokeAsync(Action workItem) | ||
{ | ||
ArgumentNullException.ThrowIfNull(workItem); | ||
if (CheckAccess()) | ||
{ | ||
workItem(); | ||
return Task.CompletedTask; | ||
} | ||
|
||
_context!.InvokeAsync(workItem); | ||
return Task.CompletedTask; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This looks like a mistake, unless I'm confused. Shouldn't this return the task returned by There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It will invoke the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. More details about the expected behavior in https://github.com/dotnet/aspnetcore/pull/48991/files#r1263731418 |
||
} | ||
|
||
public override Task InvokeAsync(Func<Task> workItem) | ||
{ | ||
ArgumentNullException.ThrowIfNull(workItem); | ||
if (CheckAccess()) | ||
{ | ||
return workItem(); | ||
} | ||
|
||
return _context!.InvokeAsync(workItem); | ||
} | ||
|
||
public override Task<TResult> InvokeAsync<TResult>(Func<TResult> workItem) | ||
{ | ||
ArgumentNullException.ThrowIfNull(workItem); | ||
if (CheckAccess()) | ||
{ | ||
return Task.FromResult(workItem()); | ||
} | ||
|
||
return _context!.InvokeAsync(static (workItem) => Task.FromResult(workItem()), workItem); | ||
} | ||
|
||
public override Task<TResult> InvokeAsync<TResult>(Func<Task<TResult>> workItem) | ||
{ | ||
ArgumentNullException.ThrowIfNull(workItem); | ||
if (CheckAccess()) | ||
{ | ||
return workItem(); | ||
} | ||
|
||
return _context!.InvokeAsync(workItem); | ||
} | ||
} | ||
|
||
internal static class SynchronizationContextExtension | ||
{ | ||
public static void InvokeAsync(this SynchronizationContext self, Action body) | ||
pavelsavara marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
Exception? exc = default; | ||
self.Send((_) => | ||
{ | ||
try | ||
{ | ||
body(); | ||
} | ||
catch (Exception ex) | ||
{ | ||
exc = ex; | ||
} | ||
}, null); | ||
if (exc != null) | ||
{ | ||
throw exc; | ||
} | ||
Comment on lines
+75
to
+89
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Doesn't this block? My understanding is that this runs the callback inline, isn't it? I'm trying to make sense of how this works when you are in a background thread and try to dispatch back to the "main" thread. My understanding is that the contract for send must block the thread. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, this will block the sender thread. If this is the main thread, it would just invoke it. |
||
} | ||
|
||
public static TRes InvokeAsync<TRes>(this SynchronizationContext self, Func<TRes> body) | ||
{ | ||
TRes? value = default; | ||
Exception? exc = default; | ||
self.Send((_) => | ||
{ | ||
try | ||
{ | ||
value = body(); | ||
} | ||
catch (Exception ex) | ||
{ | ||
exc = ex; | ||
} | ||
}, null); | ||
if (exc != null) | ||
{ | ||
throw exc; | ||
} | ||
return value!; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The logic around async and exception handling looks strange to me. At least, it will behave very differently from how
Are these differences in behavior intentional? My guess is we should behave the same as There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similarly please see how |
||
|
||
public static TRes InvokeAsync<T1, TRes>(this SynchronizationContext self, Func<T1, TRes> body, T1 p1) | ||
{ | ||
TRes? value = default; | ||
Exception? exc = default; | ||
self.Send((_) => | ||
{ | ||
try | ||
{ | ||
value = body(p1); | ||
} | ||
catch (Exception ex) | ||
{ | ||
exc = ex; | ||
} | ||
}, null); | ||
if (exc != null) | ||
{ | ||
throw exc; | ||
} | ||
return value!; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
<Router AppAssembly="@typeof(ThreadingApp.Program).Assembly"> | ||
<Found Context="routeData"> | ||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" /> | ||
<FocusOnNavigate RouteData="@routeData" Selector="h1" /> | ||
</Found> | ||
<NotFound> | ||
<LayoutView Layout="@typeof(MainLayout)"> | ||
<h2>Not found</h2> | ||
Sorry, there's nothing at this address. | ||
</LayoutView> | ||
</NotFound> | ||
</Router> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
@page "/counter" | ||
pavelsavara marked this conversation as resolved.
Show resolved
Hide resolved
|
||
@using System.Runtime.InteropServices | ||
|
||
<h1>Counter</h1> | ||
|
||
<p>Current count: @currentCount</p> | ||
|
||
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button> | ||
|
||
@code { | ||
int currentCount = 0; | ||
|
||
void IncrementCount() | ||
{ | ||
currentCount++; | ||
} | ||
|
||
protected override async Task OnInitializedAsync() | ||
{ | ||
if(!RuntimeInformation.IsOSPlatform(OSPlatform.Create("Browser"))) | ||
pavelsavara marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
return; | ||
} | ||
|
||
if (Thread.CurrentThread.ManagedThreadId != 1) | ||
{ | ||
throw new Exception("We should be on main thread!"); | ||
} | ||
|
||
Exception exc = null; | ||
try | ||
{ | ||
// send me to the thread pool | ||
await Task.Delay(10).ConfigureAwait(false); | ||
pavelsavara marked this conversation as resolved.
Show resolved
Hide resolved
|
||
StateHasChanged(); // render should throw | ||
} | ||
catch(Exception ex) | ||
{ | ||
exc=ex; | ||
Console.WriteLine(ex.Message); | ||
} | ||
if (exc == null || exc.Message != "The current thread is not associated with the Dispatcher. Use InvokeAsync() to switch execution to the Dispatcher when triggering rendering or component state.") | ||
{ | ||
throw new Exception("We should have thrown here!"); | ||
} | ||
|
||
// test that we could create new thread | ||
var tcs = new TaskCompletionSource<int>(); | ||
var t = new Thread(() => { | ||
tcs.SetResult(Thread.CurrentThread.ManagedThreadId); | ||
}); | ||
t.Start(); | ||
var newThreadId = await tcs.Task; | ||
if (newThreadId == 1){ | ||
throw new Exception("We should be on new thread in the callback!"); | ||
} | ||
|
||
new Timer(async (state) => | ||
{ | ||
// send me to the thread pool | ||
await Task.Delay(10).ConfigureAwait(false); | ||
if (Thread.CurrentThread.ManagedThreadId == 1) | ||
{ | ||
throw new Exception("We should be on thread pool thread!"); | ||
} | ||
|
||
await InvokeAsync(() => | ||
{ | ||
if (Thread.CurrentThread.ManagedThreadId != 1) | ||
{ | ||
throw new Exception("We should be on main thread again!"); | ||
} | ||
// we are back on main thread | ||
pavelsavara marked this conversation as resolved.
Show resolved
Hide resolved
|
||
IncrementCount(); | ||
StateHasChanged(); // render! | ||
}); | ||
}, null, 0, 100); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
@page "/fetchdata" | ||
@page "/fetchdata/{StartDate:datetime}" | ||
@inject HttpClient Http | ||
|
||
<h1>Weather forecast</h1> | ||
|
||
<p>This component demonstrates fetching data from the server.</p> | ||
|
||
@if (forecasts == null) | ||
{ | ||
<p><em>Loading...</em></p> | ||
} | ||
else | ||
{ | ||
<table class='table'> | ||
<thead> | ||
<tr> | ||
<th>Date</th> | ||
<th>Temp. (C)</th> | ||
<th>Temp. (F)</th> | ||
<th>Summary</th> | ||
</tr> | ||
</thead> | ||
<tbody> | ||
@foreach (var forecast in forecasts) | ||
{ | ||
<tr> | ||
<td>@forecast.DateFormatted</td> | ||
<td>@forecast.TemperatureC</td> | ||
<td>@forecast.TemperatureF</td> | ||
<td>@forecast.Summary</td> | ||
</tr> | ||
} | ||
</tbody> | ||
</table> | ||
<p> | ||
<a href="fetchdata/@startDate.AddDays(-5).ToString("yyyy-MM-dd")" class="btn btn-secondary float-left"> | ||
◀ Previous | ||
</a> | ||
<a href="fetchdata/@startDate.AddDays(5).ToString("yyyy-MM-dd")" class="btn btn-secondary float-right"> | ||
Next ▶ | ||
</a> | ||
</p> | ||
} | ||
|
||
@code { | ||
[Parameter] public DateTime? StartDate { get; set; } | ||
|
||
WeatherForecast[] forecasts; | ||
DateTime startDate; | ||
|
||
protected override async Task OnParametersSetAsync() | ||
{ | ||
startDate = StartDate.GetValueOrDefault(DateTime.Now); | ||
|
||
// send me to the thread pool, next line HTTP should work anyway | ||
await Task.Delay(10).ConfigureAwait(false); | ||
|
||
forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>( | ||
$"sample-data/weather.json?date={startDate.ToString("yyyy-MM-dd")}"); | ||
|
||
// Because ThreadingApp doesn't really have a server endpoint to get dynamic data from, | ||
// fake the DateFormatted values here. This would not apply in a real app. | ||
for (var i = 0; i < forecasts.Length; i++) | ||
{ | ||
forecasts[i].DateFormatted = startDate.AddDays(i).ToShortDateString(); | ||
} | ||
} | ||
|
||
class WeatherForecast | ||
{ | ||
public string DateFormatted { get; set; } | ||
public int TemperatureC { get; set; } | ||
public int TemperatureF { get; set; } | ||
public string Summary { get; set; } | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.