“如何在事件触发之前阻止代码流?”
您的做法是错误的。事件驱动并不意味着阻止并等待事件。您从不等待,至少您总是尽力避免这种情况。等待正在浪费资源,阻塞线程,并可能带来死锁或僵尸线程的风险(以防永不发出信号)。
应该很清楚,阻塞线程等待事件是一种反模式,因为它与事件的思想相矛盾。
通常,您有两个(现代)选项:实现异步API或事件驱动的API。由于您不想异步实现API,因此需要使用事件驱动的API。
事件驱动的API的关键在于,您可以让调用者继续并在结果准备好或操作完成后向他发送通知,而不是强制调用者同步等待结果或轮询结果。同时,呼叫者可以继续执行其他操作。
从线程的角度看问题时,事件驱动的API允许调用线程(例如执行按钮的事件处理程序的UI线程)自由地继续处理与UI相关的操作,例如呈现UI元素或处理用户输入,例如鼠标移动和按键。尽管不方便,但效果类似于异步API。
由于您没有提供足够的详细信息来说明您Utility.PickPoint()
实际上要做什么,实际正在做什么以及任务的结果是什么,或者用户为什么必须单击“网格”,所以我无法为您提供更好的解决方案。我只是可以提供一种如何实现您的要求的一般模式。
您的流程或目标显然分为至少两个步骤,以使其成为一系列操作:
- 当用户单击按钮时执行操作1
- 当用户单击“执行”时,执行操作2(继续/完成操作1)。
Grid
至少有两个约束:
- 可选:必须在允许API客户端重复序列之前完成序列。操作2运行完成后,序列即完成。
- 始终在操作2之前执行操作1。操作1启动序列。
- 必须在允许API客户端执行操作2之前完成操作1
这要求API的客户端收到两个通知,以允许非阻塞交互:
- 操作1已完成(或需要互动)
- 操作2(或目标)已完成
您应该通过公开两个公共方法和两个公共事件,让您的API实现此行为和约束。
实施/重构实用程序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
从在后台线程上执行的处理程序访问类似UI元素的操作,需要将关键操作加入或Dispatcher
使用,以避免交叉线程异常。Dispatcher.Invoke
Dispatcher.InvokeAsync
一些想法-回复您的评论
因为您正在与我一起寻找一种“更好的”阻止解决方案,以控制台应用程序为例,所以我说服您,您的看法或观点是完全错误的。
“考虑其中包含这两行代码的控制台应用程序。
var str = Console.ReadLine();
Console.WriteLine(str);
在调试模式下执行应用程序时会发生什么。它将在代码的第一行停止,并强制您在控制台UI中输入一个值,然后在输入内容并按Enter之后,它将执行下一行并实际打印您输入的内容。我在考虑完全相同的行为,只是在WPF应用程序中。”
控制台应用程序完全不同。线程概念不同。控制台应用程序没有GUI。只是输入/输出/错误流。您无法将控制台应用程序的体系结构与丰富的GUI应用程序进行比较。这行不通。您确实必须理解并接受这一点。
WPF是围绕渲染线程和UI线程构建的。这些线程始终保持旋转状态,以便与OS进行通信,例如处理用户输入-保持应用程序响应速度。您永远不想暂停/阻止此线程,因为它将阻止框架执行必要的后台工作(例如响应鼠标事件-您不希望鼠标冻结):
等待=线程阻塞=无响应=不良UX =烦恼的用户/客户=办公室出现麻烦。
有时,应用程序流程需要等待输入或例程完成。但是我们不想阻塞主线程。
这就是为什么人们发明了复杂的异步编程模型的原因,以允许在不阻塞主线程的情况下进行等待,而又不强迫开发人员编写复杂且错误的多线程代码。
每个现代应用程序框架都提供异步操作或异步编程模型,以允许开发简单而有效的代码。
您正在努力抵制异步编程模型这一事实表明,我有些缺乏理解。每个现代开发人员都喜欢异步API,而不是同步API。没有认真的开发人员会在乎使用await
关键字或声明其方法async
。没有人。您是我遇到的第一个抱怨异步API的人,他们发现它们不方便使用。
如果我要检查您的框架,该框架的目标是解决与UI相关的问题或使与UI相关的任务更加容易,那么我希望它一直都是异步的。
与UI相关的非异步API是浪费的,因为这会使我的编程风格复杂化,因此我的代码因此更容易出错并且难以维护。
一个不同的观点:当您确认等待阻塞了UI线程时,由于UI将冻结直到等待结束,因此会产生非常糟糕的用户体验,现在您意识到了这一点,为什么要提供鼓励使用API或插件模型的原因开发人员完全可以做到这一点-实施等待吗?
您不知道3rd party插件将做什么以及例程完成之前需要花费多长时间。这只是一个糟糕的API设计。当您的API在UI线程上运行时,API的调用者必须能够对其进行非阻塞调用。
如果您拒绝唯一便宜或优雅的解决方案,而不是使用示例所示的事件驱动方法。
它可以满足您的要求:启动例程-等待用户输入-继续执行-完成目标。
我确实尝试过几次以解释为什么等待/阻止是一个不好的应用程序设计。同样,您不能将控制台UI与丰富的图形UI进行比较,例如,单独的输入处理比仅侦听输入流要复杂得多。我真的不知道您的经验水平和学习起点,但是您应该开始接受异步编程模型。我不知道您尝试避免这种情况的原因。但这根本不明智。
如今,异步编程模型在每个平台,编译器,每个环境,浏览器,服务器,桌面,数据库上的任何地方都可以实现。事件驱动模型可以实现相同的目标,但是依赖于后台线程,使用起来较不方便(订阅/取消订阅事件/从事件中订阅)。事件驱动是老式的,仅当异步库不可用或不适用时才应使用。
“我已经看到了Autodesk Revit的确切行为。”
行为(您所体验或观察到的)与该体验的实施方式有很大不同。两件事。您的Autodesk很可能使用异步库或语言功能或某些其他线程机制。它也是与上下文相关的。当您想到的方法在后台线程上执行时,开发人员可以选择阻止该线程。他要么有充分的理由这样做,要么只是做出了错误的设计选择。您完全走错了路;)阻塞不好。
(Autodesk源代码是开源的吗?还是您怎么知道其实现方式?)
我不想得罪你,请相信我。但请重新考虑实现您的API异步。开发人员不喜欢使用async / await,这只是在您的脑海中。您显然有错误的心态。忘了那个控制台应用程序的参数-这是胡说八道;)
与UI相关的API 必须 尽可能使用async / await。否则,将所有工作留给API的客户端编写非阻塞代码。您会迫使我将对API的每次调用都包装到后台线程中。或者使用不太舒适的事件处理。相信我-每个开发人员都宁愿用来装饰其成员async
,而不是进行事件处理。每次使用事件时,您可能会冒潜在的内存泄漏的风险-取决于某些情况,但是这种风险是真实的,并且在不小心编程时并不罕见。
我真的希望你能理解为什么阻塞不好。我真的希望您决定使用async / await编写现代异步API。不过,尽管我敦促您使用异步/等待,但我向您展示了一种使用事件等待非阻塞的非常普通的方法。
“ API将允许程序员访问UI等。现在,假定程序员希望开发一个外接程序,当单击按钮时,将要求最终用户在UI中选择一个点。”
如果您不想让插件直接访问UI元素,则应提供一个接口来委托事件或通过抽象对象公开内部组件。
API内部将代表外接程序预订UI事件,然后通过向API客户端公开相应的“包装器”事件来委托事件。您的API必须提供一些挂钩,外接程序可以在其中连接以访问特定的应用程序组件。插件API的作用类似于适配器或Facade,以使外部对象可以访问内部对象。
允许一定程度的隔离。
看一下Visual Studio如何管理插件或允许我们实现它们。假设您想为Visual Studio编写一个插件,并对如何执行此操作进行一些研究。您将认识到Visual Studio通过接口或API公开了其内部。EG,您可以操纵代码编辑器或获取有关编辑器内容的信息,而无需真正访问它。
Aync/Await
如何执行操作A并保存该操作STATE,现在您希望该用户单击Grid ..so,如果用户单击Grid,则检查状态是否为true,然后执行其他操作即可?