От новичка до гуру: Курсы программирования на CyberDuff

Как дождаться асинхронного закрытия всех модальных диалогов?

Фон

В рамках транзакции состояния1 между различными приложениями на клиенте (обмен сообщениями между процессами WCF) важно, чтобы в это время пользователь не изменял состояние приложения. Сегодня эта «блокировка» осуществляется с помощью модальных диалогов в два этапа. Сначала на несколько сотен миллисекунд открывается «невидимый»2 модальный диалог. Это блокирует взаимодействие с пользователем, а также останавливает выполнение метода до закрытия диалогового окна. Если транзакция все еще не завершена после тайм-аута, вместо этого мы показываем видимый модальный диалог прогресса.

Это невидимое модальное диалоговое окно вызывает у нас проблемы, которые, как я думал, можно решить, просто сделав это await Task.Delay(timoutBeforeProgressDialog)3 + блокируя пользовательский ввод с помощью фильтр сообщений. Я думал, что это даст тот же эффект, что и отображение невидимого модального диалога в течение короткого промежутка времени. Однако, похоже, это не так. Если во время обратного вызова в транзакции мы показываем пользователю окно сообщения с просьбой сохранить его изменения, await Task.Delay(timoutBeforeProgressDialog) просто продолжит работу после тайм-аута и выведет диалоговое окно прогресса над окном сообщения, блокируя пользовательский ввод. Этого не произойдет с невидимым модальным окном. Когда невидимый модальный режим закрывается тайм-аутом, он не будет продолжать выполняться до тех пор, пока не будет закрыт модальный диалог обратных вызовов.

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

Я мог бы также добавить, что исторически мы не показывали невидимое модальное окно в этой ситуации, вместо этого мы вызывали Application.DoEvents() в цикле, пока не были готовы продолжить (что дает тот же эффект). Все это было реализовано задолго до того, как async await пришел в .NET.

1 Транзакция включает в себя запрос на переключение контекста и последующую обработку обратных вызовов переключения контекста.

2 Невидимое модальное окно: просто модальное диалоговое окно, которое не отвлекает внимание, не имеет ширины или высоты и открывается за пределами экрана.

3 Существует также токен отмены, чтобы остановить задержку, если транзакция завершится раньше.


Вопрос

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

public partial class Form1 :Form
{
    public Form1() {
        InitializeComponent();
    }

    private async void button_Click(object sender, EventArgs e) {

        this.BeginInvoke(new MethodInvoker(() => {
            MessageBox.Show(this, "A modal dialog", "Dialog", MessageBoxButtons.OK, MessageBoxIcon.Information);
        }));

        await Task.Delay(1000);

        // wait for all modals to be closed / the main message loop is running. 
        await AllModalsClosed();

        Console.WriteLine("This should be logged after user presses OK in the dialog shown above.");
    }

    private async Task AllModalsClosed() {
        var allModalsClosedSource = new TaskCompletionSource<bool>();
        if (!this.TopLevelControl.CanFocus) {
            // Modals are open
            Application.LeaveThreadModal += (object s, EventArgs args) => {
                allModalsClosedSource.SetResult(true);
            };
            await allModalsClosedSource.Task;
        }
    }
}

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

Я также пытался исследовать, есть ли способ отправки с помощью BeginInvoke в основной цикл сообщений, если бы это было возможно, я мог бы переписать AllModalsClosed на:

private async Task AllModalsClosed() {
    var allModalsClosedSource = new TaskCompletionSource<bool>();

    // Made up BeginInvoke variant.
    // Is anything like this possible?
    this.BeginInvokeToMainMessageLoop(new MethodInvoker(() => {
        allModalsClosedSource.SetResult(true);
    }));
    await allModalsClosedSource.Task;
}

В качестве альтернативы, может быть, есть какой-то способ настроить задачу, чтобы она продолжала только основной цикл сообщений после завершения?

11.12.2019

  • Если вы планируете использовать это в какой-то библиотеке, четко сформулируйте это, потому что это меняет правила игры. 12.12.2019

Ответы:


1

Я не уверен, что за этим стоит реальное требование, но то, что вы пытаетесь сделать в своем примере, можно легко сделать следующим образом:

private async void Form1_Load(object sender, EventArgs e)
{
    await Task.Run(() => MessageBox.Show("Hi"));
    MessageBox.Show("All dialog closed!");
}

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

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

Вы можете создать функцию для подсчета количества модальных окон. Я вижу следующие модальные окна:

  • Такие окна, как MessageBox или ColorDialog, которые имеют #32770 как класс окна
  • Те Form, которые вы показали с помощью ShowDialog, которые имеют Modal как истинное.

Чтобы перечислить их, вы можете получить все потоки текущего процесса, а затем для каждого потока, используя EnumThreadWindows получить все окна и использовать GetClassName проверьте, является ли класс #32770.

Затем с помощью Application.OpenForms получить список форм, свойство Modal которых равно true.

delegate bool EnumThreadDelegate(IntPtr hWnd, IntPtr lParam);
[DllImport("user32.dll")]
static extern bool EnumThreadWindows(int dwThreadId, EnumThreadDelegate lpfn, IntPtr lParam);
[DllImport("user32.dll")]
static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount);

static IEnumerable<IntPtr> GetModalWindowsHandles(int processId)
{
    var handles = new List<IntPtr>();
    foreach (ProcessThread thread in Process.GetProcessById(processId).Threads)
        EnumThreadWindows(thread.Id,
            (hWnd, lParam) =>
            {
                var className = new StringBuilder(256);
                GetClassName(hWnd, className, 256);
                if (className.ToString() == "#32770")
                {
                    handles.Add(hWnd);
                }
                return true;
            }, IntPtr.Zero);
    foreach (Form form in Application.OpenForms)
        form.Invoke(new Action(() =>
        {
            if (form.Modal)
                handles.Add(form.Handle);
        }));
    return handles;
}

