「イベントが発生するまでコードフローをブロックする方法」
あなたのアプローチは間違っています。イベント駆動型は、イベントをブロックして待機することを意味しません。あなたは決して待つことはありません、少なくともあなたは常にそれを避けるために一生懸命努力します。待機はリソースを浪費し、スレッドをブロックし、おそらくデッドロックまたはゾンビスレッド(シグナルが発生しない場合)のリスクをもたらします。
スレッドがイベントを待機するのをブロックすることは、イベントの概念と矛盾するため、アンチパターンであることは明らかです。
通常、2つの(モダン)オプションがあります。非同期APIまたはイベント駆動型APIを実装します。APIを非同期で実装したくないので、イベント駆動型APIが残ります。
イベント駆動型APIの重要な点は、呼び出し側に結果を同期的に待機させたり、結果をポーリングさせたりする代わりに、呼び出し側に続行させ、結果が準備できたら、または操作が完了したら通知を送信することです。その間、呼び出し元は他の操作を実行し続けることができます。
スレッディングの観点から問題を見ると、イベント駆動型APIにより、ボタンのイベントハンドラーを実行するUIスレッドなどの呼び出しスレッドが、UI要素のレンダリングや処理など、UI関連の操作を自由に継続して処理できるようになります。マウスの動きやキーの押下などのユーザー入力。非同期APIと同じ効果がありますが、便利ではありません。
実際に実行しようとしていること、Utility.PickPoint()
実際に実行していること、タスクの結果について、またはユーザーが「グリッド」をクリックしなければならない理由について十分な詳細情報を提供しなかったため、これ以上の解決策は提供できません。私はあなたの要件を実装する方法の一般的なパターンを提供できます。
フローまたは目標は、それを一連の操作にするために、少なくとも2つのステップに明らかに分割されます。
- ユーザーがボタンをクリックしたときに操作1を実行する
- ユーザーがをクリックすると、操作2(操作1の続行/完了)を実行します。
Grid
少なくとも2つの制約あり:
- オプション:APIクライアントがシーケンスを繰り返すことができるようになる前に、シーケンスを完了する必要があります。操作2が完了すると、シーケンスが完了します。
- オペレーション1は常にオペレーション2の前に実行されます。オペレーション1はシーケンスを開始します。
- APIクライアントが操作2の実行を許可される前に、操作1を完了する必要があります
これには、非ブロッキング相互作用を許可するために、APIのクライアントに2つの通知が必要です。
- 操作1が完了(または操作が必要)
- 操作2(または目標)が完了しました
2つのパブリックメソッドと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
スレッド間の例外を回避する必要があります。
いくつかの考え-コメントへの返信
あなたが「より良い」ブロッキングソリューションを見つけるために私に近づいていたので、コンソールアプリケーションの例を考えると、私はあなたの見方や見方がまったく間違っているとあなたに納得させました。
「この2行のコードを含むコンソールアプリケーションを検討してください。
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デザインです。APIがUIスレッドで動作する場合、APIの呼び出し元はAPIへの非ブロッキング呼び出しを実行できる必要があります。
私の例に示すように、イベント駆動型のアプローチを使用するよりも、安価で優雅な唯一のソリューションを否定する場合。
それはあなたが望むことを行います:ルーチンを開始します-ユーザー入力を待ちます-実行を続けます-目標を達成します。
待機/ブロックが悪いアプリケーション設計である理由を説明するために、私は実際に何度か試みました。繰り返しますが、コンソールUIをリッチなグラフィカルUIと比較することはできません。たとえば、入力ストリームだけを聞くよりも入力処理だけの方がはるかに複雑です。あなたの経験レベルとどこから始めたのか本当にわかりませんが、非同期プログラミングモデルを採用する必要があります。あなたがそれを避けようとする理由がわかりません。しかし、それはまったく賢明ではありません。
今日、非同期プログラミングモデルは、あらゆるプラットフォーム、コンパイラ、あらゆる環境、ブラウザ、サーバー、デスクトップ、データベースのあらゆる場所に実装されています。イベント駆動型モデルでも同じ目標を達成できますが、バックグラウンドスレッドに依存している(イベントへのサブスクライブ/サブスクライブ解除)の使用はあまり便利ではありません。イベント駆動型は旧式であり、非同期ライブラリーが利用できないか適用できない場合にのみ使用してください。
「Autodesk Revitで正確な動作を見てきました。」
動作(ユーザーが体験または観察するもの)は、この体験が実装される方法とは大きく異なります。2つの異なるもの。オートデスクは、非同期ライブラリ、言語機能、またはその他のスレッドメカニズムを使用している可能性が非常に高いです。また、コンテキストにも関連しています。気になるメソッドがバックグラウンドスレッドで実行されている場合、開発者はこのスレッドをブロックすることを選択できます。彼には、これを行う十分な理由があるか、設計を誤って選択したかのどちらかです。あなたは完全に間違った軌道に乗っています;)ブロッキングは良くありません。
(オートデスクのソースコードはオープンソースですか?それとも、どのように実装されているかをどうやって知るのですか?)
私はあなたを怒らせたくありません、私を信じてください。ただし、APIを非同期で実装することを再検討してください。開発者がasync / awaitを使いたくないのはあなたの頭の中だけです。あなたは明らかに間違った考え方を持っていました。そして、そのコンソールアプリケーションの引数を忘れてください-それはナンセンスです;)
UI関連のAPI は、 可能な場合は常に非同期/待機を使用する必要があります。それ以外の場合は、ノンブロッキングコードを作成するすべての作業をAPIのクライアントに任せます。APIへのすべての呼び出しをバックグラウンドスレッドにラップするように強制します。または、あまり快適でないイベント処理を使用します。私を信じて-すべての開発者はasync
、イベント処理を行うよりも、むしろでメンバーを飾ります。イベントを使用するたびに、潜在的なメモリリークのリスクが発生する可能性があります-状況によって異なりますが、リスクは現実的であり、不注意にプログラミングした場合はまれではありません。
ブロッキングが悪い理由を理解していただければ幸いです。最新の非同期APIを作成するためにasync / awaitを使用することを決定したいと思います。それでも、イベントを使用して非ブロッキングを待機する非常に一般的な方法を示しましたが、非同期/待機を使用することをお勧めします。
「APIにより、プログラマーはUIなどにアクセスできるようになります。プログラマーがボタンをクリックすると、最終ユーザーがUIでポイントを選択するように要求されるアドインを開発したいとします。」
プラグインがUI要素に直接アクセスできないようにする場合は、イベントを委任するためのインターフェースを提供するか、抽象化されたオブジェクトを介して内部コンポーネントを公開する必要があります。
APIは、アドインに代わってUIイベントを内部的にサブスクライブし、対応する「ラッパー」イベントをAPIクライアントに公開することにより、イベントを委任します。APIは、特定のアプリケーションコンポーネントにアクセスするためにアドインが接続できるフックを提供する必要があります。プラグインAPIは、外部から内部へのアクセスを提供するためのアダプターまたはファサードのように機能します。
ある程度の分離を可能にするため。
Visual Studioがプラグインを管理する方法またはプラグインを実装できるようにする方法を見てください。Visual Studio用のプラグインを作成して、これを行う方法についていくつかの調査を行いたいとしましょう。Visual StudioがインターフェイスまたはAPIを介して内部を公開していることに気付くでしょう。EGあなたは、コードエディタを操作したりすることなく、エディタのコンテンツについての情報を得ることができ、実際のそれへのアクセス。
Aync/Await
、操作Aを実行してその操作STATEを保存する方法です。ユーザーがグリッドをクリックする必要があります。