Skip to content

[Feature] Dispatcher.YieldAsync() for UWP #3064

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

Closed
hez2010 opened this issue Nov 18, 2019 · 10 comments
Closed

[Feature] Dispatcher.YieldAsync() for UWP #3064

hez2010 opened this issue Nov 18, 2019 · 10 comments
Labels
feature request 📬 A request for new changes to improve functionality help wanted Issues identified as good community contribution opportunities needs attention 👋

Comments

@hez2010
Copy link

hez2010 commented Nov 18, 2019

Describe the problem this feature would solve

Notify to update UI while executing code on a UI thread without needs of executing code on Dispatcher explicitly. It's useful for updating a progress bar in an event handler.

Describe the solution

protected async void OnXXXX(object sender, EventArgs e) 
{
    ProgressBar.Value = 10;
    await Dispatcher.YieldAsync(); // proceeds UI updates
    ProgressBar.Value = 20;
}

Instead of

protected async void OnXXXX(object sender, EventArgs e) 
{
    Dispatcher.Invoke(() => ProgressBar.Value = 10);
    Dispatcher.Invoke(() => ProgressBar.Value = 20);
}
@hez2010 hez2010 added the feature request 📬 A request for new changes to improve functionality label Nov 18, 2019
@ghost ghost added the needs triage 🔍 label Nov 18, 2019
@ghost
Copy link

ghost commented Nov 18, 2019

Thanks for submitting a new feature request! I've automatically added a vote reaction to help get things started. Other community members can vote to help us prioritize this feature in the future!

@ghost
Copy link

ghost commented Dec 3, 2019

This issue has been marked as "needs attention 👋" due to no activity for 15 days. Please triage the issue so the fix can be established.

@Kyaa-dost Kyaa-dost added help wanted Issues identified as good community contribution opportunities and removed needs triage 🔍 labels Dec 3, 2019
@Sergio0694
Copy link
Member

This seems like an interesting feature, and nice job with the PR as well, I appreciate the insights that went into building that awaitable type. I just have a few question on the feature as a whole.

I'm not entirely sure what's an actual, real-world use case scenario for this. I saw the sample code in this issue and I get how that would work, but I'm confused as to what actual problem users might face would be solved by this new featute. You would typically need to yield somehow (or to run an update on the dispatcher) when you're executing blocking code on the UI thread. But the point is that you really shouldn't be doing that in the first place: if you need to run some CPU-bound stuff you (eg. some processing loop of some kind) you should just offload that to Task.Run and await that, possibly with a callback that dispatches progress notifications to the UI thread. On that note, that's exactly why the IProgress<T> interface and the Progress<T> type were designed for, since they automatically capture the original synchronization context. They're specifically meant to be used to report progress back to another thread, which appears to be the same issue for this issue. So I'm not really sure about this 🤔

Here's a very simple example, suppose you have this in code-behind somewhere:

int[] data = new int[100];

// Instead of this...
for (int i = 0; i < data.Length; i++)
{
    ExpensiveProcessItem(data[i]);

    ProgressBar.Value = i;
    await Dispatcher.YieldAsync();
}

// Shouldn't it be this?
IProgress<int> progress = new Progress<int>(i => ProgressBar.Value = i);
await Task.Run(() =>
{
    for (int i = 0; i < data.Length; i++)
    {
        ExpensiveProcessItem(data[i]);

        progress.Report(i);
    }
});

Of course, in a real-world scenario you'd likely want to wrap that items processing as a method in your backend and only expose an API taking an IProgress<int> parameter and the input data, so that that part can be fully decoupled from the UI. Just keeping all here for clarity in this case.

Overall, I just feel like this feature could potentially incentivize devs to put more "backend" code in code-behind, using this YieldAsync extension to display updates, instead of properly dispatch to a thread pool thread using the expected patterns/APIs, which would also result in a snappier app in general in most cases. Just sharing my thoughts on this here, and I might very well be missing something here, so feel free to let me know what you think about this! 😊

