C#と諸々

C#がメインで他もまぁ諸々なブログです
おかしなこと書いてたら指摘してくれると嬉しいです(´・∀・`)
つーかコメント欲しい(´・ω・`)

2006/10/13 02:17
通常、Windowsアプリでは、イベントハンドラ内で重い処理を行う際は、フォームの一時的なハングを防ぐために重い処理を別スレッドで行う。
その時気をつけなければならないのが、コントロールの操作。コントロールは、コントロール (  の基になるウィンドウハンドル ) を所有しているスレッドで操作しなければならない。別スレッド内でそのままコントロールを操作してしまうと、正常に動作してくれない。 ( .NET 2.0から特に。 )
では、別スレッドで重い処理を行ったあと、どうやってコントロールを所有しているスレッドに処理を行わせるか?それには、Control.Invokeメソッドを使用する。Control.Invokeは、引数で受け取ったデリゲートオブジェクトを、そのコントロールを所有しているスレッド上で実行するメソッドである。

Contorol.Invokeの第一引数はDelegate型である。この引数に渡すためのデリゲートには、System.Windows.Forms名前空間にある、MethodInvokerデリゲートというデリゲートを使うのがいい 。MethodInvokerには、引数も戻り値もない。このデリゲートを使えば、Invoke用にわざわざデリゲートを自作する必要がなくなる。MSDNにもはっきりと、 「 簡単なデリゲートが必要だが定義するのが面倒な場合に使用できます。 」 と書かれている。

C#2.0の新機能である匿名メソッドを用いると、これら一連の処理が ( わりと ) 見やすくかける。匿名メソッドによって、無駄にメソッドを定義する必要がなくなり、デリゲートが扱いやすくなった。 ( 匿名メソッドはデリゲートとして扱えるが、直接Delegateクラスに変換することはできないことに注意。 )

先に述べたとおり、Control.Invokeメソッドはコントロール ( の基になるウィンドウハンドル ) を所有しているスレッドに処理を行わせる。従って、もし、重たい処理を行っている最中にフォームが閉じられたりして、ウィンドウハンドルが破棄されてしまうと、その後に呼び出されるControl.InvokeメソッドがInvalidOperationExceptionをスローすることになる。
重たい処理中に、フォームが閉じられてしまう可能性がある場合は、Control.IsHandleCreatedプロパティを利用し、コントロールにハンドルが関連付けられているかどうかを判別する必要がある。

以下に、重い処理を別スレッドで行い、処理の前後でフォーム上の各コントロールのEnabledプロパティを切り替えるサンプルコードを示す。

サンプル1// Form1.cs
private void button1_Click(object sender, EventArgs e)
{
    this.SetEnabledOfControls(false);
    Thread process = new Thread(
        delegate()
        {
            Thread.Sleep(5000); // 重たい処理の代用

            if (this.IsHandleCreated)
            {
                this.Invoke(
                    (MethodInvoker)delegate()
                    {
                        this.SetEnabledOfControls(true);
                    });
            }
        });
    process.Start();
}

private void SetEnabledOfControls(bool value)
{
    // 各コントロールのEnabledプロパティにvalueを設定する処理
}


コントロール操作を行うメソッド内でInvokeメソッドを利用するという手もある。その場合、コントロール操作を行うメソッドが別スレッド上で実行されるとは限らないので、
Control.InvokeRequiredプロパティを利用し、現在のスレッドがコントロールの所有スレッドなのかどうかを判別する。
以下にサンプルを示す。

サンプル2// Form1.cs
private void button1_Click(object sender, EventArgs e)
{
    this.SetEnabledOfControls(false);
    Thread process = new Thread(
        delegate()
        {
            Thread.Sleep(5000); // 重たい処理の代用
            this.SetEnabledOfControls(true);
        });
    process.Start();
}

private void SetEnabledOfControls(bool value)
{
    if (!(this.IsHandleCreated))
    {
        return;
    }
    MethodInvoker process =
        (MethodInvoker)delegate()
        {
            // 各コントロールのEnabledプロパティにvalueを設定する処理
        };
    if (this.InvokeRequired)
    {
        this.Invoke(process);
    }
    else
    {
        process.Invoke();
    }
}


以上2つの方法の内どちらを採用するにしろ、コントロール操作の度にControl.IsHandleCreatedプロパティとControl.InvokeRequiredプロパティ ( これは一つ目の方法だと不要 ) のチェックを行ってControl.Invokeメソッドを呼び出すのでは、コードがやや冗長である。
以下のように、静的クラスを用意して、静的メソッドとして定義してやると良い。

public static class ControlUtil
{
    public static object SafelyOperated(Control context, Delegate process)
    {
        return ControlUtil.SafelyOperated(context, process, null);
    }

    public static object SafelyOperated(Control context, Delegate process, params object[] args)
    {
        if (context == null)
        {
            throw new ArgumentNullException("context");
        }
        if (process == null)
        {
            throw new ArgumentNullException("process");
        }

        if (!(context.IsHandleCreated))
        {
            return null;
        }
        if (context.InvokeRequired)
        {
            return context.Invoke(process, args);
        }
        else
        {
            return process.DynamicInvoke(args);
        }
    }
}


この静的メソッドは、以下のように利用する。 ( Control.Invokeメソッドと同じく、引数を指定したり、戻り値を取得することも可能。 )

SafelyOperatedメソッドを用いてサンプル2のSetEnabledOfControlsメソッドを記述private void SetEnabledOfControls(bool value)
{
    MethodInvoker process =
        (MethodInvoker)delegate()
        {
            // 各コントロールのEnabledプロパティにvalueを設定する処理
        };
    ControlUtil.SafelyOperated(this, process);
}