Skip to content
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

Wrapped UI dependant calls in WindowsProvider with dispatchers #136

Merged
merged 1 commit into from
Jul 30, 2021

Conversation

shweaver-MSFT
Copy link
Member

Fixes #132

PR Type

What kind of change does this PR introduce?

  • Bugfix

What is the current behavior?

Currently, UI based calls in the WindowsProvider are not protected in anyway. Causing occasional failures when called from a non-UI thread.

What is the new behavior?

The UI dependant calls in WindowsPRovider are now wrapped with calls to DispatcherQueue.GetForCurrentThread().TryEnqueue(...); preventing threading errors regardless of calling location.

PR Checklist

Please check if your PR fulfills the following requirements:

  • Tested code with current supported SDKs
  • Sample in sample app has been added / updated (for bug fixes / features)
  • Tests for the changes have been added (for bug fixes / features) (if applicable)
  • Header has been added to all new source files (run build/UpdateHeaders.bat)
  • Contains NO breaking changes

Other information

@shweaver-MSFT shweaver-MSFT added this to the 7.1.0 milestone Jul 29, 2021
@shweaver-MSFT shweaver-MSFT self-assigned this Jul 29, 2021
@ghost
Copy link

ghost commented Jul 29, 2021

Thanks shweaver-MSFT for opening a Pull Request! The reviewers will test the PR and highlight if there is any merge conflict or changes required. If the PR is approved we will proceed to merge the pull request 🙌

@shweaver-MSFT shweaver-MSFT removed this from the 7.1.0 milestone Jul 29, 2021
@shweaver-MSFT shweaver-MSFT merged commit c23c3e0 into dev/7.1.0 Jul 30, 2021
@shweaver-MSFT shweaver-MSFT deleted the shweaver/132 branch July 30, 2021 19:46
@@ -197,7 +198,21 @@ public override async Task<string> GetTokenAsync(bool silentOnly = false)
}

// Attempt to authenticate interactively.
authResult = await AuthenticateInteractiveAsync(_scopes);
var tcs = new TaskCompletionSource<WebTokenRequestResult>();
var taskQueued = DispatcherQueue.GetForCurrentThread().TryEnqueue(async () =>
Copy link
Member

Choose a reason for hiding this comment

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

GetForCurrentThread gets the current thread still. So if SignInAsync isn't called on the UI Thread, this won't dispatch back to the UI Thread.

Instead, you want to grab a reference to the DispatcherQueue of the UI thread, for instance if you know WindowsProvider will always be constructed from the UI of the application, grab it in the constructor. Then when you use it here, it'll dispatch to that original UI thread vs. whatever thread SignInAsync is called from.

Think I got that right @azchohfi?

Copy link
Member Author

@shweaver-MSFT shweaver-MSFT Jul 30, 2021

Choose a reason for hiding this comment

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

Can I actually safely assume that the WindowsProvider will be constructed from the UI thread? Or I guess I'd be enforcing that it be constructed from the UI thread by accepting the constructor param, right?

Copy link
Member

Choose a reason for hiding this comment

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

I mean that's usually a documentation thing like 'you must construct the provider on the UI thread' but you can also just accept the DispatcherQueue as a constructor parameter. @azchohfi did that in a few cases for the Toolkit I think.

For controls we always know the constructor is called on the UI thread as the XAML engine is creating it. Here, as long as they're creating the provider in their App it should as well I think?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yeah, I would create a test for this to make sure we are covering all the cases. I don't believe we can force every developer to create this from the UI thread (using it in XAML), right?
You can expose a DispatcherQueue property that is automatically populated in the ctor, if it finds one. If it doesn't, then when you try to use it and it is null, throw an exception to tell the developer that they have to either call the ctor in the ui thread, or manually set the dispatcher once they have it. Make it a get;set; prop, not readonly. Does that make sense? I believe I followed this pattern in the Xaml Islands compat pass that I did a few years ago.

Copy link
Member

Choose a reason for hiding this comment

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

I think this is the pattern Alex is talking about:

https://github.com/CommunityToolkit/WindowsCommunityToolkit/blob/63cbb4a51bdef59083ccfc86bbcba41f966d0027/Microsoft.Toolkit.Uwp.UI/Helpers/ThemeListener.cs#L48-L51

https://github.com/CommunityToolkit/WindowsCommunityToolkit/blob/e4e06cbea6790b744b01a1e9630cd9a710814654/Microsoft.Toolkit.Uwp/Helpers/RemoteDeviceHelper/RemoteDeviceHelper.cs#L46-L49

(ugh really need GitHub to inline code-snippets across repositories)

I didn't see us checking though, just either taking the one in or grabbing the current thread, doc comment calls out constructing on UI thread. I don't think we have an explicit way to be able to determine if a particular queue is the 'ui' one, eh?

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't think we have an explicit way to be able to determine if a particular queue is the 'ui' one, eh?

Not that I know of 🤔 But thanks for the suggestion @azchohf, and thanks for the links @michael-hawker! I can totally implement that 😁

Copy link

Choose a reason for hiding this comment

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

I am curious to know what's the problem in using CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync:

await CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(
    Windows.UI.Core.CoreDispatcherPriority.Normal, async () =>
    {
        var result = await AuthenticateInteractiveAsync(_scopes);
        tcs.SetResult(result);
    });

You want to avoid referencing CoreWindow for some reason?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes, we want to avoid using CoreWindow. CoreWindow have a concept of single instance. It is literally a singleton. The problem with it is that if you have multiple windows in your app (running in different threads), then you can't know which Dispatcher to dispatch to. This is particularly an issue with Win32, meaning Xaml Islands and WinUI3. The DispatcherQueue was literally built to fix this issue. It works on both UWP and Win32 contexts.

Copy link

Choose a reason for hiding this comment

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

OK, thanks for the clarification. While waiting for your fix I fixed this locally using CoreWindow, but it is an UWP app.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants