Як заблокувати потік коду, поки подія не буде запущена в C #


11

Тут ми маємо Gridс Button. Коли користувач натискає кнопку, виконується метод класу Utility, який змушує програму отримувати клацання на Grid. Потік коду повинен зупинитися тут і не тривати, поки користувач не натисне на Grid.

У мене раніше було подібне питання:

Зачекайте, поки користувач натисне C # WPF

У цьому питанні я отримав відповідь, використовуючи функцію async / await, яка працює, але оскільки я збираюся використовувати її як частину API, я не хочу використовувати async / wait, оскільки споживачі повинні будуть позначати свої методи за допомогою асинхронізація, яку я не хочу.

Як написати Utility.PickPoint(Grid grid)метод для досягнення цієї мети?

Я бачив це, що може допомогти, але не до кінця зрозумів це, щоб застосувати тут, щоб чесно:

Блокує, поки подія не завершиться

Розглянемо це як щось на зразок методу Console.ReadKey () в додатку Console. Коли ми називаємо цей метод, потік коду припиняється, поки ми не введемо якесь значення. Налагоджувач не продовжується, поки ми щось не введемо. Я хочу точну поведінку методу PickPoint (). Потік коду припиняється, поки користувач не натисне на Сітку.

<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те, як щодо виконання операції A та збереження цієї операції ДЕРЖАВНО зараз ви хочете, щоб користувач повинен натиснути Grid .. тому якщо користувач натискає Grid, ви перевіряєте стан, якщо це правда, то виконайте свою операцію інше, просто робіть все, що ви хочете ??
Рао Хаммас Хуссей

@RaoHammasHussain Я оновив своє запитання за посиланням, яке може допомогти. Метод утиліти буде частиною API, який користувач API зателефонує, коли хоче запитати кінцевого користувача, щоб натиснути на екран. Розглянемо це як вікно підказок для тексту у звичайних програмах Windows або метод Console.Readline (). У цих випадках потік коду припиняється, поки користувач щось не вводить. Тепер я хочу точно, але цього разу користувач натискає на екран.
Вахід

AutoResetEventце не те, що ти хочеш?
Рао Хаммас Хуссей

@RaoHammasHussain Я так думаю, але насправді не знаю, як це тут використовувати.
Вахід

Це все одно, що ви навмисно впроваджуєте ЗАЧЕКАЙТЕ ДЕРЖАВУ. це справді потрібно? Тому що ти не можеш просто поставити це var point = Utility.PickPoint(Grid grid);в метод Grid Click? зробити якусь операцію і повернути відповідь?
Рао Хаммас Хуссей

Відповіді:


9

"Як заблокувати потік коду, поки подія не буде запущена?"

Ваш підхід невірний. Події, що керуються подіями, не означають блокування та очікування події. Ви ніколи не чекаєте, принаймні, завжди намагаєтесь цього уникнути. Очікування - це витрачання ресурсів, блокування потоків і, можливо, введення ризику виникнення тупикового або зомбі потоку (якщо сигнал звільнення ніколи не підвищується).
Повинно бути зрозуміло, що блокування потоку для очікування події є антидіаграмою, оскільки суперечить ідеї події.

Зазвичай у вас є два (сучасні) варіанти: реалізувати асинхронний 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 реалізувати цю поведінку та обмеження, викривши два публічні методи та дві публічні події.

Оскільки ця реалізація дозволяє лише один (без одночасного) виклику в API, рекомендується також розкрити IsBusyвластивість для вказівки запущеної послідовності. Це дозволяє опитувати поточний стан перед початком нової послідовності, хоча рекомендується дочекатися завершення події для виконання наступних викликів.

API впровадження / рефактор Utility

Utility.cs

