Как заблокировать поток кода, пока не произойдет событие в C #

10

Здесь у нас есть Gridс собой Button. Когда пользователь нажимает кнопку, выполняется метод в классе Utility, который заставляет приложение получать щелчок по Grid. Поток кода должен останавливаться здесь и не продолжаться до тех пор, пока пользователь не нажмет на Grid.

У меня был подобный вопрос, прежде чем здесь:

Подождите, пока пользователь не нажмет C # WPF

В этом вопросе я получил ответ, используя async / await, который работает, но так как я собираюсь использовать его как часть API, я не хочу использовать async / await, так как тогда потребители должны будут пометить свои методы с помощью асинхронность которой я не хочу.

Как мне написать Utility.PickPoint(Grid grid)метод для достижения этой цели?

Я видел это, которое может помочь, но не совсем понял, как это применить здесь, если честно:

Блокировка до завершения события

Считайте это чем-то вроде метода Console.ReadKey () в консольном приложении. Когда мы вызываем этот метод, поток кода останавливается, пока мы не введем какое-либо значение. Отладчик не продолжается, пока мы не введем что-либо. Я хочу точное поведение для метода PickPoint (). Поток кода будет остановлен, пока пользователь не нажмет на Grid.

<Window x:Class="WpfApp1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp1"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="3*"/>
            <RowDefinition Height="1*"/>
        </Grid.RowDefinitions>

        <Grid x:Name="View" Background="Green"/>
        <Button Grid.Row="1" Content="Pick" Click="ButtonBase_OnClick"/>
    </Grid>
</Window>

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
    {
        // do not continue the code flow until the user has clicked on the grid. 
        // so when we debug, the code flow will literally stop here.
        var point = Utility.PickPoint(View);


        MessageBox.Show(point.ToString());
    }
}

public static class Utility
{
    public static Point PickPoint(Grid grid)
    {

    }
}
Вахид
источник
Очевидный способ - это Aync/Awaitкак выполнить операцию А и сохранить эту операцию СОСТОЯНИЕ. Теперь вы хотите, чтобы пользователь щелкнул «Сетка». Поэтому, если пользователь щелкает «Сетка», вы проверяете состояние, если оно истинно, а затем выполняете свою операцию, просто делая то, что вы хотите ??
Рао Хаммас Хуссейн
@RaoHammasHussain Я обновил свой вопрос ссылкой, которая может помочь. Служебный метод будет частью API, который пользователь API будет вызывать всякий раз, когда он хочет попросить конечного пользователя нажать на экран. Считайте, что это окно запроса текста в обычных приложениях Windows или метод Console.Readline (). В этих случаях поток кода останавливается, пока пользователь не введет что-либо. Теперь я хочу точную вещь, но на этот раз пользователь нажимает на экран.
Вахид
AutoResetEventэто не то, что вы хотите?
Рао Хаммас Хуссейн
@RaoHammasHussain Я так думаю, но на самом деле не знаю, как использовать это здесь.
Вахид
Это как будто вы намеренно реализуете ПОДОЖДИТЕ ГОСУДАРСТВО. это действительно требуется? Потому что ты не можешь просто поместить это var point = Utility.PickPoint(Grid grid);в метод Grid Click? сделать какую-то операцию и вернуть ответ?
Рао Хаммас Хуссейн

Ответы:

8

"Как заблокировать поток кода, пока не сработает событие?"

Ваш подход неверен. Управляемый событиями не означает блокирование и ожидание события. Вы никогда не ждете, по крайней мере, вы всегда стараетесь избежать этого. Ожидание - это бесполезная трата ресурсов, блокировка потоков и, возможно, введение риска тупика или потока зомби (в случае, если сигнал освобождения никогда не поднимается).
Должно быть ясно, что блокировка потока для ожидания события является анти-паттерном, поскольку противоречит идее события.

Обычно у вас есть два (современных) варианта: реализовать асинхронный API или API, управляемый событиями. Поскольку вы не хотите реализовывать свой API-интерфейс асинхронным, у вас остается API, управляемый событиями.

Ключ API, управляемого событиями, заключается в том, что вместо того, чтобы заставлять вызывающего абонента синхронно ожидать результата или опросить результат, вы позволяете вызывающему продолжить и отправлять ему уведомление, как только результат будет готов или операция завершена. Между тем, вызывающая сторона может продолжать выполнять другие операции.

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

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

Ваш поток или цель, очевидно, разделены по крайней мере на два шага, чтобы сделать это последовательностью операций:

  1. Выполните операцию 1, когда пользователь нажимает кнопку
  2. Выполните операцию 2 (продолжить / завершить операцию 1), когда пользователь нажимает на Grid

по крайней мере с двумя ограничениями:

  1. Необязательно: последовательность должна быть завершена до того, как клиенту API разрешено ее повторить. Последовательность завершается после завершения операции 2.
  2. Операция 1 всегда выполняется перед операцией 2. Операция 1 запускает последовательность.
  3. Операция 1 должна завершиться, прежде чем клиенту API будет разрешено выполнить операцию 2.

Это требует от двух уведомлений для клиента API, чтобы разрешить неблокирующее взаимодействие:

  1. Операция 1 завершена (или требуется взаимодействие)
  2. Операция 2 (или цель) завершена

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

Реализация / рефакторинг Utility API

Utility.cs

class Utility
{
  public event EventHandler InitializePickPointCompleted;
  public event EventHandler<PickPointCompletedEventArgs> PickPointCompleted;
  private bool IsPickPointInitialized { get; set; }
  private bool IsExecutingSequence { get; set; }

  // The prefix 'Begin' signals the caller or client of the API, 
  // that he also has to end the sequence explicitly
  public void BeginPickPoint(param)
  {
    // Implement constraint 1
    if (this.IsExecutingSequence)
    {
      // Alternatively just return or use Try-do pattern
      throw new InvalidOperationException("BeginPickPoint is already executing. Call EndPickPoint before starting another sequence.");
    }

    // Set the flag that a current sequence is in progress
    this.IsExecutingSequence = true;

    // Execute operation until caller interaction is required.
    // Execute in background thread to allow API caller to proceed with execution.
    Task.Run(() => StartOperationNonBlocking(param));
  }

  public void EndPickPoint(param)
  {
    // Implement constraint 2 and 3
    if (!this.IsPickPointInitialized)
    {
      // Alternatively just return or use Try-do pattern
      throw new InvalidOperationException("BeginPickPoint must have completed execution before calling EndPickPoint.");
    }

    // Execute operation until caller interaction is required.
    // Execute in background thread to allow API caller to proceed with execution.
    Task.Run(() => CompleteOperationNonBlocking(param));
  }

  private void StartOperationNonBlocking(param)
  {
    ... // Do something

    // Flag the completion of the first step of the sequence (to guarantee constraint 2)
    this.IsPickPointInitialized = true;

    // Request caller interaction to kick off EndPickPoint() execution
    OnInitializePickPointCompleted();
  }

  private void CompleteOperationNonBlocking(param)
  {
    // Execute goal and get the result of the completed task
    Point result = ExecuteGoal();

    // Reset API sequence
    this.IsExecutingSequence = false;
    this.IsPickPointInitialized = false;

    // Notify caller that execution has completed and the result is available
    OnPickPointCompleted(result);
  }

  private void OnInitializePickPointCompleted()
  {
    // Set the result of the task
    this.InitializePickPointCompleted?.Invoke(this, EventArgs.Empty);
  }

  private void OnPickPointCompleted(Point result)
  {
    // Set the result of the task
    this.PickPointCompleted?.Invoke(this, new PickPointCompletedEventArgs(result));
  }
}

PickPointCompletedEventArgs.cs

class PickPointCompletedEventArgs : EventArgs
{
  public Point Result { get; }

  public PickPointCompletedEventArgs(Point result)
  {
    this.Result = result;
  }
}

Используйте API

MainWindow.xaml.cs

partial class MainWindow : Window
{
  private Utility Api { get; set; }

  public MainWindow()
  {
    InitializeComponent();

    this.Api = new Utility();
  }

  private void StartPickPoint_OnButtonClick(object sender, RoutedEventArgs e)
  {
    this.Api.InitializePickPointCompleted += RequestUserInput_OnInitializePickPointCompleted;

    // Invoke API and continue to do something until the first step has completed.
    // This is possible because the API will execute the operation on a background thread.
    this.Api.BeginPickPoint();
  }

  private void RequestUserInput_OnInitializePickPointCompleted(object sender, EventArgs e)
  {
    // Cleanup
    this.Api.InitializePickPointCompleted -= RequestUserInput_OnInitializePickPointCompleted;

    // Communicate to the UI user that you are waiting for him to click on the screen
    // e.g. by showing a Popup, dimming the screen or showing a dialog.
    // Once the input is received the input event handler will invoke the API to complete the goal   
    MessageBox.Show("Please click the screen");  
  }

  private void FinishPickPoint_OnGridMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
  {
    this.Api.PickPointCompleted += ShowPoint_OnPickPointCompleted;

    // Invoke API to complete the goal
    // and continue to do something until the last step has completed
    this.Api.EndPickPoint();
  }

  private void ShowPoint_OnPickPointCompleted(object sender, PickPointCompletedEventArgs e)
  {
    // Cleanup
    this.Api.PickPointCompleted -= ShowPoint_OnPickPointCompleted;

    // Get the result from the PickPointCompletedEventArgs instance
    Point point = e.Result;

    // Handle the result
    MessageBox.Show(point.ToString());
  }
}

MainWindow.xaml

<Window>
  <Grid MouseLeftButtonUp="FinishPickPoint_OnGridMouseLeftButtonUp">
    <Button Click="StartPickPoint_OnButtonClick" />
  </Grid>
</Window>

замечания

События, созданные в фоновом потоке, будут выполнять свои обработчики в том же потоке. Доступ к DispatcherObjectкак элемент пользовательского интерфейса от обработчика, который выполняется в фоновом потоке, требуется критическая операция должна быть поставлена в очередь к Dispatcherиспользуя либо Dispatcher.Invokeили , Dispatcher.InvokeAsyncчтобы избежать исключений кросса-нити.
Прочитайте замечания о том, DispatcherObjectчтобы узнать больше об этом явлении, называемом сходством с диспетчером или сродством потока


Некоторые мысли - ответьте на ваши комментарии

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

«Рассмотрим консольное приложение с этими двумя строками кода.

var str = Console.ReadLine(); 
Console.WriteLine(str);

Что происходит, когда вы запускаете приложение в режиме отладки. Он остановится на первой строке кода и заставит вас ввести значение в пользовательском интерфейсе консоли, а затем, после того как вы введете что-то и нажмете Enter, выполнит следующую строку и фактически напечатает то, что вы ввели. Я думал о том же поведении, но в приложении WPF. "

Консольное приложение - это нечто совершенно другое. Концепция потоков немного отличается. Консольные приложения не имеют графического интерфейса. Просто вход / выход / потоки ошибок. Вы не можете сравнить архитектуру консольного приложения с многофункциональным графическим приложением. Это не сработает. Вы действительно должны понять и принять это.

Также не обманывайтесь внешностью . Вы знаете, что происходит внутри Console.ReadLine ? Как это реализовано ? Он блокирует основной поток и параллельно читает ввод? Или это просто опрос?
Вот оригинальная реализация Console.ReadLine:

public virtual String ReadLine() 
{
  StringBuilder sb = new StringBuilder();
  while (true) 
  {
    int ch = Read();
    if (ch == -1) 
      break;
    if (ch == '\r' || ch == '\n') 
    {
      if (ch == '\r' && Peek() == '\n') 
        Read();
      return sb.ToString();
    }
    sb.Append((char)ch);
  }
  if (sb.Length > 0) 
    return sb.ToString();
  return null;
}

Как видите, это простая синхронная операция. Опрашивает пользовательский ввод в «бесконечном» цикле. Нет магического блока и продолжай.

