Я пытаюсь открыть окно WPF, содержащее элемент управления WebView2, чтобы добавить аутентификацию OpenID Connect в существующее приложение C++ CLI.
Мы используем https://github.com/IdentityModel/IdentityModel.OidcClient.Samples/tree/main/WpfWebView2/WpfWebView2 в качестве основы нашего кода.
Если вы не знакомы с этим примером, то его идея заключается в том, что существует «пользовательский» класс, представляющий «браузер». Этот класс отвечает за создание экземпляра окна WPF, добавление элемента управления WebView2 в качестве его содержимого, переход на сайт OAuth и возврат результатов аутентификации вызывающему объекту. Все это происходит при вызове его метода InvokeAsync()
.
Этот асинхронный метод вызывается классом OidcClient
из библиотеки IdentityModel.OidcClient
. Класс OidcClient
принимает класс настроек, одним из которых является класс, реализующий его интерфейс IBrowser
. Вы запускаете эту логику аутентификации, вызывая OidcClient.LoginAsync
из своего приложения. В моем случае это код C++.
Когда я вызываю LoginAsync()
из своего кода на C++, приложение блокируется и окно не открывается. Я почти уверен, что это проблема с потоком пользовательского интерфейса, но хоть убей, я не могу этого понять.
Я попробовал несколько подходов, включая попытку обернуть вызовы в Application.Current.Dispatcher.Invoke()
и Application.Current.Dispatcher.BeginInvoke()
. Я попытался определить, был ли я в теме пользовательского интерфейса, чтобы создать DispatcherSynchronizationContext
.
Я подозреваю, что проблема в вызове async/await
из C++, который начинается с работы, не связанной с пользовательским интерфейсом, и в какой-то момент должен выполнить работу с пользовательским интерфейсом. С помощью C++ я могу легко создавать экземпляры WPF Window (с помощью gcnew
), а затем вызывать Show()
или ShowDialog()
, но этот многоуровневый дизайн async/await вызывает проблемы.
Я использую LoginAsync().Wait()
со стороны C++, поэтому вполне возможно, что дело просто в неправильном вызове асинхронного метода из C++. Я даже не уверен, где еще искать и какие дополнительные знания мне нужны для отладки этой конкретной настройки.
В последнем абзаце предполагается, что вы вызываете Wait()
для объекта Task .NET. Это просьба заблокировать.
Согласен @BenVoigt. Чем больше я исследую это, тем больше я уверен, что моя основная проблема заключается в том, «как мне вызвать асинхронный метод C# из C++/CLI». У меня нет контроля над библиотекой IdentityModel.OidcClient, но мне нужно использовать ее для отображения окна входа в систему. И его методы асинхронны.
Вызов .Wait()
наверняка вызывает тупик.
Если вам не нужен результат LoginAsync немедленно, вместо этого зарегистрируйте продолжение, используя .ContinueWith(...)
.
Если вам нужен синхронный результат LoginAsync, запустите новый DispatcherFrame в потоке пользовательского интерфейса. Код C# будет выглядеть так:
// Assuming that the 't' is a Task<...>.
var t = LoginAsync();
var frame = new DispatcherFrame();
t.ContinueWith((_) => { frame.Continue = false; }, TaskContinuationOptions.ExecuteSynchronously);
// Block current thread until LoginAsync is completed, while performing all UI stuff.
// (Make sure you call this in UI thread)
Dispatcher.PushFrame(frame);
// Since 't' is already completed, this call won't block the thread.
var loginResult = t.Result;
В C++/CLI нет удобного синтаксиса лямбда-выражений (теперь они есть, но они никогда не были интегрированы с поддержкой .NET, поэтому здесь бесполезны). Таким образом, OP придется создать явную вспомогательную функцию и явно создать экземпляр делегата (управляемой версии указателя функции) для передачи в ContinueWith. Но подход хороший.
Спасибо вам обоим. Я поиграюсь с этим и отчитаюсь. Я не знаком с DispatcherFrame, поэтому мне нужно провести небольшое исследование.
Поскольку мой первоначальный вопрос касался вызова асинхронного метода C# из C++/CLI без блокировки всего приложения, я опубликую здесь свой перенесенный код из ответа, предоставленного Sinus32. (Ему будет дана благодарность за ответ на сообщение.)
При этом я до сих пор не уверен на 100%, почему DispatcherFrame
является решением, а не других диспетчеров, таких как Application.Current.Dispatcher
...
// C# class that gathers OAuth settings from the app config file and ultimately
// calls into IdentityModel.OidcClient to do the authentication
MyCompany::Client::Authentication::IAuthenticationService^ authService =
gcnew MyCompany::Client::Authentication::OidcAuthenticationService();
// PerformLoginAsync calls OidcClient.LoginAsync() which calls IBrowser.InvokeAsync()
// which is responsible for creating the WPF Window with the embedded browser
//
// It is in here where I added code to switch to the WPF UI thread so
// the controls can be created.
auto loginTask = authService->PerformLoginAsync();
auto frame = gcnew System::Windows::Threading::DispatcherFrame();
// Since the callback needs to update the Continue method of DispatcherFrame and the
// callback is of Action<Task>, you need to treat frame variable as a "captured variable"
// and explicitly pass it in. It is my understanding that the dotnet compliler does this
// via a Field but I choose to do so via the ctor
auto helper = gcnew MyCompany::Client::Authentication::DispatcherFrameHelper(frame);
// This is needed since, as Ben Voight mentions, C++/CLI does not support lambdas.
auto callback = gcnew System::Action<System::Threading::Tasks::Task^>(helper, &MyCompany::Client::Authentication::DispatcherFrameHelper::SetDispatcherFrameToContinue);
loginTask->ContinueWith(callback, System::Threading::Tasks::TaskContinuationOptions::ExecuteSynchronously);
System::Windows::Threading::Dispatcher::PushFrame(frame);
auto result = loginTask->Result;
PushFrame
чем-то похож на DoEvents
из WinForms. Dispatcher
и DispatcherFrame
тесно связаны — просто посмотрите, как выглядит метод Dispatcher.Run()
: referencesource.microsoft.com/#WindowsBase/Base/System/Windows/… Во время работы Dispatcher
имеет хотя бы один верхний фрейм. Запуск этого кадра — последнее, что делает Application.Run()
после инициализации. И когда этот верхний кадр завершается, Dispatcher
останавливается, Application.Run()
завершается, static void Main()
заканчивается и приложение закрывается.
В отношении WPF не существует такой вещи, как «вызов async/await из C++».
async
/await
— это ключевые слова C#, которые запускают синтаксический сахар (преобразования компилятора). Современный стандарт C++ имеет свои собственные обещания и синтаксис ожидания, но я почти уверен, что они несовместимы с задачами .NET.