@hez2010
Copy link
Author

hez2010 commented Apr 1, 2020

Sometimes we only need a few simple UI updates apart from progress report, such as debounce and status indicator.
For example:

private async void Button_Clicked(......)
{
    Button.IsEnabled = false;
    ProgressRing.IsActive = true;
    await Dispatcher.YieldAsync();
    await DoNetworkRequest();
    Button.IsEnabled = true;
    ProgressRing.IsActive = false;
}

@skendrot
Copy link
Contributor

skendrot commented Apr 1, 2020

@hez2010 How does the sample above benefit? As soon as DoNetworkRequest starts the request the async operation starts and the Button_Clicked method will be exited giving control back to the UI thread.
I have to agree with @Sergio0694 on the use of IProgress

@hez2010
Copy link
Author

hez2010 commented May 2, 2020

Consider this ViewModel:

private ObservableCollection<string> list; // which has been bind to a ListBox
<ListBox ItemSource="{x:Bind list, Mode=OneWay}" />

Now you need to insert plenty of entries to the list:

for (var i = 0; i < 100000; i++)
{
    list.Add($"test{i}");
}

which will lead to long-time UI frozen.

But with Dispatcher.YieldAsync():

for (var i = 0; i < 100000; i++)
{
    list.Add($"test{i}");
    if (i % 100 == 0) await Dispatcher.YieldAsync();
}

It can make UI response while inserting to the list, and you can operate the part which has been inserted to the list without need of waiting all data to be inserted.

It make less use of IProgress and is easier to implement.

@Sergio0694
Copy link
Member

I'm not sure I'm following your example 🤔

If you use the MVVM pattern, that list would be in your viewmodel, and you'd do all operations from there - the ListView would simply bind to that collection, but you'd never modify it from code behind. And your viewmodel wouldn't be able to use that YieldAsync call anyway, as it would have no concept of what a Dispatcher is at all, as that's a UI-related component that should never be visible from a viewmodel (imagine your viewmodels being all in a separate, .NET Standard and platform-agnostic project). I mean, in general a viewmodel should include no *.UI.* directives at all.

Also other than the point above, I wonder how this ties in with the DispatcherQueue APIs that the whole library is switching to (see #3205) . As far as I know, the usage of the Dispatcher class is basically being deprecated, as it doesn't work when using XAML islands, so I wonder if it makes sense to add a new Dispatcher-based API at this point in time for this reason as well.

@hez2010
Copy link
Author

hez2010 commented May 2, 2020

@Sergio0694 Actually you can get a Dispatcher from CoreApplication.MainView.Dispatcher

@Sergio0694
Copy link
Member

Yeah I know that, I never said that retrieving a Dispatcher outside of a view is not possible.
What I said is that:

  • You should have your viewmodels either in a separate .NET Standard project, or anyway just act like they were and write them without assuming any UI-specific API being present. Dispatcher is strictly a UI specific API, and as such it should never be used from a viewmodel - it goes against the whole point of MVVM as it breaks the modularization and causes a strong dependency.
  • Given that Dispatcher should no longer be used now, and instead DispatcherQueue should be used to replace it (see that issue/PR I linked in my previous message), it seems like adding a new extension specifically for the Dispatcher class at this time has a bad timing. It would basically end up being deprecated almost immediately after being hypothetically shipped in the toolkit.

Hope that makes sense!

@hez2010
Copy link
Author

hez2010 commented May 2, 2020

Thanks. I'll try out the new DispatcherQueue, and I'm closing this issue and related PR.

@hez2010 hez2010 closed this as completed May 2, 2020
@ghost ghost locked as resolved and limited conversation to collaborators Jul 1, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
feature request 📬 A request for new changes to improve functionality help wanted Issues identified as good community contribution opportunities needs attention 👋
Projects
None yet
Development

No branches or pull requests

4 participants