WPF построен вокруг потока рендеринга и потока пользовательского интерфейса. Эти потоки постоянно вращаются, чтобы обмениваться данными с ОС, например, обрабатывать ввод данных пользователем, поддерживая отзывчивость приложения . Вы никогда не захотите приостанавливать / блокировать этот поток, поскольку он не позволит каркасу выполнять важную фоновую работу, например, реагирование на события мыши - вы не хотите, чтобы мышь зависала:

ожидание = блокировка потока = неотзывчивость = плохой UX = раздраженные пользователи / клиенты = проблемы в офисе.

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

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

Тот факт, что вы изо всех сил пытаетесь противостоять модели асинхронного программирования, показывает некоторое отсутствие понимания для меня. Каждый современный разработчик предпочитает асинхронный API, а не синхронный. Ни один серьезный разработчик не хочет использовать awaitключевое слово или объявлять его метод async. Никто. Вы первый раз сталкиваетесь с теми, кто жалуется на асинхронные API и считает их неудобными для использования.

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

Другая точка зрения: когда вы признаете, что ожидание блокирует поток пользовательского интерфейса, создает очень плохой и нежелательный пользовательский интерфейс, поскольку пользовательский интерфейс будет зависать до тех пор, пока не закончится ожидание, теперь, когда вы это понимаете, зачем вам предлагать модель API или плагина, которая побуждает разработчика сделать именно это - реализовать ожидание?
Вы не знаете, что будет делать сторонний плагин и сколько времени займет выполнение процедуры. Это просто плохой дизайн API. Когда ваш API работает в потоке пользовательского интерфейса, то вызывающая сторона вашего API должна иметь возможность делать неблокирующие вызовы к нему.

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

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

Сегодня модели асинхронного программирования применяются везде, на любой платформе, компиляторе, в любой среде, браузере, сервере, настольном компьютере, базе данных - везде. Модель, управляемая событиями, позволяет достичь той же цели, но она менее удобна в использовании (подписка / отмена подписки на события и от них, чтение документов (если есть документы), чтобы узнать о событиях), опираясь на фоновые потоки. Управляемый событиями старомоден и должен использоваться только тогда, когда асинхронные библиотеки недоступны или неприменимы.

В качестве дополнительного примечания: .NET Framwork (.NET Standard) предлагает TaskCompletionSource(среди прочих целей) простой способ преобразования существующего ровного API в асинхронный API.

«Я видел точное поведение Autodesk Revit».

Поведение (то, что вы испытываете или наблюдаете) сильно отличается от того, как этот опыт реализован. Две разные вещи. Ваш Autodesk, скорее всего, использует асинхронные библиотеки или языковые функции или какой-либо другой механизм потоков. И это также связано с контекстом. Когда ваш метод выполняется в фоновом потоке, разработчик может заблокировать этот поток. У него либо есть очень веская причина, либо он сделал неправильный выбор дизайна. Вы совершенно не на том пути;) Блокировка не очень хорошая.
(Является ли исходный код Autodesk открытым исходным кодом? Или как вы знаете, как он реализован?)

Я не хочу тебя обидеть, пожалуйста, поверь мне. Но, пожалуйста, пересмотрите возможность реализации вашего API асинхронно. Только в вашей голове разработчики не любят использовать async / await. Вы явно ошиблись. И забыть об этом аргументе консольного приложения - это нонсенс;)

API, связанный с пользовательским интерфейсом, ДОЛЖЕН использовать async / await всякий раз, когда это возможно. В противном случае вы оставляете всю работу по написанию неблокирующего кода клиенту вашего API. Вы бы заставили меня обернуть каждый вызов вашего API в фоновый поток. Или использовать менее удобную обработку событий. Поверьте мне - каждый разработчик скорее украшает своих участников async, чем занимается обработкой событий. Каждый раз, когда вы используете события, вы можете рисковать потенциальной утечкой памяти - это зависит от некоторых обстоятельств, но риск реален и не редок при небрежном программировании.

Я действительно надеюсь, что вы понимаете, почему блокировка это плохо. Я действительно надеюсь, что вы решите использовать async / await для написания современного асинхронного API. Тем не менее, я показал вам очень распространенный способ ожидания неблокирования, используя события, хотя я призываю вас использовать async / await.

«API позволит программисту иметь доступ к пользовательскому интерфейсу и т. Д. Теперь предположим, что программист хочет разработать надстройку, чтобы при нажатии кнопки конечному пользователю предлагалось выбрать точку в пользовательском интерфейсе».

Если вы не хотите, чтобы плагин имел прямой доступ к элементам пользовательского интерфейса, вы должны предоставить интерфейс для делегирования событий или предоставления внутренних компонентов через абстрагированные объекты.
Интерфейс API будет подписываться на события пользовательского интерфейса от имени надстройки, а затем делегирует это событие, предоставляя соответствующее событие «оболочки» клиенту API. Ваш API должен предлагать несколько ловушек, с помощью которых надстройка может подключаться для доступа к определенным компонентам приложения. API плагинов действует как адаптер или фасад, предоставляя внешним доступам к внутренним объектам.
Чтобы позволить степень изоляции.

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

BionicCode
источник
Привет, спасибо, что подошли к вопросу с другой точки зрения. Извините, если вопрос был немного расплывчат в деталях. Рассмотрим консольное приложение с этими двумя строками кода. var str = Console.ReadLine(); Console.WriteLine(str);Что происходит, когда вы запускаете приложение в режиме отладки. Он остановится на первой строке кода и заставит вас ввести значение в пользовательском интерфейсе консоли, а затем, после того как вы введете что-то и нажмете Enter, выполнит следующую строку и фактически напечатает то, что вы ввели. Я думал о том же поведении, но в приложении WPF.
Вахид
В разрабатываемом САПР-приложении пользователи должны расширять его с помощью надстроек / плагинов. API позволит программисту иметь доступ к пользовательскому интерфейсу и т. Д. Теперь предположим, что программист хочет разработать надстройку, чтобы при нажатии кнопки конечному пользователю предлагалось выбрать точку в пользовательском интерфейсе, а затем код будет выполнять другие действия. классная штука с данной точкой. Может быть, они попросят выбрать другую точку, нарисовать линию и т. Д.
Вахид
Я видел точное поведение в Autodesk Revit.
Вахид
1
Мне есть что сказать по вашему требованию. Пожалуйста, прочитайте мой обновленный ответ. Я разместил ответ там, потому что он стал как-то длиннее. Я признаю, что вы действительно вызвали меня. Пожалуйста, при чтении помните, что я не хочу вас обидеть.
BionicCode
Спасибо за ваш обновленный ответ. Конечно, я не обижаюсь. Наоборот, я очень благодарен за время и усилия, которые вы приложили для этого.
Вахид
5

Лично я думаю, что все это слишком усложняют, но, возможно, я не до конца понимаю причину, по которой это нужно делать определенным образом, но кажется, что здесь можно использовать простую проверку bool.

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

<grid MouseLeftButtonUp="Grid_MouseLeftButtonUp" IsHitTestVisible="True" Background="Transparent">

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

Пример:

bool awaitingClick = false;


private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
   awaitingClick=true;
}

private void Grid_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{     
     //Stop here if the program shouldn't do anything when grid is clicked
     if (!awaitingClick) { return; } 

     //Run event
     var point = Utility.PickPoint(View);
     MessageBox.Show(point.ToString());

     awaitingClick=false;//Reset
}
Tronald
источник
Привет Трональд, я думаю, вы неправильно поняли вопрос. Мне нужно, чтобы код остановился на Utility.PickPoint (View) и продолжился только после того, как пользователь нажал на Grid.
Вахид
О да, я совершенно не понял. Мои извинения, я не осознавал, что вам нужно все, чтобы на самом деле остановиться. Я не думаю, что это возможно без многопоточности, поскольку весь пользовательский интерфейс будет заблокирован.
Трональд
Я все еще не уверен, если это невозможно. Это определенно возможно с async / await, который не является многопоточным решением. Но мне нужна альтернатива асинхронному / ожидающему решению.
Вахид
1
Конечно, но вы упомянули, что вы не можете использовать async / await. Кажется, что вам нужно использовать диспетчер и поток, который отделен от основного потока (который выполняется в пользовательском интерфейсе). Я надеюсь, что вы найдете другой путь, хотя я сам интересуюсь
Трональд
2