class Utility
{
  public event EventHandler InitializePickPointCompleted;
  public event EventHandler<PickPointCompletedEventArgs> PickPointCompleted;
  public bool IsBusy { get; set; }
  private bool IsPickPointInitialized { 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.IsBusy)
    {
      // 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.IsBusy = 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 (allow next client invocation)
    this.IsBusy = 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 : AsyncCompletedEventArgs 
{
  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подібного елемента UI від обробника, який виконується на фоновому потоці, вимагає, щоб критична операція була допущена до Dispatcherвикористання Dispatcher.Invokeабо Dispatcher.InvokeAsyncуникати винятків перехресних потоків.
Прочитайте зауваження, DispatcherObjectщоб дізнатися більше про це явище, яке називається спорідненістю диспетчера або спорідненістю потоку.
Для зручного використання API я пропоную перенести всі події в початковий контекст абонента або захоплюючи, і використовуючи абонент, SynchronizationContextабо використовуючи AsyncOperation(або AsyncOperationManager).

Наведений вище приклад можна легко покращити, надавши скасування (рекомендується), наприклад, шляхом викриття Cancel()методу, наприклад, PickPointCancel()та звітування про хід (бажано, використовуючи Progress<T>).


Деякі думки - відповідь на ваші коментарі

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

"Розгляньте додаток Console з цими двома рядками коду в ньому.

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

Що відбувається при виконанні програми в режимі налагодження. Він зупиниться на першому рядку коду і змусить вас ввести значення в інтерфейсі консолі, а потім після того, як ви щось введете і натисніть Enter, він виконає наступний рядок і фактично надрукує те, що ви ввели. Я думав про таку саму поведінку, але у застосуванні WPF ".

Консольний додаток - це щось зовсім інше. Концепція різьблення дещо інша. У консольних програмах немає графічного інтерфейсу. Просто потоки введення / виводу / помилок. Ви не можете порівняти архітектуру консольного додатка з багатою програмою GUI. Це не спрацює. Ви дійсно повинні це зрозуміти і прийняти.

Також не обманюйте погляд . Чи знаєте ви, що відбувається всередині 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 / очікувати. Ви, очевидно, неправильно склалися. І забудьте про той аргумент програми консолі - це нісенітниця;)

API, пов'язаний з інтерфейсом, ОБОВ'ЯЗКОВО використовувати async / очікувати, коли це можливо. В іншому випадку ви залишаєте всю роботу над написанням незаблокувального коду клієнту свого API. Ви змусите мене обернути кожен виклик вашого API у фонову нитку. Або використовувати менш зручне поводження з подіями. Повірте - кожен розробник швидше прикрашає своїх членів async, ніж займається подіями. Кожен раз, коли ви використовуєте події, ви можете ризикувати потенційним витоком пам'яті - залежить від деяких обставин, але ризик є реальним і не рідким, коли програмування недбале.

Я дуже сподіваюся, що ви зрозуміли, чому блокування погано. Я дуже сподіваюся, що ви вирішите використовувати async / чекаєте, щоб написати сучасний асинхронний API. Тим не менш, я показав вам дуже поширений спосіб зачекати неблокування, використовуючи події, хоча я закликаю вас використовувати async / wait.

"API дозволить програмісту отримати доступ до інтерфейсу користувача і т.д.

Якщо ви не хочете дозволити плагіну мати прямий доступ до елементів інтерфейсу, вам слід надати інтерфейс для делегування подій або викриття внутрішніх компонентів через абстраговані об'єкти.
API внутрішньо підписується на події інтерфейсу від імені надбудови, а потім делегує подію, виставляючи відповідну подію "обгортки" клієнту API. Ваш API повинен запропонувати кілька гачків, на яких надбудова може підключитися, щоб отримати доступ до конкретних компонентів програми. API плагіна діє як адаптер або фасад, щоб надати зовнішнім користувачам доступ до внутрішніх приміщень.
Щоб дозволити ступінь ізоляції.

Погляньте, як Visual Studio управляє плагінами чи дозволяє нам їх реалізувати. Притворіться, що ви хочете написати плагін для Visual Studio і провести кілька досліджень, як це зробити. Ви зрозумієте, що Visual Studio розкриває внутрішню програму через інтерфейс або API. EG ви можете маніпулювати редактором коду або отримувати інформацію про вміст редактора без реального доступу до нього.


Привіт, дякую, що підходили до питання з іншої точки зору. Вибачте, якщо питання було трохи розпливчастим щодо деталей. Розглянемо додаток Console з цими двома рядками коду в ньому. var str = Console.ReadLine(); Console.WriteLine(str);Що відбувається при виконанні програми в режимі налагодження. Він зупиниться на першому рядку коду і змусить вас ввести значення в інтерфейсі консолі, а потім після того, як ви щось введете і натисніть Enter, він виконає наступний рядок і фактично надрукує те, що ви ввели. Я думав про абсолютно таку саму поведінку, але у застосуванні WPF.
Вахід

У програмі CAD, яку я розробляю, користувачі повинні мати можливість розширити її за допомогою добавок / плагінів. API дозволить програмісту отримати доступ до інтерфейсу користувача і т. Д. Тепер припустимо, що програміст хоче розробити додаток, що після натискання кнопки кінцевому користувачеві пропонується вибрати точку в інтерфейсі, і тоді код зробить інше круті речі із заданим пунктом. Можливо, вони попросять вибрати інший пункт і провести лінію тощо.
Вахід

Я бачив точну поведінку в Autodesk Revit.
Вахід

1
Я маю що сказати про вашу вимогу. Будь ласка, прочитайте мою оновлену відповідь. Я опублікував відповідь там, тому що це якось довше. Я визнаю, що ти мене справді спровокував. Будь ласка, читаючи, майте на увазі, що я не хочу вас ображати.
BionicCode

Дякую за оновлену відповідь. Звичайно, я не ображаюся. Навпаки, я дуже вдячний за час і зусилля, які ви доклали до цього.
Вахід

5

Я особисто думаю, що це надто складно для всіх, але, можливо, я не до кінця розумію причину, чому це потрібно зробити певним чином, але, схоже, тут можна використовувати просту перевірку булів.

Перш за все, зробіть вашу сітку тестувальною, встановивши 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
}

Привіт, Трональде, я думаю, що ти зрозумів це питання. Мені потрібно, щоб код зупинився на Utility.PickPoint (Перегляд) і продовжувався лише після того, як користувач натиснув Grid.
Вахід

О так, я абсолютно не зрозумів. Вибачте, я не зрозумів, що вам потрібно все, щоб насправді зупинити. Я не думаю, що це неможливо без багатопотокової передачі, оскільки весь інтерфейс буде заблокований.
Трональд

Я досі не впевнений, чи це неможливо. Це, безумовно, можливо за допомогою асинхронізації / очікування, що не є багатопотоковим рішенням. Але те, що мені потрібно, - це альтернатива рішенню async / очікувати.
Вахід

1
Звичайно, але ви згадали, що не можете використовувати async / wait. Здається, вам потрібно скористатися диспетчером і потоком, який відокремлений від основного потоку (який виконується в інтерфейсі). Сподіваюся, ви знайдете інший спосіб, хоча як я сам зацікавлений
Трональд

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

    }
}

