Skip to content

Allow Dispatcher to be converted to a TaskScheduler - #21032

Merged
MrJul merged 6 commits into
AvaloniaUI:masterfrom
Enscape:feature/dispatcher-taskscheduler
May 18, 2026
Merged

MrJul merged 6 commits into
AvaloniaUI:masterfrom
Enscape:feature/dispatcher-taskscheduler

Conversation

@TomEdwardsEnscape

@TomEdwardsEnscape TomEdwardsEnscape commented Mar 29, 2026 •

Copy link
Copy Markdown
Contributor

This PR addresses as longstanding irritation with the boilerplate code required to execute continuation tasks on Avalonia's UI thread.

What is the current behaviour?

To ensure that a continuation task executes on the UI thread, you must execute TaskScheduler.FromCurrentSynchronizationContext on the UI thread, and pass the returned object to ContinueWith.

This isn't difficult, but is annoying and unintuitive.

Now that Avalonia permits multiple threads to have dispatchers, the situation is more confusing than ever because you can no longer store the scheduler object in a static property and access this everywhere. You instead need each object tied to a dispatcher to convert Dispatcher.Current to a scheduler by itself.

If you didn't create the scheduler when your object was constructed then to you probably have to post the TaskScheduler.FromCurrentSynchronizationContext call to the dispatcher and wait for it to execute there!

What is the updated/expected behavior with this PR?

Avalonia.Dispatcher can now be converted to TaskScheduler, letting us write simple and intuitive code like this:

myTask.ContinueWith(HandleResultMethod, Dispatcher.ToTaskScheduler());

This will work no matter what thread it's executed on.

Because the dispatcher is being converted to a different type, there is no change to its API. This avoids confusion between Avalonia's invocation methods and those of TaskScheduler.

How was the solution implemented (if it's not obvious)?

Unfortunately Microsoft don't make this job easy. TaskScheduler is an abstract class that you can implement yourself, but if you try then you will find that you can't execute continuation tasks. The method you need to call to do this is internal and exposed only via the object returned by TaskScheduler.FromCurrentSynchronizationContext...which ONLY uses the current thread's context.

So we have to change SynchronizationContext to be the target dispatcher, call the method, then reset SynchronizationContext back to its original value. Luckily this is a per-thread field and there are no side-effects to this switching.

Breaking changes

None.

Obsoletions / Deprecations

None.

@avaloniaui-bot

Copy link
Copy Markdown

You can test this PR using the following package version. 12.0.999-cibuild0064212-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

@kekekeks

Copy link
Copy Markdown
Member

Won't it be cheaper/easier to copy-paste https://github.com/dotnet/runtime/blob/51cfb0e382532e43500216c755d8bd8e1a8f371d/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/TaskScheduler.cs#L536-L602, wire it directly to Dispatcher::Post (or other API) rather than synchronization context and keep a lazy-crated reference in Dispatcher's field?

@kekekeks kekekeks added the api-needs-review The PR adds new public APIs that should be reviewed. label Mar 29, 2026
@TomEdwardsEnscape

Copy link
Copy Markdown
Contributor Author

Won't it be cheaper/easier to copy-paste https://github.com/dotnet/runtime/blob/51cfb0e382532e43500216c755d8bd8e1a8f371d/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/TaskScheduler.cs#L536-L602, wire it directly to Dispatcher::Post (or other API) rather than synchronization context and keep a lazy-crated reference in Dispatcher's field?

This is the class which calls the internal method I mention in the PR description.

@MrJul MrJul added the feature label Mar 30, 2026
@kekekeks

kekekeks commented May 7, 2026

Copy link
Copy Markdown
Member

@MrJul

MrJul commented May 7, 2026

Copy link
Copy Markdown
Member

Notes from the API review meeting:

While the feature itself is good, we'd prefer explicit methods over implicit operators, especially since we need overloads.

  • Add Dispatcher.ToTaskScheduler() that captures the current synchronization context's priority if the dispatchers match.
  • Add Dispatcher.ToTaskScheduler(DispatcherPriority) to specify the priority.

@MrJul MrJul added api-needs-changes The new public APIs need some changes. and removed api-needs-review The PR adds new public APIs that should be reviewed. labels May 7, 2026
@TomEdwardsEnscape

Copy link
Copy Markdown
Contributor Author

API changes made!

Why do we need to call the internal method? It's called by TaskScheduler.TryExecuteTask: https://github.com/dotnet/runtime/blob/5dd24c0265d6682025aa4a2678776b7dc1200b15/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/TaskScheduler.cs#L398-L406

task.ExecutingTaskScheduler != this, so the method throws before executing the task.

@TomEdwardsEnscape TomEdwardsEnscape changed the title Allow Dispatcher to be implicitly cast to TaskScheduler Allow Dispatcher to be converted to a TaskScheduler May 10, 2026
@MrJul

MrJul commented May 15, 2026

Copy link
Copy Markdown
Member

The PR needs to be rebased onto the main branch for the tests to pass, and your branch is protected.

@avaloniaui-bot

Copy link
Copy Markdown

You can test this PR using the following package version. 12.1.999-cibuild0065451-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

@MrJul MrJul left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@MrJul
MrJul added this pull request to the merge queue May 18, 2026
Merged via the queue into AvaloniaUI:master with commit aa7dcf1 May 18, 2026
11 checks passed
@TomEdwardsEnscape
TomEdwardsEnscape deleted the feature/dispatcher-taskscheduler branch May 18, 2026 11:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api-needs-changes The new public APIs need some changes. feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants