ワーカースレッド/クラスからGUIスレッド/クラスを更新するにはどうすればよいですか?
-
26-09-2019 - |
質問
最初の質問はここです。皆さん、こんにちは。
私が取り組んでいる要件は、シリアル ポート経由で外部デバイスと通信する小さなテスト アプリケーションです。通信には長時間かかる場合があり、デバイスはあらゆる種類のエラーを返す可能性があります。
デバイスは独自のクラスで適切に抽象化されており、GUI スレッドが独自のスレッドで実行を開始し、通常のオープン/クローズ/データ読み取り/データ書き込みの基本機能を備えています。GUI も非常にシンプルです。COM ポートを選択し、開く、閉じる、デバイスから読み取ったデータやエラーを表示する、変更を許可する、書き戻すなどを行います。
問題は、デバイス クラスから GUI をどのように更新するかということだけです。デバイスが扱うデータにはいくつかの異なるタイプがあるため、GUI フォーム/スレッド クラスと動作中のデバイス クラス/スレッドの間に比較的汎用的なブリッジが必要です。GUI からデバイスへの方向では、オープン/クローズ/読み取り/書き込みなどの [Begin]Invoke 呼び出しですべてが正常に機能します。さまざまな GUI で生成されたイベント。
スレッドを読みました ここ (C# の別のスレッドから GUI を更新するには?) ここでは、GUI とワーカー スレッドが同じクラスにあると仮定しています。Google 検索では、デリゲートの作成方法や従来のバックグラウンド ワーカーの作成方法が表示されますが、解決策の一部である可能性はあっても、それは私が必要とするものではありません。では、使用できる単純だが汎用的な構造はあるのでしょうか?
私の C# のレベルは中程度で、それを理解する (そしてポストバックする) ヒントがあれば、これまでずっとプログラミングをしてきました。よろしくお願いいたします。
解決
UI クラスでパブリック メソッドを公開し、デバイス クラスが UI に渡す必要があるすべての情報とともにバックグラウンド スレッドで呼び出すことができます。このパブリック メソッドはバックグラウンド スレッドのコンテキストで実行されますが、UI クラスに属しているため、これまでに説明した呼び出しマーシャリング手法のいずれかを使用できます。
したがって、最も単純な設計は次のようになります。
- UI クラスにメソッドを追加します (たとえば、
MyUIForm
)次のようなものと呼ばれますUpdateUI()
これは、デバイスから使用する UI にデータを渡すために使用しているデータ構造を受け取ります。そのメソッドをインターフェイスで宣言できます(たとえば、IUIForm
)、後で DI/IoC をサポートし、フォームにそれを実装させる場合。 - スレッド A (UI スレッド) では、UI クラスがデバイス クラスを作成し、必要な設定をすべて初期化し、そのバックグラウンド スレッドを開始します。また、それ自体へのポインタも渡します。
- スレッド B では、デバイスがデータを収集して呼び出します。
MyUIForm.UpdateUI()
(またはIUIForm.UpdateUI()
). UpdateUI
するInvoke
またはBeginInvoke
適切に。
これには、すべての UI およびプレゼンテーション ロジックを UI クラスにカプセル化するという副次的な利点があることに注意してください。これで、デバイス クラスはハードウェアの処理に集中できるようになります。
アップデート: スケーラビリティに関する懸念に対処するには -
アプリがどれだけ成長し、UI クラスがどれだけ増えても、更新する特定の UI クラスに対して BeginInvoke を使用してスレッド境界を越える必要があります。(その UI クラスは、特定のコントロールまたは特定のビジュアル ツリーのルートである可能性がありますが、実際には問題ではありません)。主な理由は、複数の UI スレッドがある場合、そのスレッド上で UI の更新が確実に行われるようにする必要があることです。この特定の UI は、Windows メッセージングとウィンドウの動作方法に基づいて作成されました。したがって、境界スレッドを越える実際のロジックは UI レイヤーにカプセル化する必要があります。
デバイス クラスは、どの UI クラスとどのスレッドを更新する必要があるかを気にする必要はありません。実際、私は個人的に、デバイスを UI について完全に無視し、さまざまな UI クラスがサブスクライブできるイベントをそのデバイス上に公開するだけです。
別の解決策は、スレッドをデバイス クラスに完全にカプセル化して、UI がバックグラウンド スレッドの存在を認識しないようにすることであることに注意してください。ただし、スレッド境界の交差はデバイス クラスの責任となり、そのロジック内に含まれる必要があるため、スレッドを交差する UI 方法を使用すべきではありません。これは、デバイス クラスが特定の UI スレッドにバインドされていることも意味します。
他のヒント
イベントハンドラーを搭載したバージョンです。
簡素化されているため、フォームに UI コントロールはなく、SerialIoEventArgs クラスにプロパティもありません。
- UI を更新するコードをコメントの下に配置します // Update UI
- シリアル IO を読み取るコードをコメントの下に配置します // Read from Serial IO
- SerialIoEventArgs クラスにフィールド/プロパティを追加し、OnReadCompleated メソッドに設定します。
public class SerialIoForm : Form
{
private delegate void SerialIoResultHandlerDelegate(object sender, SerialIoEventArgs args);
private readonly SerialIoReader _serialIoReader;
private readonly SerialIoResultHandlerDelegate _serialIoResultHandler;
public SerialIoForm()
{
Load += SerialIoForm_Load;
_serialIoReader = new SerialIoReader();
_serialIoReader.ReadCompleated += SerialIoResultHandler;
_serialIoResultHandler = SerialIoResultHandler;
}
private void SerialIoForm_Load(object sender, EventArgs e)
{
_serialIoReader.StartReading();
}
private void SerialIoResultHandler(object sender, SerialIoEventArgs args)
{
if (InvokeRequired)
{
Invoke(_serialIoResultHandler, sender, args);
return;
}
// Update UI
}
}
public class SerialIoReader
{
public EventHandler ReadCompleated;
public void StartReading()
{
ThreadPool.QueueUserWorkItem(ReadWorker);
}
public void ReadWorker(object obj)
{
// Read from serial IO
OnReadCompleated();
}
private void OnReadCompleated()
{
var readCompleated = ReadCompleated;
if (readCompleated == null) return;
readCompleated(this, new SerialIoEventArgs());
}
}
public class SerialIoEventArgs : EventArgs
{
}
そこで、上記の回答に基づいていくつかの調査を行った後、さらに Google で検索し、C# について少し詳しい同僚に質問した結果、私が選んだ問題の解決策は以下のとおりです。私は引き続きコメント、提案、改良に興味を持っています。
まず、問題についてさらに詳しく説明します。これは、GUI が応答する必要がある一連のイベントを通じて、完全に抽象的なものでなければならない何かを制御しているという意味で、実際には非常に一般的なものです。いくつかの明確な問題があります。
- さまざまなデータ型を持つイベント自体。プログラムが進化するにつれて、イベントは追加、削除、変更されます。
- GUI を構成する複数のクラス (さまざまな UserControl) とハードウェアを抽象化するクラスをブリッジする方法。
- すべてのクラスはイベントを生成および消費できますが、可能な限り分離されたままにする必要があります。
- コンパイラは、コーディングのコックアップを可能な限り特定する必要があります (例:あるデータ型を送信するが、消費者は別のデータ型を期待するイベント)
この最初の部分はイベントです。GUI とデバイスは、さまざまなデータ型が関連付けられた複数のイベントを発生させる可能性があるため、イベント ディスパッチャーが便利です。これはイベントとデータの両方で汎用的である必要があるため、次のようになります。
// Define a type independent class to contain event data
public class EventArgs<T> : EventArgs
{
public EventArgs(T value)
{
m_value = value;
}
private T m_value;
public T Value
{
get { return m_value; }
}
}
// Create a type independent event handler to maintain a list of events.
public static class EventDispatcher<TEvent> where TEvent : new()
{
static Dictionary<TEvent, EventHandler> Events = new Dictionary<TEvent, EventHandler>();
// Add a new event to the list of events.
static public void CreateEvent(TEvent Event)
{
Events.Add(Event, new EventHandler((s, e) =>
{
// Insert possible default action here, done every time the event is fired.
}));
}
// Add a subscriber to the given event, the Handler will be called when the event is triggered.
static public void Subscribe(TEvent Event, EventHandler Handler)
{
Events[Event] += Handler;
}
// Trigger the event. Call all handlers of this event.
static public void Fire(TEvent Event, object sender, EventArgs Data)
{
if (Events[Event] != null)
Events[Event](sender, Data);
}
}
ここでいくつかのイベントが必要になりますが、C の世界では列挙型が好きなので、GUI が発生させるいくつかのイベントを定義します。
public enum DEVICE_ACTION_REQUEST
{
LoadStuffFromXMLFile,
StoreStuffToDevice,
VerifyStuffOnDevice,
etc
}
EventDispatcher の静的クラスのスコープ (通常は名前空間) 内のどこでも、新しいディスパッチャーを定義できるようになりました。
public void Initialize()
{
foreach (DEVICE_ACTION_REQUEST Action in Enum.GetValues(typeof(DEVICE_ACTION_REQUEST)))
EventDispatcher<DEVICE_ACTION_REQUEST>.CreateEvent(Action);
}
これにより、列挙型内の各イベントのイベント ハンドラーが作成されます。
そして、消費する Device オブジェクトのコンストラクターで次のコードのようにイベントをサブスクライブすることで消費されます。
public DeviceController( )
{
EventDispatcher<DEVICE_ACTION_REQUEST>.Subscribe(DEVICE_ACTION_REQUEST.LoadAxisDefaults, (s, e) =>
{
InControlThread.Invoke(this, () =>
{
ReadConfigXML(s, (EventArgs<string>)e);
});
});
}
InControlThread.Invoke は、呼び出し呼び出しを単にラップする抽象クラスです。
イベントは GUI で次のように簡単に発生させることができます。
private void buttonLoad_Click(object sender, EventArgs e)
{
string Filename = @"c:\test.xml";
EventDispatcher<DEVICE_ACTION_REQUEST>.Fire(DEVICE_ACTION_REQUEST.LoadStuffFromXMLFile, sender, new EventArgs<string>(Filename));
}
これには、イベントの発生型と消費型が一致しない場合 (ここでは文字列 Filename)、コンパイラが不平を言うという利点があります。
できる強化はたくさんありますが、これが問題の核心です。コメントでも言ったように、特に明らかな省略/バグや欠陥がある場合は興味があります。これが誰かの役に立てば幸いです。