如何在C#中阻塞代码流直到事件被触发

11

我有一个在 WPF 应用程序中的带有按钮的网格。当用户点击按钮时,将执行实用类中的方法,该方法会强制应用程序接收对网格的单击。代码流必须在此处停止并且不继续,直到用户单击了网格。

我以前有过类似的问题,并得到了使用 async/await 的答案,但由于我正在使用此方法作为 API 的一部分,因此我不想使用 async/await,因为这需要 API 的使用者将他们的方法标记为 async,这是我不想要的。

Wait till user click C# WPF

如何编写 Utility.PickPoint(Grid grid) 方法以实现此目标而不使用 async/await?我看到这个答案可能有帮助,但我没有完全理解如何将其应用于我的情况。

Blocking until an event completes

将其视为控制台应用程序中的 Console.ReadKey() 方法。当我们调用此方法时,代码流会停止,直到我们输入一些值。除非我们输入东西,否则调试器不会继续。我想要 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并保存该操作的状态,现在您希望用户单击网格,那么如果用户单击网格,则检查状态是否为真,然后执行您的操作,否则只需执行您想要的任何操作即可。 - Rao Arman
@RaoHammasHussain 我更新了我的问题,并提供了一个链接,可能会有所帮助。这个实用方法将成为API的一部分,API的用户将在每次想要请求最终用户点击屏幕时调用它。可以将其视为普通Windows应用程序中文本提示窗口或Console.ReadLine()方法。在这些情况下,代码流程会停止,直到用户输入内容。现在我想要完全相同的东西,但这次是用户点击屏幕。 - Vahid
@RaoHammasHussain 我也这么认为,但真的不知道如何在这里使用它。 - Vahid
就好像你有意地实现了等待状态。这真的必要吗?因为你不能只把 var point = Utility.PickPoint(Grid grid); 放在 Grid Click 方法中吗?执行一些操作并返回响应? - Rao Arman
你似乎正在做的是,首先用户点击某个按钮,等待状态就会开启,现在如果用户点击了网格,则您将执行某些操作,然后您希望回到等待状态位置?我知道这可能是您的要求,但如果用户不点击网格呢?那么呢? - Rao Arman
显示剩余3条评论
7个回答

11
如何在事件触发前阻塞代码流? 你的方法是错误的。事件驱动并不意味着阻塞和等待事件。你永远不要等待,至少你总是尽力避免等待。等待浪费资源,阻塞线程,可能会引入死锁或僵尸线程的风险(如果释放信号从未被提出)。很明显,阻塞线程以等待事件是一种反模式,因为它与事件的想法相矛盾。 通常有两个(现代)选项:实现异步API或事件驱动API。由于您不想将API实现异步,因此只能使用事件驱动API。 事件驱动API的关键是,不是强制调用者同步等待结果或轮询结果,而是让调用者继续,并在结果准备好或操作完成时向其发送通知。同时,调用者可以继续执行其他操作。 从线程角度看问题,事件驱动API允许调用线程(例如UI线程,执行按钮的事件处理程序)自由继续处理其他UI相关操作,例如渲染UI元素或处理用户输入,例如鼠标移动和按键按下。事件驱动API具有与异步API相同的效果或目标,尽管它要不方便得多。 由于您没有提供有关您实际要执行的操作,Utility.PickPoint()实际上在做什么以及任务的结果是什么或者为什么用户必须单击Grid的足够详细的信息,我不能为您提供更好的解决方案。我只能提供一个实现您要求的一般模式。 显然,您的流程或目标至少分为两个步骤,以使其成为操作序列: 1. 当用户单击按钮时执行操作1 2. 当用户单击Grid时执行操作2(继续/完成操作1) 具有至少两个约束条件: 1. 可选:必须在允许API客户端重复之前完成序列。序列完成后,操作2已运行到完成。 2. 操作1始终在操作2之前执行。操作1启动序列。 3. 在允许API客户端执行操作2之前,操作1必须完成。 这需要向API的客户端发送两个通知(事件)以允许非阻塞交互: 1. 操作1完成(或需要交互) 2. 操作2(或目标)完成 您应该通过公开两种公共方法和两种公共事件来让您的API实现此行为和约束。

由于此实现仅允许对API进行单个(非并发)调用,因此建议公开一个IsBusy属性以指示运行序列。这允许在启动新序列之前轮询当前状态,尽管建议等待完成事件以执行后续调用。

实现/重构实用程序API

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>

备注