@Thanks, це та сама відповідь, яку я отримав на своє інше питання, яке використовує async / wait.
Вахід

о, так ! я помітив це зараз, але я здогадуюсь, це єдиний спосіб, який я знайшов, що це працює
Рао Хаммас Хуссейн

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, тобто він не може бути заблокований і обробляти події одночасно.


Дякую за альтернативну відповідь. Цікаво, як це робиться, наприклад, у Console.Readline ()? Коли ви досягнете цього методу в налагоджувачі, він магічно зупиняється на ньому, якщо ми щось не вводимо? Це кардинально відрізняється в додатках консолей? Чи не можемо ми мати таку саму поведінку у програмі WinForms / WPF? Я бачив це в API Autodesk Revit, там є метод PickPoint (), який змушує вас вибрати точку на екрані, і я не бачив жодного використовуваного асинхроніка / очікування! Чи можливо приховати ключове слово, що очікує, і якось викликати його із методу синхронізації?
Вахід

@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 Так і ні. Так, ви можете викликати метод блокування в інший потік і загортати його в інший метод. Однак метод обгортки все-таки потрібно викликати в іншій потоці, крім потоку UI, щоб уникнути блокування UI. Тому що обгортка блокує викликову нитку, якщо вона синхронна . Хоча внутрішньо обгортка блокує інший потік, він все одно повинен дочекатися результату і блокує виклик потоку. Якщо абонент викликає метод обгортки в потоці користувальницького інтерфейсу, інтерфейс користувача блокується.
Кен Хун

0

Думаю, питання в самому дизайні. Якщо ваш API працює на конкретному елементі, його слід використовувати в обробнику подій цього самого елемента, а не на іншому елементі.

Наприклад, тут ми хочемо отримати позицію події клацання на Grid, API потрібно використовувати в обробнику подій, пов'язаному з подією на елементі Grid, а не на елементі кнопки.

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

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


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, не блокує нитку інтерфейсу вікна консолі, будь-яка причина, коли програма консолі чекає на ваш вхід, але все ж може реагувати на інший вхід, наприклад, клацання миші.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.