C#定石 - モーダルダイアログに対する最小化の辻褄あわせ

## はじめに

C# ソフト開発時に、決まり事として実施していた内容を記載します。

:::note
本情報について、Windows Forms は Form クラス、WPF は Window クラスなどの違いがあり、双方を併記すると訳が分からなくなりそうなので、以降は Windows Forms ベースでの記載とします。

WPF に関しては、Windows Forms サンプルコードと同等の対応をしたサンプルコードのみを記載します。 
:::

### テスト環境

ここに記載した情報/ソースコードは、Visual Studio Community 2022 を利用した下記プロジェクトで生成したモジュールを Windows 11 24H2 で動作確認しています。
- Windows Forms - .NET Framework 4.8
- Windows Forms - .NET 8
- WPF - .NET Framework 4.8
- WPF - .NET 8

::: note
Visual Studio 2022 - .NET Framework 4.8 は、C# 7.3 が既定です。
このため、サンプルコードは、C# 7.3 機能範囲で記述しています。
:::

## モーダルダイアログの最小化

Windows Forms において、メインフォームから Form.ShowDialog で、モーダルダイアログ表示したサブフォームを最小化した場合、サブフォームのみ最小化して、メインフォームは何の操作もできないのに、表示されたままという残念な挙動になっています。

:::note
Form.Show を用いた場合は、複数サブフォームを同時表示できるので、ひとつのサブフォームを最小化しても、メインフォームの表示/非表示に影響を与えないのは当然の動作だと思います。

状況は異なりますが、Form.Show した場合、メインフォームの最小化で、サブフォーム（複数でも）を連動して非表示にしてくれてます。
このようなフォーム同士の関係性に基づいた挙動を Form.ShowDialog したサブフォームの最小化でも、取り入れてくれたら良かったと思うんですけど、、、
:::

### 対応方法

この残念な挙動の改善策として、いくつかの手法が考えられると思いますが、下記アプローチで考えることとします。

- サブフォームの SizeChanged イベントハンドラ
  - サブフォームが最小化されたら、メインフォームを Visible = false にして非表示とします。
  - サブフォームが最小化から復帰したら、メインフォームを Visible = ture にして表示します。
- サブフォームの FormClosed イベントハンドラ
  - タスクバー上、対象アプリのアイコンをマウスオーバーするとプレビュー画面が表示されて、プレビュー画面右端の X で対象フォームのクローズができてしまいます。<br>前述 SizeChanged でサブフォームの最小化に連動して、メインフォームが Visible = false となった状態で、サブフォームをクローズするとメインフォームは非表示のままです。<br>そこで FromClosed 時に、メインフォームを Visible = ture にして表示させます。

Form1 → Form2 → Form3 と多階層にサブフォームを Form.ShowDialog するケースまで考えると、上記に加えて、下記対処も必要となります。

- サブフォームの VisibleChanged イベントハンドラ
  - 前述 SizeChanged の対処をして、Form3 を最小化すると、Form3.SizeChanged で Form2 は Visible = false となりますが、Form2.SizeChanged イベントは発生せず、 Form1 はそのままとなります。<br>Form1 まで玉突き非表示とするには、Form2.VisibleChanged イベントハンドラで、Form1.Visible = Form2.Visible とする必要があります。

:::note
上記、対応方法は、Form.ShowDialog のみ利用で、Form.Show が対象ソース上にないという前提のものです。
:::

### 実装上のポイント１

Form1 から Form2 を Form.ShowDialog した場合、現時点では Form2 プロパティの Owner, Parent は null となっています。
Form2 から Form1 を操作させるために、呼び出し元の Form1 で Form2.Owner に Form1 をセットします。

```CSharp
var subForm = new Form2();
subForm.Owner = this;     // これを忘れずに !!
DialogResult result = subForm.ShowDialog();
subForm.Dispose();
```

:::note
「現時点では Form2 プロパティの Owner, Parent は null となっています」と、スッキリしない文面としています。
このようにした理由は、ひと昔前まで、 Form1 から Form2 を Form.ShowDialog した場合、Form2.Owner = Form1 が自動的にセットされていた気がするからです。
どこかの時点からセットされなくなったと思うのですが、今となっては確かめようがありません、、、
:::

### 実装上のポイント２

FormClosed イベント後に発生する VisibleChanged イベントでは、今回の対応を動作させたくないので、FormClosed イベントハンドラで CurrentFormClosed = true をセットします。

