我在WPF应用程序中有一个带有按钮的网格。当用户单击该按钮时,将执行实用程序类中的一个方法,该方法强制应用程序接收网格上的单击操作。代码流必须在此处停止,并且在用户单击网格之前不能继续。
我以前也有过类似的问题,我使用async/await得到了答案,但由于我将此方法作为API的一部分使用,因此我不想使用async/await,因为它将要求API的使用者将其方法标记为async,这是我不希望的。
Wait till user click C# WPF
如何在不使用async/await的情况下编写Utility.PickPoint(Grid grid)
方法来实现这一目标?我看到了这个答案,它可能会有帮助,但我不完全理解如何将它应用到我的情况中。
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)
{
}
}
7条答案
按热度按时间mmvthczy1#
“如何阻止代码流直到事件被激发?”
你的方法是错误的。事件驱动并不意味着阻塞和等待一个事件。你从不等待,至少你总是努力避免它。等待是浪费资源,阻塞线程,并可能引入死锁或僵尸线程的风险(在释放信号从未被引发的情况下)。
应该清楚的是,阻止线程 * 等待 * 事件是一种反模式,因为它与事件的概念相矛盾。
您通常有两个(现代)选项:实现异步API或事件驱动的API。由于您不希望实现异步API,因此您只能使用事件驱动的API。
事件驱动API的关键是,不是强制调用者同步等待结果或轮询结果,而是让调用者继续,并在结果准备就绪或操作完成时向他发送通知。同时,调用者可以继续执行其他操作。
当从线程的Angular 来看问题时,事件驱动的API允许执行按钮的事件处理程序的调用线程(例如UI线程)自由地继续处理例如其他UI相关操作,如呈现UI元素或处理用户输入(如鼠标移动和按键)。事件驱动的API具有与异步API相同的效果或目标。尽管它远不方便。
由于您没有提供足够的细节来说明您真正要做什么、
Utility.PickPoint()
实际在做什么、任务的结果是什么或者为什么用户必须单击'Grid,因此我无法提供更好的解决方案,只能提供一个如何实现您的需求的一般模式。你的流或目标显然被分成至少两个步骤来使它成为一个操作序列:
1.用户单击按钮时执行操作1
1.当用户单击
Grid
时,执行操作2(继续/完成操作1)具有至少两个约束:
1.可选:必须先完成序列,然后才允许API客户机重复序列。工序2运行完成后,序列即告完成。
1.工序1始终在工序2之前执行。工序1启动工序。
1.必须先完成操作1,然后才允许API客户端执行操作2
这需要两个通知(事件),以便API的客户端允许非阻塞交互:
1.操作1已完成(或需要交互)
1.操作2(或目标)已完成
您应该通过公开两个公共方法和两个公共事件来让API实现此行为和约束。
由于此实现只允许对API进行一次(非并发)调用,因此建议公开一个
IsBusy
属性来指示正在运行的序列。这允许在启动新序列之前轮询当前状态,尽管建议等待已完成的事件来执行后续调用。实作/重构公用程式API
实用程序.cs
选取点完成事件参数.cs
使用API
主窗口.xaml.cs
主窗口.xaml
备注
在背景执行绪上引发的事件会在相同的执行绪上执行其行程常式。从行程常式存取
DispatcherObject
(例如UI项目)(在背景执行绪上执行),需要使用Dispatcher.Invoke
或Dispatcher.InvokeAsync
将关键作业排入Dispatcher
队列,以避免跨执行绪例外状况。阅读关于
DispatcherObject
的评论,了解更多关于这种称为调度程序关联或线程关联的现象。为了方便地使用API,我建议通过捕获和使用调用者的
SynchronizationContext
或通过使用AsyncOperation
(或AsyncOperationManager
),将所有事件编组到调用者的原始上下文。通过提供取消(推荐的),例如通过展示
Cancel()
方法(例如PickPointCancel()
)和进度报告(优选地使用Progress<T>
),可以容易地增强上述示例。一些想法-回复您的评论
因为你找我是为了找到一个“更好的”阻止解决方案,给我一个控制台应用程序的例子,我觉得要说服你,你的看法或观点是完全错误的。
“考虑一个包含这两行代码的控制台应用程序。
当你在调试模式下执行应用程序时会发生什么。它会在第一行代码处停止,并强制你在控制台UI中输入一个值,然后在你输入一些内容并按Enter键后,它会执行下一行,并实际打印你输入的内容。我正在考虑完全相同的行为,但在WPF应用程序中。”
控制台应用程序是完全不同的东西。线程概念有点不同。控制台应用程序没有GUI。只有输入/输出/错误流。你不能将控制台应用程序的架构与丰富的GUI应用程序进行比较。这是行不通的。你真的必须理解并接受这一点。
也不要被 * 外表 * 所欺骗。你知道 *
Console.ReadLine
* 内部发生了什么吗?它是如何 * 实现 * 的?它是阻塞主线程并并行读取输入吗?还是只是轮询?下面是
Console.ReadLine
的原始实现:正如你所看到的,这是一个简单的 synchronous 操作。它在一个“无限”循环中轮询用户输入。没有魔术块,继续。
WPF是围绕一个渲染线程和一个UI线程构建的。这些线程 * 总是 * 旋转,以便与操作系统进行通信,比如处理用户输入-保持应用程序 * 响应 *。您永远不想暂停/阻止这个线程,因为它会阻止框架进行必要的后台工作,比如响应鼠标事件-您不希望鼠标冻结:
等待=线程阻塞=无响应=糟糕的UX =惹恼用户/客户=办公室的麻烦。
有时,应用程序流需要等待输入或例程完成,但我们不想阻塞主线程。
这就是为什么人们发明了复杂的异步编程模型,以允许在不阻塞主线程的情况下进行等待,并且不迫使开发人员编写复杂且错误的多线程代码。
每个现代应用程序框架都提供异步操作或异步编程模型,以允许开发简单而高效的代码。
事实上,你正在努力抵制异步编程模型,这对我来说是缺乏理解的。每个现代的开发者都喜欢异步API而不是同步API。没有一个严肃的开发者关心使用
await
关键字或者声明他的方法async
。没有人。你是我遇到的第一个抱怨异步API并且发现它们不方便使用的人。如果我检查您的框架,它的目标是解决UI相关的问题或使UI相关的任务更容易,我会 * 期望 * 它是异步的-所有的方式。
与UI相关的非异步API是浪费,因为它会使我的编程风格复杂化,因此我的代码变得更容易出错,难以维护。
不同的视角:当你意识到等待会阻塞UI线程时,会产生一种非常糟糕和不理想的用户体验,因为UI会冻结,直到等待结束,既然你意识到了这一点,为什么还要提供一个API或插件模型来鼓励开发人员这样做-实现等待呢?
你不知道第三方插件会做什么,也不知道例程要花多长时间才能完成。这是一个糟糕的API设计。当你的API在UI线程上运行时,你的API调用者必须能够对它进行非阻塞调用。
如果您否定唯一廉价或优雅的解决方案,那么请使用事件驱动的方法,如我的示例所示。
它可以满足您的需求:启动例程-等待用户输入-继续执行-完成目标。
我真的试过好几次解释为什么等待/阻塞是一个糟糕的应用程序设计。同样,你不能把一个控制台用户界面和一个丰富的图形用户界面相比,比如说,单独的输入处理比仅仅听输入流要复杂得多。我真的不知道你的经验水平和你是从哪里开始的,但是你应该开始接受异步编程模型。我不知道你为什么要避免它。但是这一点都不明智。
今天,异步编程模型在任何地方,在任何平台、编译器、任何环境、浏览器、服务器、桌面、数据库--任何地方都被实现。(订阅/取消订阅事件,阅读文档(当存在文档时)以了解事件),依赖于后台线程。事件驱动是老式的,只应在异步库不可用或不适用时使用。
作为旁注:NET框架(.NET标准)提供了
TaskCompletionSource
(以及其他用途)来提供一种将现有的事件驱动API转换为异步API的简单方法。“我已经在Autodesk Revit中看到了确切的行为。”
行为(您所体验或观察到的)与如何实现这种体验有很大的不同。两件不同的事情。您的Autodesk很可能使用异步库或语言功能或其他线程机制。而且它还与上下文相关。当您脑海中的方法在后台线程上执行时,开发人员可能会选择阻止此线程。他要么有很好的理由这样做,要么只是做了一个糟糕的设计选择。你完全走错了路;)堵塞不好。
(Is Autodesk源代码开源?或者你怎么知道它是如何实现的?)
我不想冒犯你,请相信我。但是请重新考虑异步实现你的API。只是在你的头脑中,开发人员不喜欢使用async/await。你显然有错误的心态。忘记控制台应用程序的论点--这是胡说八道;)
UI相关的API * 必须 * 尽可能使用async/await。否则,你会把所有的工作都留给你API的客户端去写非阻塞代码。你会迫使我把对你API的每个调用都 Package 到一个后台线程中。或者使用不太舒服的事件处理。相信我-每个开发人员都宁愿用
async
来装饰他的成员,每次使用事件时,都可能会有潜在的内存泄漏风险-这取决于某些情况,但当编程粗心时,这种风险是真实的的,并不罕见。我真的希望你能理解为什么阻塞是不好的。我真的希望你决定使用async/await来编写一个现代的异步API。尽管如此,我还是向你展示了一个非常常见的非阻塞等待方法,使用事件,尽管我强烈建议你使用async/await。
API将允许程序员访问UI等。现在假设程序员希望开发一个加载项,当单击按钮时,最终用户将被要求在UI中选择一个点。
如果您不想让插件直接访问UI元素,您应该提供一个接口来委托事件或通过抽象对象公开内部组件。
API会在内部代表增益集订阅UI事件,然后透过将Map的“ Package 函数”事件公开给API客户端来委派事件。您的API必须提供一些拦截,增益集可以在这些拦截处连接以存取特定的应用程序元件。插件API的作用就像配接器或外观,给予外部存取内部。
允许一定程度的隔离。
看看Visual Studio是如何管理插件或允许我们实现它们的。假设你想为Visual Studio编写一个插件,并研究一下如何做到这一点。你会发现Visual Studio通过一个接口或API公开它的内部。例如,你可以操纵代码编辑器或获得有关编辑器内容的信息,而不必 * 真正 * 访问它。
bgtovc5b2#
我个人认为这是过于复杂的每个人,但也许我不完全理解的原因,为什么这需要这样做的某种方式,但它似乎像一个简单的布尔检查可以在这里使用。
首先也是最重要的,通过设置
Background
和IsHitTestVisible
属性使网格可命中测试,否则它甚至无法捕获鼠标点击。接下来创建一个bool值,用于存储是否应该发生“GridClick”事件。当网格被单击时,检查该值,如果它正在等待单击,则从网格单击事件执行。
示例:
j5fpnvbx3#
我尝试了一些东西,但我无法使它没有
async/await
。因为如果我们不使用它,它会导致DeadLock
或UI被阻止,然后我们能够采取Grid_Click
输入。ycl3bljg4#
您可以使用
SemaphoreSlim
:您不能,也不想同步阻塞调度程序线程,因为这样它将永远无法处理
Grid
上的点击,也就是说,它不能同时被阻塞和处理事件。iqjalb3h5#
从技术上讲,使用
AutoResetEvent
和不使用async/await
都是可行的,但有一个明显的缺点:缺点:如果您像范例程式码一样,直接在按钮事件行程常式中呼叫这个方法,就会发生死锁,而且您会看到应用程序停止回应。因为您是使用唯一的UI执行绪来等候使用者的按一下,所以它无法回应任何使用者的动作,包括使用者在方格上的按一下。
方法的消费者应该在另一个线程中调用它以防止死锁。如果可以保证,那就好。否则,你需要像这样调用方法:
这可能会给你的API的消费者带来更多的麻烦,除非他们习惯于管理自己的线程。
ss2ws0br6#
我认为问题出在设计本身。如果你的API在一个特定的元素上工作,那么它应该用在这个元素的事件处理程序中,而不是用在另一个元素上。
例如,在这里,我们要获取单击事件在Grid上的位置,需要在与Grid元素上的事件关联的事件处理程序中使用API,而不是在button元素上。
现在,如果要求仅在我们单击Button后处理Grid上的单击,则Button的职责将是在Grid上添加事件处理程序,Grid上的单击事件将显示消息框并删除按钮添加的此事件处理程序,以便在此单击后不再触发...(无需阻塞UI Thread)
我只想说,如果在单击按钮时阻塞UI线程,我认为UI线程将无法在之后触发Grid上的单击事件。
km0tfn4u7#
首先,UI线程不能被阻塞,就像你从前面的问题中得到的答案一样。
如果你能同意这一点,那么避免async/await以使你的客户做更少的修改是可行的,甚至不需要任何多线程。
但如果你想阻塞UI线程或“代码流”,答案将是不可能的。因为如果UI线程被阻塞,就没有进一步的输入可以接收。
既然你提到了控制台应用程序,我只是做一些简单的解释。
当您运行控制台应用程序或从未附加到任何控制台(窗口)的进程调用
AllocConsole
时,将执行可提供控制台(窗口)的conhost.exe,并且控制台应用程序或调用方进程将附加到控制台(窗口)。因此,您编写的任何可能阻塞调用者线程(如
Console.ReadKey
)的代码都不会阻塞控制台窗口的UI线程,这就是为什么当控制台应用程序等待您的输入时,仍然可以响应其他输入(如鼠标单击)的原因。