Я попробовал несколько вещей, но я не могу сделать это без async/await. Потому что, если мы не используем его, это вызывает, DeadLockили пользовательский интерфейс заблокирован, и тогда мы можем принимать Grid_Clickданные.

private async void ToolBtn_OnClick(object sender, RoutedEventArgs e)
{
    var senderBtn = sender as Button;
    senderBtn.IsEnabled = false;

    var response = await Utility.PickPoint(myGrid);
    MessageBox.Show(response.ToString());
    senderBtn.IsEnabled = true;
}  

public static class Utility
{
    private static TaskCompletionSource<bool> tcs;
    private static Point _point = new Point();

    public static async Task<Point> PickPoint(Grid grid)
    {
        tcs = new TaskCompletionSource<bool>();
        _point = new Point();

        grid.MouseLeftButtonUp += GridOnMouseLeftButtonUp;


        await tcs.Task;

        grid.MouseLeftButtonUp -= GridOnMouseLeftButtonUp;
        return _point;
    }


    private static void GridOnMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
    {

        // do something here ....
        _point = new Point { X = 23, Y = 34 };
        // do something here ....

        tcs.SetResult(true); // as soon its set it will go back

    }
}
Рао Хаммас Хуссейн
источник
@ спасибо, это тот же ответ, который я получил на другой мой вопрос, который использует async / await.
Вахид
о да ! я заметил это сейчас, но я думаю, что это единственный способ, который я нашел, работает
Рао Хаммас Хуссейн
2

Вы можете заблокировать асинхронно, используя SemaphoreSlim:

public partial class MainWindow : Window, IDisposable
{
    private readonly SemaphoreSlim _semaphoreSlim = new SemaphoreSlim(0, 1);

    public MainWindow()
    {
        InitializeComponent();
    }

    private async void ButtonBase_OnClick(object sender, RoutedEventArgs e)
    {
        var point = Utility.PickPoint(View);

        // do not continue the code flow until the user has clicked on the grid. 
        // so when we debug, the code flow will literally stop here.
        await _semaphoreSlim.WaitAsync();

        MessageBox.Show(point.ToString());
    }

    private void View_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
        //click on grid detected....
        _semaphoreSlim.Release();
    }

    protected override void OnClosed(EventArgs e)
    {
        base.OnClosed(e);
        Dispose();
    }

    public void Dispose() => _semaphoreSlim.Dispose();
}

Вы не можете и не хотите блокировать поток диспетчера синхронно, потому что тогда он никогда не сможет обрабатывать щелчок Grid, т. Е. Он не может одновременно блокироваться и обрабатывать события.

MM8
источник
Спасибо за альтернативный ответ. Интересно, как это делается например в Console.Readline ()? Когда вы достигаете этого метода в отладчике, он волшебным образом останавливается, если мы не введем что-нибудь? Это принципиально отличается в консольных приложениях? Разве мы не можем иметь такое же поведение в приложении WinForms / WPF? Я видел это в Autodesk Revit API, там есть метод PickPoint (), который заставляет вас выбирать точку на экране, и я не видел ни одного асинхронного / ожидающего использования! Можно ли хотя бы спрятать ключевое слово await и как-то вызвать его из метода синхронизации?
Вахид
@Vahid: Console.Readline блокирует , т.е. не возвращает, пока не будет прочитана строка. Ваш PickPointметод не делает. Возвращается немедленно. Это может потенциально заблокировать, но тогда вы не сможете обрабатывать ввод пользовательского интерфейса в то же время, как я написал в своем ответе. Другими словами, вам придется обрабатывать щелчок внутри метода, чтобы получить то же поведение.
мм8
Блоки Console.Realine (), но в то же время разрешены события KeyPress. Разве мы не можем вести себя точно так же? Блокировка с помощью PickPoint () и только разрешение MouseEvents? Я не могу понять, почему это возможно в консоли, но не в приложении на основе пользовательского интерфейса.
Вахид
Затем вам нужно настроить отдельный диспетчер, PickPointкоторый обрабатывает события мыши. Я не вижу, куда вы идете с этим?
мм8
1
@Vahind: сделать код асинхронным и позволить пользователю ждать метода? Это API, который я бы ожидал как разработчик пользовательского интерфейса. Вызов метода блокировки в приложении пользовательского интерфейса не имеет никакого смысла.
мм8
2

