Постановка задачи
В рамках разработки одного приложения потребовалось реализовать такую схему:
- Асинхронный метод запрашивает данные
- Пользователь вводит данные с клавиатуры
- Метод получает результат ввода как результат выполнения функции и продолжает с того же места
Дополнительные требование: Не создавать дополнительных окон.
Казалось бы, просто? Как оказалось, действительно просто. Но обо всём по порядку.
Решение
Первая попытка сделать это в лоб и без поиска в интернете привела к блокировке основного потока, и, следовательно, ни к чему хорошему. И я уже собирался использовать ShowDialog, как наткнулся на статью. Автор заглянул в то, как сделан ShowDialog в WPF. То, что нужно!
В своей статье он предлагает создать собственную имплементацию метода ShowDialog
[DllImport("user32")] internal static extern bool EnableWindow(IntPtr hwnd, bool bEnable); public void ShowModal() { IntPtr handle = (new WindowInteropHelper(Application.Current.MainWindow)).Handle; EnableWindow(handle, false); DispatcherFrame frame = new DispatcherFrame(); this.Closed += delegate { EnableWindow(handle, true); frame.Continue = false; }; Show(); Dispatcher.PushFrame(frame); }
Мне же не требуется блокировка окна, так как всё показывается в одном окне, а так же требуется возвращаемое значение. Убираем немного лишнего, добавляем нужное...
public string GetInput() { var frame = new DispatcherFrame(); ButtonClicked += () => { frame.Continue = false; }; Dispatcher.PushFrame(frame); return Text; }
Dispatcher.PushFrame(frame) предотвращает выход из метода GetInput() до тех пор, пока frame.Continue не станет false. Когда новый фрейм запушен, главный цикл приостанавливается и запускается новый. Этот цикл обрабатывает системные сообщения, в то время как точка выполнения в главном цикле не движется дальше. Когда мы выходим из текущего фрейма (frame.Continue = false), главный цикл продолжает работу с того же места.
Теперь осталось лишь проверить работоспособность.
В MainWindow создадим кнопку и повесим на нее обработчик, который запустит таск, в котором мы и обратимся к вводу с клавиатуры.
Код обработчика:
public RelayCommand ButtonClick => new RelayCommand(() => { Task.Factory.StartNew(() => { // имитация работы Thread.Sleep(1000); // создадим контрол-обработчик ввода var control = new PopupControlModel(); // вызов метода, который останавливает выполнение главного цикла Result = control.GetInput(); // имитация дальнейшей работы Thread.Sleep(2000); }); }); }
Я использовал это решение для ввода пользователем капчи и дополнительного кода при двухфакторной авторизации. Но применений может быть огромное количество.
! В коде примера содержатся нарушения принципа mvvm, и не бейте сильно отсутствует дизайн
Исходный код на github: Proof of concept
Полезные ссылки
Статья "Кастомный ShowDialog"
Скудное описание класса DispatcherFrame с применением машинного перевода
Ожидание завершения через await приведено в этой статье