在后台线程上引发的事件将在同一线程上执行其处理程序。从在后台线程上执行的处理程序中访问像UI元素这样的DispatcherObject,需要将关键操作使用Dispatcher.InvokeDispatcher.InvokeAsync排队到Dispatcher中,以避免跨线程异常。
阅读有关DispatcherObject的备注,了解称为调度程序亲和性或线程亲和性的现象的更多信息。
为了方便使用API,建议通过捕获和使用调用者的SynchronizationContext或使用AsyncOperation(或AsyncOperationManager)将所有事件调度到调用者的原始上下文中。

可以通过提供取消(推荐)例如通过公开Cancel()方法例如PickPointCancel()和进度报告(最好使用Progress<T>)来轻松增强上述示例。


一些想法 - 回应你的评论

因为你向我寻求“更好”的阻止解决方案,给出了控制台应用程序的例子,我感到需要说服你,你的观念或观点是完全错误的。

"Consider a Console application with these two lines of code in it.

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

What happens when you execute the application in debug mode. It will stop at the first line of code and force you to enter a value in Console UI and then after you enter something and press Enter, it will execute the next line and actually print what you entered. I was thinking about exactly the same behavior but in WPF application."

一个控制台应用程序是完全不同的东西。线程概念也有点不同。控制台应用程序没有 GUI。只有输入/输出/错误流。你不能将控制台应用程序的架构与丰富的 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围绕渲染线程和UI线程构建。这些线程始终保持旋转,以便与操作系统通信,如处理用户输入-保持应用程序响应。您永远不希望暂停/阻止此线程,因为这将阻止框架执行必要的后台工作,例如响应鼠标事件-您不希望鼠标冻结:
等待=线程阻塞=不响应=糟糕的UX=恼怒的用户/客户=办公室麻烦。
有时,应用程序流需要等待输入或程序例行完成。但是我们不想阻止主线程。这就是人们发明复杂的异步编程模型的原因,以允许等待而不会阻止主线程,并且不强制开发人员编写复杂和错误的多线程代码。
每个现代应用程序框架都提供异步操作或异步编程模型,以允许开发简单高效的代码。
您努力抵制异步编程模型的事实,对我来说显示出一些理解上的欠缺。每个现代开发人员都喜欢异步API而不是同步API。没有认真的开发人员会关心使用await关键字或声明其方法为async。你是我遇到的第一个抱怨异步API并发现它们不方便使用的人。
如果我检查您的框架,该框架旨在解决UI相关问题或使UI相关任务更容易,我会期望它是异步的-一路上都是这样。非异步的UI相关API是浪费,因为它会使我的编程风格变得更加复杂,从而使我的代码变得更容易出错且难以维护。
不同的角度:当您承认等待会阻塞UI线程时,会创建非常糟糕和不可取的用户体验,因为UI将在等待结束之前冻结,现在您意识到这一点,为什么要提供一个API或插件模型,鼓励开发人员正好这样做-实现等待?您不知道第三方插件将做什么以及例行程序需要多长时间才能完成。这只是一种糟糕的API设计。当您的API在UI线程上运行时,则调用者必须能够对其进行非阻塞调用。
如果您拒绝唯一的便宜或优雅的解决方案,请像我的示例中所示使用事件驱动方法。它做你想要的事情:启动例行程序-等待用户输入-继续执行-完成目标。
我真的尝试了几次解释为什么等待/阻止是一种糟糕的应用程序设计。再次强调,您无法将控制台UI与丰富的图形化UI进行比较,例如仅监听输入流的输入处理要复杂得多。我真的不知道您的经验水平和起点在哪里,但是您应该开始接受异步编程模型。我不知道您避免使用它的原因。但这一点都不明智。
今天,异步编程模型已经在各个平台、编译器、环境、浏览器、服务器、桌面和数据库等各个领域得到了广泛的应用。事件驱动模型也可以实现相同的目标,但它使用起来不太方便(需要订阅/取消订阅事件、阅读文档(如果有)以了解事件),并且依赖于后台线程。事件驱动是老式的,只有在没有可用或不适用的异步库时才应使用。
顺便提一下:.NET Framework (.NET Standard) 提供了 TaskCompletionSource(以及其他用途),以提供将现有的事件驱动 API 转换为异步 API 的简单方法。
“我已经在 Autodesk Revit 中看到了完全相同的行为。”
行为(你所体验或观察到的)与实现方式有很大的不同。这是两回事。你的 Autodesk 很可能正在使用异步库、语言特性或其他线程机制。这也与上下文相关。当你心中的方法在后台线程上执行时,开发人员可以选择阻塞该线程。他要么有非常好的理由这样做,要么只是做了一个糟糕的设计选择。你完全走错了方向;阻塞不好。(Autodesk 的源代码开源吗?或者你是怎么知道它的实现方式的?)
我不想冒犯你,请相信我。但请重新考虑使用异步实现你的 API。只有在你的头脑中,开发人员才不喜欢使用 async/await。你显然有错误的心态。而且忘记那个控制台应用程序的论点——那是无稽之谈;)
与 UI 相关的 API 必须尽可能使用 async/await。否则,你会将所有编写非阻塞代码的工作留给你的 API 的客户端。你会迫使我将对你的 API 的每次调用都包装到后台线程中。或者使用不太方便的事件处理。相信我——每个开发人员都更愿意使用 async 来修饰他的成员,而不是进行事件处理。每当使用事件时,您可能会冒险出现潜在的内存泄漏风险——这取决于某些情况,但风险是真实存在的,并不罕见。
我真的希望你理解为什么阻塞是不好的。我真的希望你决定使用 async/await 来编写现代异步 API。尽管如此,我向你展示了一种非常常见的等待非阻塞的方法,使用事件,尽管我敦促你使用 async/await。
“该 API 将允许程序员访问 UI 等。现在假设程序员想要开发一个插件,当单击按钮时,最终用户被要求在 UI 中选择一个点。”
如果您不希望插件直接访问UI元素,您应该提供一个接口来委托事件或通过抽象对象公开内部组件。
API内部将代表Add-in订阅UI事件,然后通过向API客户端公开相应的"包装器"事件来委托事件。您的API必须提供一些钩子,Add-in可以连接以访问特定的应用程序组件。插件API像适配器或外观一样,为外部使用者提供访问内部的方式。
为了实现一定程度的隔离,请看看Visual Studio如何管理插件或允许我们实现它们。假设您想为Visual Studio编写一个插件,并查找如何实现此操作。您会发现Visual Studio通过接口或API公开其内部。例如:您可以在没有真正访问它的情况下操纵代码编辑器或获取有关编辑器内容的信息。