```CSharp
private bool CurrentFormClosed = false;  // FormClosed イベント後かの判断値

// VisibleChanged イベントハンドラ
private void Form2_VisibleChanged(object sender, EventArgs e)
{
  // this.Owner が Form でないならば以降の処理はしない
  if (this.Owner is Form owner)
  {
    // FormClosed イベント後に発生する VisibleChanged イベントでは処理しない
    if (!CurrentFormClosed)
    {
      owner.Visible = this.Visible;
    }
  }
}

// FormClosed イベントハンドラ
private void Form2_FormClosed(object sender, FormClosedEventArgs e)
{
  // this.Owner が Form でないならば以降の処理はしない
  if (this.Owner is Form owner)
  {
    owner.Visible = true;
  }
  // FormClosed イベント後に発生する VisibleChanged イベントで
  // 親フォームに対する処理を抑止するため CurrentFormClosed 更新
  CurrentFormClosed = true;
}
```

## サンプルコード

### Windows Forms - .NET Framework 4.8 / .NET 8

Form1 → Form2 → Form3 と Form.ShowDialog するサンプルコードです。
.NET Framework 4.8 と .NET8 で同一コードとなります。

https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.forms.form.showdialog

```CSharp:Form1.cs
private void DoAction()
{
  // サブフォームをモーダルダイアログ表示
  var subForm = new Form2();
  subForm.Owner = this;
  DialogResult result = subForm.ShowDialog();
  subForm.Dispose();
}
```

```CSharp:Form2.cs
private void DoAction()
{
  // サブフォームをモーダルダイアログ表示
  var subForm = new Form3();
  subForm.Owner = this;
  DialogResult result = subForm.ShowDialog();
  subForm.Dispose();
}

// SizeChanged イベントハンドラ
private void Form2_SizeChanged(object sender, EventArgs e)
{
  // this.Owner が Form でないならば以降の処理はしない
  if (this.Owner is Form owner)
  {
    if (this.WindowState == FormWindowState.Minimized)
    {
      // 最小化　→ 呼び出し元を非表示
      owner.Visible = false;
    }
    else
    {
      owner.Visible = true;
    }
  }
}

// VisibleChanged イベントハンドラ
private void Form2_VisibleChanged(object sender, EventArgs e)
{
  // this.Owner が Form でないならば以降の処理はしない
  if (this.Owner is Form owner)
  {
    // FormClosed イベント後に発生する VisibleChanged イベントでは処理しない
    if (!CurrentFormClosed)
    {
      owner.Visible = this.Visible;
    }
  }
}

// FormClosed イベントハンドラ
private void Form2_FormClosed(object sender, FormClosedEventArgs e)
{
  // this.Owner が Form でないならば以降の処理はしない
  if (this.Owner is Form owner)
  {
    owner.Visible = true;
  }
  // FormClosed イベント後に発生する VisibleChanged イベントで
  // 親フォームに対する処理を抑止するため CurrentFormClosed 更新
  CurrentFormClosed = true;
}
```

```CSharp:Form3.cs
// SizeChanged イベントハンドラ
private void Form3_SizeChanged(object sender, EventArgs e)
{
  // this.Owner が Form でないならば以降の処理はしない
  if (this.Owner is Form owner)
  {
    if (this.WindowState == FormWindowState.Minimized)
    {
      // 最小化　→ 呼び出し元を非表示
      owner.Visible = false;
    }
    else
    {
      owner.Visible = true;
    }
  }
}

// VisibleChanged イベントハンドラ
private void Form3_VisibleChanged(object sender, EventArgs e)
{
// Form3 から ShowDialog していないので処理不要
// // this.Owner が Form でないならば以降の処理はしない
// if (this.Owner is Form owner)
// {
//   // FormClosed イベント後に発生する VisibleChanged イベントでは処理しない
//   if (!CurrentFormClosed)
//   {
//     owner.Visible = this.Visible;
//   }
// }
} 

// FormClosed イベントハンドラ
private void Form3_FormClosed(object sender, FormClosedEventArgs e)
{
  // this.Owner が Form でないならば以降の処理はしない
  if (this.Owner is Form owner)
  {
    owner.Visible = true;
  }
  // FormClosed イベント後に発生する VisibleChanged イベントで
  // 親フォームに対する処理を抑止するため CurrentFormClosed 更新
  CurrentFormClosed = true;
}
```


### WPF - .NET Framework 4.8

MainWindow → Window1 → Window2 と Window.ShowDialog するサンプルコードです。

https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.window.showdialog


```CSharp:MainWindow.xaml.cs
private void DoAction()
{
  // サブフォームをモーダルダイアログ表示
  var subForm = new Window1();
  subForm.Owner = this;
  bool? dialogResult = subForm.ShowDialog();
}
```

```CSharp:Window1.xaml.cs
public Window1()
{
  InitializeComponent();

  StateChanged += Window1_StateChanged;
  Closed += Window1_Closed;
  IsVisibleChanged += Window1_IsVisibleChanged;
}

private void DoAction()
{
  // サブフォームをモーダルダイアログ表示
  var subForm = new Window2();
  subForm.Owner = this;
  bool? dialogResult = subForm.ShowDialog();
}

private bool CurrentFormClosed = false;

// .NET Framework - SizeChanged イベントハンドラの代替
private void Window1_StateChanged(object sender, System.EventArgs e)
{
  // this.Owner が Window でないならば以降の処理はしない
  if (this.Owner is Window owner)
  {
    if (this.WindowState == WindowState.Minimized)
    {
      // 最小化　→ 呼び出し元を非表示
      owner.Visibility = Visibility.Collapsed;
    }
    else if (this.Owner.Visibility == Visibility.Collapsed)
    {
      owner.Visibility = Visibility.Visible;
    }
  }
}

// .NET Framework - VisibleChanged イベントハンドラ相当
private void Window1_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
{
  // this.Owner が Window でないならば以降の処理はしない
  if (this.Owner is Window owner)
  {
    if (!CurrentFormClosed)
    {
      if (this.Visibility == Visibility.Visible)
      {
        if (owner.Visibility == Visibility.Collapsed)
        {
          owner.Visibility = Visibility.Visible;
        }
      }
      else if (this.Visibility == Visibility.Collapsed)
      {
        owner.Visibility = Visibility.Collapsed;
      }
    }
  }
}

// .NET Framework - FormClosed イベントハンドラ相当
private void Window1_Closed(object sender, System.EventArgs e)
{
  // this.Owner が Window でないならば以降の処理はしない
  if (this.Owner is Window owner)
  {
    if (this.Owner.Visibility == Visibility.Collapsed)
    {
      owner.Visibility = Visibility.Visible;
    }
  }
  CurrentFormClosed = true;
}
```

```CSharp:Window2.xaml.cs
public Window2()
{
  InitializeComponent();

  StateChanged += Window2_StateChanged;
  Closed += Window2_Closed;
  IsVisibleChanged += Window2_IsVisibleChanged;
}

private void DoAction()
{
  // サブフォームをモーダルダイアログ表示
  var subForm = new Window2();
  subForm.Owner = this;
  bool? dialogResult = subForm.ShowDialog();
}

private bool CurrentFormClosed = false;

// .NET Framework - SizeChanged イベントハンドラの代替
private void Window2_StateChanged(object sender, System.EventArgs e)
{
  // this.Owner が Window でないならば以降の処理はしない
  if (this.Owner is Window owner)
  {
    if (this.WindowState == WindowState.Minimized)
    {
      // 最小化　→ 呼び出し元を非表示
      owner.Visibility = Visibility.Collapsed;
    }
    else if (owner.Visibility == Visibility.Collapsed)
    {
      owner.Visibility = Visibility.Visible;
    }
  }
}

// .NET Framework - VisibleChanged イベントハンドラ相当
private void Window2_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
{
// Window2 から ShowDialog していないので処理不要
// // this.Owner が Window でないならば以降の処理はしない
// if (this.Owner is Window owner)
// {
//   if (!CurrentFormClosed)
//   {
//     if (this.Visibility == Visibility.Visible)
//     {
//       if (owner.Visibility == Visibility.Collapsed)
//       {
//         owner.Visibility = Visibility.Visible;
//       }
//     }
//     else if (this.Visibility == Visibility.Collapsed)
//     {
//       owner.Visibility = Visibility.Collapsed;
//     }
//   }
// }
}

// .NET Framework - FormClosed イベントハンドラ相当
private void Window2_Closed(object sender, System.EventArgs e)
{
  // this.Owner が Window でないならば以降の処理はしない
  if (this.Owner is Window owner)
  {
    if (this.Owner.Visibility == Visibility.Collapsed)
    {
      owner.Visibility = Visibility.Visible;
    }
  }
  CurrentFormClosed = true;
}
```

### WPF - .NET 8

MainWindow → Window1 → Window2 と Window.ShowDialog するサンプルコードです。

WPF - .NET Framework 4.8 とほぼ同一コードです。
イベントハンドラの第一引数を下記のように `object?` に変更するだけです。

```CSharp:Window1.xaml.cs
// .NET Framework - SizeChanged イベントハンドラの代替
private void Window1_StateChanged(object? sender, System.EventArgs e)
{
  // TODO
}

// .NET Framework - FormClosed イベントハンドラ相当
private void Window1_Closed(object? sender, System.EventArgs e)
{
  // TODO
}
```

```CSharp:Window2.xaml.cs
// .NET Framework - SizeChanged イベントハンドラの代替
private void Window2_StateChanged(object? sender, System.EventArgs e)
{
  // TODO
}

// .NET Framework - FormClosed イベントハンドラ相当
private void Window2_Closed(object? sender, System.EventArgs e)
{
  // TODO
}
```