Технически это возможно с AutoResetEventи без async/await, но есть существенный недостаток:

public static Point PickPoint(Grid grid)
{
    var pointPicked = new AutoResetEvent(false);
    grid.MouseLeftButtonUp += (s, e) => 
    {
        // do whatever after the grid is clicked

        // signal the end of waiting
        pointPicked.Set();
    };

    // code flow will stop here and wait until the grid is clicked
    pointPicked.WaitOne();
    // return something...
}

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

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

private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
    // here I used ThreadPool, but you may use other means to run on another thread
    ThreadPool.QueueUserWorkItem(new WaitCallback(Capture));
}

private void Capture(object state)
{
    // do not continue the code flow until the user has clicked on the grid. 
    // so when we debug, the code flow will literally stop here.
    var point = Utility.PickPoint(View);


    MessageBox.Show(point.ToString());
}

Это может вызвать больше проблем у пользователей вашего API, за исключением того, что они использовали для управления своими собственными потоками. Вот почему async/awaitбыл изобретен.

Кен Хунг
источник
Спасибо, Кен, возможно ли, что надстройка запускается из другого потока, а затем его события не блокируют основной поток пользовательского интерфейса?
Вахид
@Vahid Да и нет. Да, вы можете вызвать метод блокировки в другом потоке и перенести его в другой метод. Тем не менее, метод оболочки по-прежнему необходимо вызывать в другом потоке, отличном от потока пользовательского интерфейса, чтобы избежать блокировки пользовательского интерфейса. Потому что оболочка будет блокировать вызывающий поток, если он синхронный . Хотя внутренне оболочка блокирует другой поток, ей все равно нужно дождаться результата и заблокировать вызывающий поток. Если вызывающая сторона вызывает метод-оболочку в потоке пользовательского интерфейса, пользовательский интерфейс блокируется.
Кен Хунг
0

Я думаю, что проблема заключается в самом дизайне. Если ваш API работает с определенным элементом, его следует использовать в обработчике событий этого самого элемента, а не с другим элементом.

Например, здесь мы хотим получить позицию события click в Grid, API нужно использовать в обработчике событий, связанном с событием в элементе Grid, а не в элементе button.

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

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

jsami
источник
0

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

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
    {
        Utility.PickPoint(View, (x) => MessageBox.Show(x.ToString()));
    }
}

public static class Utility
{
    private static Action<Point> work;

    public static void PickPoint(Grid grid, Action<Point> work)
    {
        if (Utility.work == null)
        {
            grid.PreviewMouseLeftButtonUp += Grid_PreviewMouseLeftButtonUp;
            Utility.work = work;
        }
    }

    private static void Grid_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
    {
        var grid = (Grid)sender;
        work.Invoke(e.GetPosition(grid));
        grid.PreviewMouseLeftButtonUp -= Grid_PreviewMouseLeftButtonUp;
        Utility.work = null;
    }
}   

Но если вы хотите заблокировать поток пользовательского интерфейса или «поток кода», ответ будет, что это невозможно. Потому что, если поток пользовательского интерфейса был заблокирован, дальнейший ввод не может быть получен.
Поскольку вы как бы упоминали о консольном приложении, я просто приведу несколько простых объяснений.
Когда вы запускаете консольное приложение или вызываете AllocConsoleпроцесс, который не подключен к какой-либо консоли (окну), будет запущен conhost.exe, который может предоставить консоль (окно), и консольное приложение или процесс вызывающего абонента будет присоединен к консоли ( окно).
Таким образом, любой код, который вы пишете, который может блокировать поток вызывающей стороны, например Console.ReadKey, не будет блокировать поток пользовательского интерфейса консольного окна, любой причина, по которой консольное приложение ожидает вашего ввода, но все еще может реагировать на другой ввод, такой как щелчок мышью.

Alex.Wei
источник