Пример

Следующий пример открывает несколько модальных окон и ждет, пока они все не закроются:

private async void Form1_Load(object sender, EventArgs e)
{
    Task.Run(() => MessageBox.Show("Hi"));
    Task.Run(() => new ColorDialog().ShowDialog());
    Task.Run(() => new Form().ShowDialog());
    await WaitUntil(() => GetModalWindowsHandles(Process.GetCurrentProcess().Id).Count() == 0);
    MessageBox.Show("All dialog closed!");
}
public async Task WaitUntil(Func<bool> condition, int frequency = 25, int timeout = -1)
{
    var waitTask = Task.Run(async () =>
    {
        while (!condition()) await Task.Delay(frequency);
    });
    if (waitTask != await Task.WhenAny(waitTask,
            Task.Delay(timeout)))
        throw new TimeoutException();
}

WaitUntil взял из это сообщение для использования в этом примере.

12.12.2019
  • Мне также интересно узнать реальное требование, поскольку оно выглядит как проблема XY мне. 12.12.2019
  • Это больше похоже на то, что эффект был после, но намного сложнее, чем я надеялся, по сравнению с тем, как мы решаем это сегодня (см. обновленный раздел «Предыстория»). Я немного смущен тем, что мы должны перебирать все потоки в процессе, чтобы проверить наличие диалогов, я думал, что их может открыть только поток пользовательского интерфейса... но мое понимание .NET, вероятно, отсутствует. Мне может потребоваться некоторое время, чтобы разобраться в этом, но я обязательно отмечу его как правильный, если найду, что это лучший ответ на вопрос. Спасибо. 12.12.2019
  • Обычно приложение Windows Forms имеет один поток пользовательского интерфейса. Но, например, в приведенном выше примере с помощью Task.Run я создал модальные диалоговые окна в разных потоках. И спасибо за отзыв, не беспокойтесь, приложите достаточно усилий, чтобы проверить все решения. 13.12.2019

  • 2

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

    Чтобы открыть/показать диалог, вы должны вызвать этот класс DialogManager вместо того, чтобы делать это напрямую.

    Когда количество открытых диалогов достигает нуля, вы можете создать событие в этом классе, например. «Все диалоги закрыты».

    Затем вы можете подписаться на это событие AllDialogsClosed в своем приложении в другом классе и выполнить свою логику.

    public class DialogManager
    {
      private int _numberOfOpenDialogs;
      public event EventHandler AllDialogsClosed;
    
    
      public ShowDialog(object dialogInstance)
      {
          _numberOfOpenDialogs++;
      }
    
      public CloseDialog(object dialogInstance)
      {
          _numberOfOpenDialogs--;
    
          if (_numberOfOpenDialogs == 0)
          {
              OnAllDialogsClosed();
          }
      }
    
      protected virtual void OnAllDialogsClosed(EventArgs e)
      {
        EventHandler handler = AllDialogsClosed;
        if (handler != null)
        {
            handler(this, e);
        }
      } 
    }
    
    12.12.2019
  • Я думаю, что это может быть хорошим решением для некоторых людей, которые могут найти этот вопрос, но это своего рода то, что я имел в виду под [...], чтобы сделать эту реализацию независимой от фактического диалога [... ], но я понимаю, что это могло быть неясно. Если это работает только в том случае, если другие разработчики не забывают вызывать этот класс каждый раз, когда открывают диалоговое окно, в будущем это будет очень подвержено ошибкам. Это также потребовало бы от меня изменения потенциальных сотен (или тысяч) мест, где мы открываем модальные диалоги. Я надеялся, что есть способ сохранить реализацию в одном классе или функции. 12.12.2019
  • Новые материалы

    5 простых концепций Python, ставших сложными
    #заранее извините 1) Переменные x = 4 y = 5 Переменная в Python — это символическое представление объекта. После присвоения некоторого объекта переменной Python мы приобретаем..

    «Освоение вероятности: изучение совместной, предельной, условной вероятности и теоремы Байеса —…
    Виды вероятности: Совместная вероятность Предельная вероятность Условная вероятность Диаграмма Венна в вероятностях: В “Set Theory” мы создаем диаграмму Венна...

    Основы Spring: Bean-компоненты, контейнер и внедрение зависимостей
    Как лего может помочь нашему пониманию Когда мы начинаем использовать Spring, нам бросают много терминов, и может быть трудно понять, что они все означают. Итак, мы разберем основы и будем..

    Отслеживание состояния с течением времени с дифференцированием снимков
    Время от времени что-то происходит и революционизирует часть моего рабочего процесса разработки. Что-то более забавное вместо типичного утомительного и утомительного процесса разработки. В..

    Я предполагаю, что вы имеете в виду методы обработки категориальных данных.
    Я предполагаю, что вы имеете в виду методы обработки категориальных данных. Пожалуйста, проверьте мой пост Инструментарий специалиста по данным для кодирования категориальных переменных в..

    Игра в прятки с данными
    Игра в прятки с данными Я хотел бы, чтобы вы сделали мне одолжение и ответили на следующие вопросы. Гуглить можно в любое время, здесь никто не забивается. Сколько регионов в Гане? А как..

    «Раскрытие математических рассуждений с помощью Microsoft MathPrompter и моделей больших языков»
    TL;DR: MathPrompter от Microsoft показывает, как использовать математические рассуждения с большими языковыми моделями; 4-этапный процесс для улучшения доверия и рассуждений в математических..