WindowsForms の ActiveControl に関する動き

先日 WindowsFormsで作成したアプリケーションで、ちょっとした意図しない現象に合いました。その意図しない現象について確認した内容についてメモを残しておきます。

## 発生した現象

2つのウィンドウがあります。一つをメイン画面、サブ画面とします。
サブ画面からメイン画面にある Control (ボタンなど）を削除する処理を実行すると、アクティブな画面がサブ画面からメイン画面に切り替わります。

文字だけだと分からないと思いますので、簡易版のアプリケーションを作り再現しました。
Form2 がサブ画面で Form1 がメイン画面だと思って下さい。
Form2 から Form１ のボタンを Dispose しています。 Dispose 後、Form1 の画面が前面にきていることが分かります。「意図しない現象」というのはメイン画面が前面にくる現象のことです。
![dispose_1_active_1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960076/f0376460-23eb-bdb1-41ff-6aad2b197e13.gif)


## 再現用のコード

この現象を再現するためのサンプルプログラムは下記の通りです。なお、事前にVisual Studio 側のデザインで、フォーム1とフォーム2にそれぞれボタンをセットしておきます。
フォーム1（メイン画面）
![WindowsFormsApp1 - Microsoft Visual Studio 2023-07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960076/9783e24b-e26f-1c2c-5223-28111f2948d8.png)

フォーム2（サブ画面）
![WindowsFormsApp1 - Microsoft Visual Studio 2023-07_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960076/7a2e7f2a-e3b2-2c6e-856d-3fdd2eafb915.png)


フォーム1のコードです。DisposeTarget1() がフォーム2 から呼ばれて button2 を削除していることが分かっていただければ十分です。
その他のコードについては、後程出てくる Controls の数と Dispose 対象の関係性の調査のためのコードになりますので流し読みしてください。

```Form1.cs
using System;
using System.Windows.Forms;

namespace WindowsFormsApp1
{
    public partial class Form1 : Form
    {
        private Form2 form2;

        public Form1()
        {
            InitializeComponent();
            // フォーム１の一つめのボタンを作成
            button1.Text = "フォーム２を表示";
            button1.Location = new System.Drawing.Point(10, 10);
            button1.Click += Button1_Click;
            Controls.Add(button1);

            button2.Text = "Target 1";
            button2.Location = new System.Drawing.Point(10, 40);
            Controls.Add(button2);

            button3.Text = "Target 2";
            button3.Location = new System.Drawing.Point(10, 70);
            Controls.Add(button3);
        }

        public void DisposeTarget1()
        {
            button2.Dispose();
        }

        public void DisposeTarget2()
        {
            button3.Dispose();
        }

        public void SetActiveControlNull()
        {
            ActiveControl = null;
        }

        // Controls にある別の Control に ActiveControl を移す
        // 例えば今が button1 なら 次は button2 に移す
        // Controls の最後の要素まで切り替わったら最初の要素に戻す
        public void ChangeActiveControl()
        {
            var index = Controls.IndexOf(ActiveControl);

            if (index == -1)
            {
                ActiveControl = Controls[0];
            }
            else
            {
                index++;
                if (index >= Controls.Count)
                {
                    index = 0;
                }
                ActiveControl = Controls[index];
            }
        }

        // button を追加する
        public void AddControl()
        {
            var button = new Button();
            button.Text = "追加したボタン";
            button.Location = new System.Drawing.Point(500, 10);
            Controls.Add(button);
        }

        private void Button1_Click(object sender, EventArgs e)
        {
            button1.Dispose();

            // フォーム２を表示
            form2 = new Form2();
            form2.Show();
        }
    }
}
```

フォーム2のコード
```Form2.cs
using System;
using System.Windows.Forms;

namespace WindowsFormsApp1
{
    public partial class Form2 : Form
    {
        public Form2()
        {
            InitializeComponent();
            // フォーム２のボタンを作成
            button1.Text = "Dispose Target 1";
            button1.Location = new System.Drawing.Point(10, 10);
            button1.Click += DisposeTarget1;
            Controls.Add(button1);

            button2.Text = "Dispose Target 2";
            button2.Location = new System.Drawing.Point(10, 40);
            button2.Click += DisposeTarget2;
            Controls.Add(button2);

            button3.Text = "ActiveControl = null";
            button3.Location = new System.Drawing.Point(10, 70);
            button3.Click += DisposeAfterSettingActiveControlNull;
            Controls.Add(button3);

            button4.Text = "ChangeActiveFocus";
            button4.Location = new System.Drawing.Point(10, 100);
            button4.Click += ChangeActiveFocus;
            Controls.Add(button4);

            button5.Text = "AddControl";
            button5.Location = new System.Drawing.Point(10, 130);
            button5.Click += AddControl;
            Controls.Add(button5);
        }

        private Form1 Form1 => (Form1)Application.OpenForms["Form1"];

        private void DisposeTarget1(object sender, EventArgs e)
        {
            Form1?.DisposeTarget1();
        }

        private void DisposeTarget2(object sender, EventArgs e)
        {
            Form1?.DisposeTarget2();
        }

        private void DisposeAfterSettingActiveControlNull(object sender, EventArgs e)
        {
            Form1?.SetActiveControlNull();
        }

        private void ChangeActiveFocus(object sender, EventArgs e)
        {
            Form1?.ChangeActiveControl();
        }

        private void AddControl(object sender, EventArgs e)
        {
            Form1?.AddControl();
        }
    }
}
```

## Dispose 前後で ActiveForm はどうなっているか確認する

アクティブなフォームが基本的には全面に出てくると思います。ということで Form.ActiveForm の値が Dispose 前後でどのようになっているかデバッガで確認しました。

Dispose 前は ActiveForm は Form2（サブ画面）であることが分かります。これは意図通りです。
![before.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960076/33989fc8-ea79-118b-7044-bc8341d3c6fa.png)

Dispose 後は ActiveForm は null になることが分かりました。これは想定外でした。
![after.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960076/ce1270c8-f7fb-ce13-49d2-066402c3242b.png)

この現象がなぜ発生するかの根拠となるドキュメントは見つけられませんでした。（感覚的には理解できる気もしますが）とりあえず、Control の Dispose が行われると Form.ActiveForm が null 値になる仕様のようです。

## Controls の数と Dispose 対象が ActiveControl かどうかで動作が決まる

引き続き調査を行っているとフォームが持つ Controls の数と Dispose 対象が ActiveControl かどうかで動作が決まることが分かってきました。先にまとめです。

次の条件を満たすときに dispose される Control をもつフォームが前面にきます。
1. Form.Controls の数が2つ以上のとき（ActiveControlが移る先があるとき)
2. Dispose される Control が ActiveControl にセットされている

