Help us understand the problem. What is going on with this article?

C# 別のフォームのコントロールを操作する InvaildOperationExceptionはなぜ発生するのか?

前置き

「別のフォームから、メインフォーム(他)のコントロールを操作したい!」
ってことありませんか?

例えば、Form2のボタンをクリックするとForm1のラベルの文字を変更するようにしたい時。
image.png

ここで「ラベルのアクセス修飾子をPublicにして、インスタンス生成して変更してやればいいんじゃないの?」と考えるかもしれません。
ですが、それではダメです。
実際に、やってみると...

sld0k-ax4m2.gif

変わりません。※注1
なぜでしょうか?

ここで、Form1の非同期メソッドから、ラベルの文字を変更してみます。

private async void button1_Click(object sender, EventArgs e)
{
    await Task.Run(() => 
    {
        label1.Text = "非同期実行中";
    });
}

ewne2-ccfxm.gif

落ちてしまいました。
発生している例外は、InvaildOperationExceptionですね。


コメント欄よりご指摘より追記:

※注1... インスタンスは存在していますが、Show()していないゴーストフォーラムに対してラベルの変更命令を送りつけているのが原因です。
Form1のインスタンスを受け取り、そのインスタンスに対してラベルの変更命令を送るのが正しい例です。


原因

上記2つの例
・ラベルをインスタンスから参照
・非同期関数から参照
どちらにも共通している点があります。

それぞれ別のスレッドで動作しているということです。
別スレッド同士で無効な参照を行っていたのが原因です。

crossthread.png

解決方法

ここでは、2つの解決方法を紹介します。

・デリゲートの定義/呼び出し
・メソッドのInvoke

デリゲートを定義して呼び出し

Form1にデリゲートを定義して、該当デリゲートをCallする方法です。

Form1.cs
public partial class Form1 : Form
{
    //デリゲートの定義
    public delegate void ChangeTextDelegate(String text);
    public ChangeTextDelegate del;

    //コンストラクタ
    public Form1()
    {
        InitializeComponent();
        //ハンドルが作成されていなければ作成。Invokeする際に必要です。
        if (!IsHandleCreated) CreateHandle();
        //デリゲートに委任する処理(関数)を定義。
        del = ChangeText;
    }

    private void Form1_Load(object sender, EventArgs e)
    {
        //Form2のインスタンスを定義し、自身のインスタンスを渡します。
        Form2 form2 = new Form2(this);
        form2.Show();
    }

    private async void button1_Click(object sender, EventArgs e)
    {
        await Task.Run(() => 
        {
            //label1.Text = "非同期実行中"; だめ
            //del("sometext"); これもだめ
        });
    }

    public void ChangeText(String text)
    {
        label1.Text = (text);
    }
}
Form2.cs
public partial class Form2 : Form
{
    Form1 form1;
    public Form2(Form1 __form1)
    {
        InitializeComponent();
        if (!IsHandleCreated) CreateHandle();
        form1 = __form1;
    }

    private void button1_Click(object sender, EventArgs e)
    {
        //form1.label1.Text = "something"; ok

        //form1.Invoke(form1.del, "sometext"); ok | Invokeでもデリゲートを呼べる
        form1.del("sometext"); //ok
    }
}

メソッドをInvoke

Form1のインスタンスから単純に関数をInvokeする方法です。

public partial class Form1 : Form
{
    //コンストラクタ
    public Form1()
    {
        InitializeComponent();
        //ハンドルが作成されていなければ作成。Invokeする際に必要です。
        if (!IsHandleCreated) CreateHandle();
    }

    private void Form1_Load(object sender, EventArgs e)
    {
        //Form2のインスタンスを定義し、自身のインスタンスを渡します。
        Form2 form2 = new Form2(this);
        form2.Show();
    }

    private async void button1_Click(object sender, EventArgs e)
    {
        await Task.Run(() => 
        {
            //label1.Text = "非同期実行中"; だめ
            //del("sometext"); これもだめ
        });
    }

    public void ChangeText(String text)
    {
        label1.Text = (text);
    }
}
public partial class Form2 : Form
{
    Form1 form1;
    public Form2(Form1 __form1)
    {
        InitializeComponent();
        if (!IsHandleCreated) CreateHandle();
        form1 = __form1;
    }

    private void button1_Click(object sender, EventArgs e)
    {
        //form1.label1.Text = "something"; ok

        //関数をInvoke
        form1?.Invoke(new Action<String>(form1.ChangeTextA), "sometext");
    }
}

Asyncからの呼び出し

asyncからの呼び出しも、デリゲートをInvokeするか、関数を直接Invokeすることで
ラベルの文字列を変更できます。
注意すべき点は、デリゲートを直接呼ぶことはできないということです。
asyncは別スレッドで実行される為、Form1で定義されている変数delにアクセスできないからです。

private async void button1_Click(object sender, EventArgs e)
{
    await Task.Run(() => 
    {
        //label1.Text = "非同期実行中"; だめ
        //del("sometext"); これもだめ | デリゲートは直接呼べない
        this.Invoke(del, "sometext"); //ok
        this.Invoke(new Action<String>(ChangeText), "sometext2"); //ok
    });
}

メリット・デメリット

・デリゲート経由での呼び出し
→冗長なコードになりがち。

・Invoke
→最小限のコードで済む。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした