33

winform のクロススレッド更新規則にひどく違反していた古いアプリケーションのメンテナンス中に、不正な呼び出しを発見したときにすばやく修正する方法として、次の拡張メソッドを作成しました。

/// <summary>
/// Execute a method on the control's owning thread.
/// </summary>
/// <param name="uiElement">The control that is being updated.</param>
/// <param name="updater">The method that updates uiElement.</param>
/// <param name="forceSynchronous">True to force synchronous execution of 
/// updater.  False to allow asynchronous execution if the call is marshalled
/// from a non-GUI thread.  If the method is called on the GUI thread,
/// execution is always synchronous.</param>
public static void SafeInvoke(this Control uiElement, Action updater, bool forceSynchronous)
{
    if (uiElement == null)
    {
        throw new ArgumentNullException("uiElement");
    }

    if (uiElement.InvokeRequired)
    {
        if (forceSynchronous)
        {
            uiElement.Invoke((Action)delegate { SafeInvoke(uiElement, updater, forceSynchronous); });
        }
        else
        {
            uiElement.BeginInvoke((Action)delegate { SafeInvoke(uiElement, updater, forceSynchronous); });
        }
    }
    else
    {
        if (!uiElement.IsHandleCreated)
        {
            // Do nothing if the handle isn't created already.  The user's responsible
            // for ensuring that the handle they give us exists.
            return;
        }

        if (uiElement.IsDisposed)
        {
            throw new ObjectDisposedException("Control is already disposed.");
        }

        updater();
    }
}

使用例:

this.lblTimeDisplay.SafeInvoke(() => this.lblTimeDisplay.Text = this.task.Duration.ToString(), false);

クロージャを活用して読み取る方法も気に入っていますが、その場合は forceSynchronous を true にする必要があります。

string taskName = string.Empty;
this.txtTaskName.SafeInvoke(() => taskName = this.txtTaskName.Text, true);

レガシー コードの不正な呼び出しを修正するためのこのメソッドの有用性については疑問ではありませんが、新しいコードについてはどうでしょうか。

UI を更新しようとしているスレッドがわからない場合に、このメソッドを使用して新しいソフトウェアの UI を更新するのは良い設計ですか? または、新しい Winforms コードには通常、適切なInvoke()関連の配管を備えた特定の専用メソッドが含まれている必要があります。そのようなすべてのUIの更新?(もちろん、BackgroundWorker など、他の適切なバックグラウンド処理手法を最初に使用しようとします。)

興味深いことに、これはToolStripItemsでは機能しません。最近、それらがControlではなくComponentから直接派生していることを発見しました。代わりに、包含の呼び出しを使用する必要があります。ToolStrip

コメントへのフォローアップ:

一部のコメントは、次のことを示唆しています。

if (uiElement.InvokeRequired)

次のようにする必要があります。

if (uiElement.InvokeRequired && uiElement.IsHandleCreated)

次のmsdn ドキュメントを検討してください。

つまり、InvokeRequired は 、Invoke が必要でない (呼び出しが同じスレッドで発生する)場合、またはコントロールが別のスレッドで作成されたが、コントロールのハンドルがまだ作成されていない場合に、 false を返すことができます。

コントロールのハンドルがまだ作成されていない場合は、コントロールのプロパティ、メソッド、またはイベントを単純に呼び出すべきではありません。これにより、コントロールのハンドルがバックグラウンド スレッドで作成され、メッセージ ポンプのないスレッドでコントロールが分離され、アプリケーションが不安定になる可能性があります。

バックグラウンド スレッドで InvokeRequired が false を返したときに IsHandleCreated の値も確認することで、このケースを防ぐことができます。

コントロールが別のスレッドで作成されたが、コントロールのハンドルがまだ作成されていない場合は、 InvokeRequiredfalse を返します。これは、 if InvokeRequiredreturns true,IsHandleCreatedが常に true になることを意味します。再度テストすることは冗長であり、正しくありません。

4

3 に答える 3

11

Begin および End 拡張メソッドも作成する必要があります。また、ジェネリックを使用すると、呼び出しの見栄えを少し良くすることができます。

public static class ControlExtensions
{
  public static void InvokeEx<T>(this T @this, Action<T> action)
    where T : Control
  {
    if (@this.InvokeRequired)
    {
      @this.Invoke(action, new object[] { @this });
    }
    else
    {
      if (!@this.IsHandleCreated)
        return;
      if (@this.IsDisposed)
        throw new ObjectDisposedException("@this is disposed.");

      action(@this);
    }
  }

  public static IAsyncResult BeginInvokeEx<T>(this T @this, Action<T> action)
    where T : Control
  {
    return @this.BeginInvoke((Action)delegate { @this.InvokeEx(action); });
  }

  public static void EndInvokeEx<T>(this T @this, IAsyncResult result)
    where T : Control
  {
    @this.EndInvoke(result);
  }
}

これで、呼び出しが少し短くなり、きれいになります。

this.lblTimeDisplay.InvokeEx(l => l.Text = this.task.Duration.ToString());

var result = this.BeginInvokeEx(f => f.Text = "Different Title");
// ... wait
this.EndInvokeEx(result);

sに関してはComponent、フォームまたはコンテナー自体で呼び出すだけです。

this.InvokeEx(f => f.toolStripItem1.Text = "Hello World");
于 2009-04-03T17:02:37.683 に答える
5

全体的な考え方は気に入っていますが、1 つ問題があります。EndInvokes を処理することが重要です。そうしないと、リソース リークが発生する可能性があります。多くの人がこれを信じていないことは知っていますが、これは本当です。

これについて話している1つのリンクがあります。他にもあります。

しかし、私が持っている主な反応は次のとおりです。はい、ここでいいアイデアを思いついたと思います。

于 2009-04-03T16:25:47.070 に答える
0

これは実際には回答ではありませんが、受け入れられた回答に対するいくつかのコメントに回答します。

標準IAsyncResultパターンの場合、BeginXXXメソッドにはAsyncCallbackパラメーターが含まれているため、「これはどうでもいい。完了したら EndInvoke を呼び出して、結果を無視するだけ」と言いたい場合は、次のようにすることができます (これは目的ですActionが、他のデリゲート タイプに合わせて調整できます):

    ...
    public static void BeginInvokeEx(this Action a){
        a.BeginInvoke(a.EndInvoke, a);
    }
    ...
    // Don't worry about EndInvoke
    // it will be called when finish
    new Action(() => {}).BeginInvokeEx(); 

(残念ながら、このパターンを使用するたびに変数を宣言せずにヘルパー関数を持たないという解決策はありません)。

しかし、Control.BeginInvoke私たちは を持っていないのでAsyncCallBack、これをControl.EndInvoke確実に呼び出されるように表現する簡単な方法はありません。それが設計されている方法は、オプションであるという事実を促しControl.EndInvokeます。

于 2014-01-30T05:54:16.763 に答える