感谢您从另一个角度考虑问题。对于问题描述不够清晰,我表示抱歉。考虑一个控制台应用程序,其中包含以下两行代码。 var str = Console.ReadLine(); Console.WriteLine(str); 当您在调试模式下执行应用程序时会发生什么?它将停止在第一行代码处,并强制您在控制台 UI 中输入一个值,然后在您输入内容并按Enter后,它将执行下一行并实际打印您输入的内容。我正在考虑完全相同的行为,但是在 WPF 应用程序中实现。 - Vahid
在我正在开发的CAD应用程序中,用户应该能够通过插件来扩展它。API将允许程序员访问用户界面等内容。现在假设程序员想要开发一个插件,当单击按钮时,最终用户会被要求在用户界面中选择一个点,然后代码将使用给定的点进行其他酷炫操作。也许他们会要求选择另一个点并画一条线等等。 - Vahid
1
我有关于你的需求要说一些话。请读一下我的更新答案。因为它变得有点长,所以我在那里发布了回复。我承认你确实让我有些激动。请注意,我在阅读时并不想冒犯你。 - BionicCode
所以你一点也不会错过任何东西。你已经实现了最好的解决方案。我不知道具体细节,但只要你使用任务 API,你至少走在了正确的轨道上。 - BionicCode
@Vahid 如果你还感兴趣的话,可以看一下“一些想法-回复你的评论”部分——开头。我已经发布了Console.ReadLine的原始实现,向你展示了其中没有任何魔法。事实上,它只是一个简单的while(true)循环来轮询输入。没有阻塞和继续。在幕后,Console.ReadLine不断执行,直到满足中止条件。我仔细阅读了我的帖子,并再次认识到你对Console.ReadLine的行为感到印象深刻和好奇。当你审查这个实现时,你应该消除最后的疑虑。 - BionicCode
显示剩余14条评论

5
我个人认为大家把这个问题弄得太复杂了,但可能我没有完全理解需要以某种方式完成的原因,不过似乎可以使用一个简单的bool检查。
首先,通过设置"Background"和"IsHitTestVisible" 属性让您的网格可检测点击事件,否则它甚至不能捕获鼠标点击。
<grid MouseLeftButtonUp="Grid_MouseLeftButtonUp" IsHitTestVisible="True" Background="Transparent">

接下来创建一个布尔变量,用于存储“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(View)处停止,并且只有在用户单击Grid后才继续。 - Vahid
哦,是的,我完全误解了。对不起,我没有意识到你需要一切都真正停止。我认为这是不可能的,除非使用多线程,因为整个用户界面将被阻塞。 - Tronald
我仍然不确定是否不可能。使用async/await肯定是可能的,但这不是一个多线程的解决方案。但我需要的是async/await解决方案的替代方法。 - Vahid
1
当然可以,但是你提到不能使用async/await。看起来你需要使用一个分离于主线程(在UI上执行)的调度程序和线程。我希望你能找到另一种方法,因为我也很感兴趣。 - Tronald

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...
}

缺点:如果像您的示例代码一样在按钮事件处理程序中直接调用此方法,则会发生死锁,您会发现应用程序停止响应。因为您正在使用唯一的UI线程等待用户的单击,它无法响应任何用户操作,包括对网格的单击。
方法的使用者应该在另一个线程中调用它以防止死锁。如果可以保证,那就没问题。否则,您需要像这样调用方法:
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


谢谢Ken,插件是否可以从另一个线程开始,然后它的事件不会阻塞主UI线程? - Vahid
@Vahid 是的和不是。是的,你可以在另一个线程中调用阻塞方法并将其包装在另一个方法中。然而,为了避免UI阻塞,包装方法仍然需要在除UI线程之外的另一个线程中被调用。因为如果它是同步的,那么包装器将会阻塞调用线程。虽然内部包装器会阻塞另一个线程,但它仍然需要等待结果并阻塞调用线程。如果调用者在UI线程中调用包装器方法,UI将会被阻塞。 - Ken Hung

2

我尝试了几种方法,但是如果不使用 async/await,会导致 DeadLock 或者 UI 被阻塞,然后我们就无法接收到 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 的问题得到的答案是相同的。 - Vahid
哦,是的!我现在注意到了,但我想这是我找到的唯一有效的方法。 - Rao Arman

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 应用程序中拥有相同的行为吗?我在 Autodesk Revit 的 API 中看到过这种情况,那里有一个 PickPoint() 方法,它强制你在屏幕上选择一个点,而我没有看到任何异步/等待的使用!至少可以隐藏 await 关键字并从同步方法中调用它吗? - Vahid
@Vahid:Console.Readline阻塞的,也就是说,它在读取到一行之前不会返回。而你的PickPoint方法则不然,它会立即返回。虽然它可能会潜在地被阻塞,但这样做的话,在此期间你将无法处理UI输入,正如我在我的答案中所写的那样。换句话说,你必须在方法内部处理点击事件才能获得相同的行为。 - mm8
Console.ReadLine() 会阻塞,但同时允许 KeyPress 事件。我们不能在这里有完全相同的行为吗?用 PickPoint() 阻塞并只允许 MouseEvents?我无法理解为什么在控制台中是可能的,但在基于 UI 的应用程序中却不是。 - Vahid
然后,在PickPoint中,您需要设置一个单独的调度程序来处理鼠标事件。我没有看懂您的意思? - mm8
1
@Vahind:将代码设置为异步,让用户等待该方法?这是我作为UI开发人员所期望的API。在UI应用程序中调用阻塞方法没有任何意义。 - mm8
显示剩余4条评论

0

我认为问题在于设计本身。如果您的API适用于特定元素,则应在该元素的事件处理程序中使用它,而不是在另一个元素上。

例如,在这里我们想要获取单击事件在网格上的位置,API需要在与网格元素上的事件相关联的事件处理程序中使用,而不是在按钮元素上。

现在,如果要求仅在单击按钮后才处理网格上的单击,则按钮的责任将是在网格上添加事件处理程序,并且网格上的单击事件将显示消息框并删除此由按钮添加的事件处理程序,以便在此单击后不再触发...(无需阻止UI线程)

只是说,如果您在按钮单击时阻止UI线程,我认为UI线程将无法触发之后的网格单击事件。


0

首先,UI线程不能像你早期问题的答案一样被阻塞。
如果您同意这一点,那么避免使用async/await来减少客户端修改是可行的,甚至不需要任何多线程。

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;
    }
}   

但是如果你想要阻塞UI线程或者"代码流",答案将会是不可能的。因为如果UI线程被阻塞了,就无法接收到进一步的输入。
既然你提到了控制台应用程序,我来简单解释一下。
当你运行一个控制台应用程序或从未附加到任何控制台(窗口)的进程中调用AllocConsole时,将执行可以提供控制台(窗口)的conhost.exe,并将控制台应用程序或调用进程附加到控制台(窗口)。
因此,任何可能阻塞调用线程的代码,例如Console.ReadKey,都不会阻塞控制台窗口的UI线程,这也是为什么控制台应用程序在等待您的输入时仍然可以响应其他输入,如鼠标点击的原因。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接