Статьи

AlertDialogs, который блокирует выполнение и ожидает ввода на Android (MonoDroid / Xamarin.Android)

Этот пост в блоге посвящен следующим вопросам:

1. Почему иногда требуется блокировка интерфейса и ожидание ввода пользователя?

2. Как создать модал оповещения, который ожидает ввода пользователя на Android с помощью MonoDroid (Xamarin.Android)

проблема

У меня есть бизнес-логика (контроллеры), которые предполагают, что показ окна подтверждения (alertDialog) является блокирующим вызовом пользовательского интерфейса. Это не нужно менять, пока мне нужно выполнить требование и показать экраны с полем подтверждения.

Детали проблемы

Я видел все аргументы ( здесь и здесь ) и во многих других местах о том, как плохо блокировать пользовательский интерфейс, чтобы ждать обратной связи, и я не могу согласиться больше.

Тем не менее, быть хорошим или плохим — это одно, а наличие требования для конкретной функции (блокировка диалогового окна предупреждения пользовательского интерфейса) — это другое.

Я разработал приложение для Android с использованием MonoDroid (Xamarin.Android), и оно использует шаблон проектирования MVC. Все экраны спроектированы как фиктивные фрагменты (классы), которые связаны с контроллерами.

Контроллеры являются основной частью бизнеса для этого клиента. Эти контроллеры были разработаны для Windows, но у них есть только Business Logic, для меня требовалось создать аналогичное приложение для Android (аналогичное Windows), которое будет использовать MVC и использовать эти контроллеры без изменений (или минимально возможного) ,

Это понятно, учитывая тот факт, что эти контроллеры (включая алгоритмы, библиотеки и т. Д.) Были протестированы, использованы и доверены.

Я разработал приложение достаточно хорошо, без необходимости менять какой-либо контроллер, и все работало хорошо, пока у меня не было подтверждения.

Стиль Windows для MessageBox или Confirm Box является синхронным, что означает, что он блокирует пользовательский интерфейс и ждет, пока пользователь нажмет Да / Нет. Это было предположение, которое проникло в эти контроллеры, и мне нужно найти способ выполнить это.

Даже когда я портировал приложение на iOS, у меня не было никаких проблем с этим, поскольку iOS использует механизм блокировки пользовательского интерфейса для отображения диалоговых окон Alert / Confirm (Modal).

Тем не менее, Android использует Asynch подход, чтобы справиться со всем, что связано с пользовательским интерфейсом. Это хорошая новость с точки зрения пользовательского опыта, но для меня это ужасная новость, что я пообещал сделать это приложение и максимально приблизить его к Windows и iOS «без смены контроллеров».

Я начал искать варианты, нашел вопросы и ответы, упомянутые выше, на SO и других сайтах, и все они говорят об одном и том же, если вам нужно передать обратный вызов в alertDialog. Это не вариант для меня, потому что контроллеры предполагают, что у нас есть блокирующий вызов пользовательского интерфейса, когда нам требуется подтверждение от пользователя.

Чтобы продемонстрировать это, давайте посмотрим на обработчик событий Logout Button.

// logout call on Windows/iOS
public void Logout()
{
    var args = new ConfirmEventArgs("Are you sure you want to logout?", "Logout?");
    OnConfirmUser(args);
    if (!args.Cancel)
    {
        DataManager.IsUserLoggingOff = true;
        OnApplyWorkflow(new StimulusEventArgs(DataObject as IWorkflowDomain, Constants.Stimuli.Logout));
    }
}

Я нашел (http://stackoverflow.com/questions/2028697/dialogs-alertdialogs-how-to-block-execution- while-dialog-is-up-net-style#10358260) этот ответ на SO, который некоторые люди говорят он работает на Android с использованием Java, но на Xamarin.Android это не сработало. Причина заключалась в том, что этот подход предполагал, что мы отправляем сообщения на более низком уровне между потоками, а затем выкидываем исключение из одного и затем перехватываем это исключение в другом потоке.

Это не сработало на Xamarin.Android, и когда его попросили о помощи у команды Xamarin, все, что они сделали, это показали мне пример кода, где вам нужно предоставить обратный вызов для MessageBox, как если бы я его раньше не видел.

Решение

После долгих раздумий я пришел к своему подходу, который в основном создает поток для подтверждения и ПОДОЖДИТЕ для ввода пользователя.

Причина, по которой нам нужен новый поток, состоит в том, чтобы избежать блокировки основного потока пользовательского интерфейса, пока мы ожидаем ввода пользователя.

Конечный результат был примерно таким:

// In the BaseController, I added this method, which checks for platform and creats a new thread on Android only
// (since the controllers code is shared cross-platforms)
protected void RunConfirmAction(Action runnableAction)
{
    if (runnableAction != null)
    {
        if (Core.Platform.IsAndroid)
        {
            var confirmThread = new Thread(() => runnableAction());
            confirmThread.Start();
        }
        else
        {
            runnableAction();
        }
    }
}

// The call to the logout method has now changed like this:
RunConfirmAction(Logout);

// the implemtation of the MessageBox waiting is like this:
public DialogResult MessageBoxShow(string message, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton)
{
    if (_CurrentContext != null && _CurrentContext.Screen != null && MainForm.MainActivity != null)
    {
        Action<bool> callback = OnConfirmCallBack;
        _IsCurrentlyInConfirmProcess = true;
        Action messageBoxDelegate = () => MessageBox.Show(((Activity)MainForm.MainActivity), callback, message, caption, buttons);
        RunOnMainUiThread(messageBoxDelegate);
        while (_IsCurrentlyInConfirmProcess)
        {
            Thread.Sleep(1000);
        }              
    }
    else
    {
        LogHandler.LogError("Trying to display a Message box with no activity in the CurrentContext. Message was: " + message);
    }
    return _ConfirmBoxResult ? DialogResult.OK : DialogResult.No;

}

private void OnConfirmCallBack(bool confirmResult)
{
    _ConfirmBoxResult = confirmResult;
    _IsCurrentlyInConfirmProcess = false;
}

private bool _ConfirmBoxResult = false;
private bool _IsCurrentlyInConfirmProcess = false;

Код для класса MessageBox можно найти здесь, я создал это давно, и он прекрасно работает для проектов с зелеными полями, где вы можете отправлять обратные вызовы в поле подтверждения.

И все, после всего этого, я получил что-то, удовлетворяющее требованиям, и мой клиент был очень рад, что им не нужно менять свои контроллеры (что означает меньше ошибок, меньше тестирования, более быструю доставку).

Таким образом, я надеюсь, что у меня есть хорошие основания для того, почему иногда необходимо иметь блокирующий пользовательский интерфейс alertDialog на Android и как это можно сделать.

Если у вас есть какие-либо вопросы, или вы думаете, что это можно сделать лучше, лучше или проще, я хотел бы услышать от вас.