Постановка задачи
В рамках разработки одного приложения потребовалось реализовать такую схему:
- Асинхронный метод запрашивает данные
- Пользователь вводит данные с клавиатуры
- Метод получает результат ввода как результат выполнения функции и продолжает с того же места
Дополнительные требование: Не создавать дополнительных окон.
Казалось бы, просто? Как оказалось, действительно просто. Но обо всём по порядку.
Решение
Первая попытка сделать это в лоб и без поиска в интернете привела к блокировке основного потока, и, следовательно, ни к чему хорошему. И я уже собирался использовать 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 приведено в этой статье