![まとめ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960076/da23cbbe-cb73-66c3-fe11-9ecb0bbf8f7a.png)



ここからは実際の確認の Gif を載せていきます。すべてに共通して言えることですが、なぜそうなるかは確信が持てるドキュメントが見つからなかったので書いていません。書いてある原因については私自身の予想になります。

#### 既存の Control へ ActiveControl を切り替える

ActiveControl を切り替えるだけで、サブ画面からメイン画面に切り替わるのか確認しました。
画面は切り替わりませんでした。

![a.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960076/5fd993be-56d2-6249-d5c8-bc3e2a7095e1.png)

![change_active_focus.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960076/9940fa99-6de0-11e3-b66a-11dc161b5636.gif)


#### 新しい Control を追加する

新しい Control （今回はボタン）を追加した場合にサブ画面からメイン画面に切り替わるのか確認しました。
画面は切り替わりませんでした。

![b.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960076/f4a686fd-ce40-3d81-c919-8293aa682507.png)

![add_button.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960076/83eba9a2-4468-0c96-20f6-a03363bb4fa4.gif)


#### Controls の数と ActiveControl と Dispose する対象

次の条件を満たすときに dispose される Control をもつフォームが前面にきました。
1. Form.Controls の数が2つ以上のとき（ActiveControlが移る先があるとき)
2. Dispose される Control が ActiveControl にセットされている

![まとめ2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960076/43a9ee56-1a94-5a2f-6ea9-29ad61c29b5c.png)

以下に再現の様子を示します。なお、ボタン違い（Controls に格納されている順番）による差分は出なかったのでボタン違いの結果については割愛します

##### Controls が1つ で ActiveControl が null のとき

メイン画面に切り替わりませんでした。
![dispose_null.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960076/baedb4f0-1c37-4026-d673-40797761093e.gif)

##### Controls が1つで ActiveControl の Control を Dispose するとき

メイン画面にきりかわりませんでした。
![dispose_1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960076/7c1b4f22-5dd9-bdda-47ba-d3a895e69c0e.gif)

##### Controls が2つで ActiveControl が null のとき

メイン画面に切り替わりませんでした。
![dispose_1_null.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960076/26320220-a46c-5810-ffde-76c28a9ed164.gif)


##### Controls が2つで ActiveControl の Control を Dispose するとき

**メイン画面に切り替わりました。**
![dispose_1_active_1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960076/0c7bcfb6-2204-dae9-2b0b-34bc7de2262e.gif)


##### Controls が2つで ActiveControl でない Control を Dispose するとき

メイン画面に切り替わりませんでした。
![dispose_1_active_2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960076/d6b85d45-570d-fce9-c9e2-91066df9d5ea.gif)

## 本件に対する対応

次の条件を満たすときに dispose される Control をもつフォームが前面にくることが分かりました。
1. Form.Controls の数が2つ以上のとき（ActiveControlが移る先があるとき)
2. Dispose される Control が ActiveControl にセットされている

この件の対策として、Dispose する直前で ActiveControl を null にして、Dispose するようにしました。これによりメイン画面に切り替わることはなくなりました。ただし、既存の動きでは、Dispose 後に別の Control に ActiveControl が切り替わるのに対し、Dispose 直前に ActiveControl を null 値にする場合は、Dispose 後も ActiveControl が null のままとなるので注意が必要です。アプリケーションの要件に合わせて別途、ActiveControl をセットし直すなどの処理が必要になると思います。

## 感想

単純に面白い結果だなと思いました。また、再現するためのデスクトップアプリケーションが簡単に作れてしまう WindowsForms(.Net) は本当にすごいなと感じました。