「ボタンを押して新しいフォームを開いたら、前のフォームで設定した値がなぜか 0 に戻っている」――Windowsフォーム(WinForms)では非常に起きやすい落とし穴です。本記事では、原因の正体(“別インスタンス”問題)を図解的に理解し、静的フィールド・コンストラクタ引数・プロパティ注入・イベント/デリゲート・共有サービス(DI)など、規模や保守性に応じて選べる実装まで、実務で迷わない判断軸とコード例を網羅します。
状況の整理:なぜ variableA が 0 に戻るのか
まずは典型的な再現例です。First_Form でフィールドを 1 または 2 に更新し、New_Form を開きます。
// First_Form
public partial class First_Form : Form
{
public ushort variableA = 0;
private void button1_Click(object sender, EventArgs e)
{
variableA = 1;
new New_Form().Show();
}
private void button2_Click(object sender, EventArgs e)
{
variableA = 2;
new New_Form().Show();
}
}
// New_Form
public partial class New_Form : Form
{
private void New_Form_Load(object sender, EventArgs e)
{
First_Form myForm = new First_Form();
if (myForm.variableA == 1) { /* … */ } // ここが常に false
}
}
問題の本質はただ一つ。new First_Form() で完全に新しいインスタンスを生成しているため、variableA は再び 0 に初期化されます。つまり「値が届かない」のではなく、「別のオブジェクトを見ている」だけです。
フォームのインスタンスと状態:ほんの少しだけ内部を理解する
WinForms のフォームは通常「インスタンス(= オブジェクト)ごとに状態を持つ」設計です。フィールドやプロパティはインスタンス単位で保持され、新たに new すればデフォルト値に戻ります。下図のイメージで把握しましょう。
Before:First_Form #A(画面に表示中) … variableA = 1
After:New_Form 内で new First_Form() → First_Form #B を作成 … variableA = 0
あなたが見たかったのは #A ですが、コードは #B を参照しています。ここを正せば一気に解決に近づきます。
解決アプローチ早見表(用途と規模で選ぶ)
| 方法 | 概要 | 向く規模 | メリット | デメリット/注意点 |
|---|---|---|---|---|
| 1. 静的フィールド | public static ushort VariableA を使い First_Form.VariableA と参照 | 小規模試作・ツール | 実装が最小。既存コードを大きく崩さない | アプリ全体で共有され副作用が増える。テスト困難 |
| 2. コンストラクタ引数 | New_Form(ushort current) で値を受け取る | 中規模以上 | 必要な値だけ渡せる。依存が明確 | 生成箇所ごとに引数を渡し忘れない設計が必要 |
| 3. プロパティ注入 | new New_Form { InitialValue = variableA } | 中規模 | 生成と設定のタイミングを柔軟に制御 | セット漏れのリスク。初期化順序に注意 |
| 4. イベント/デリゲート | ボタンクリック時に値をイベント引数で通知/コールバックを渡す | 中規模~分離重視 | 疎結合でテストしやすい | イベント購読の解除忘れ・設計がやや複雑 |
| 5. 共有サービス(DI) | AppState のような状態オブジェクトを注入して共有 | 中~大規模、長期保守 | 構造化された状態管理。拡張・テスト容易 | 導入コスト。DI の理解が必要 |
| 6. MVVM/VM共有 | INotifyPropertyChanged な ViewModel を複数フォームで共有 | 大規模・複雑UI | UI とロジック分離。双方向データバインドが容易 | 学習・実装コスト。WinFormsでのバインド知識が必要 |
まずは誤りの最小再現と是正
誤りパターン(新しいインスタンスを見てしまう)
// NG: New_Form で新しい First_Form を生成してしまう
private void New_Form_Load(object sender, EventArgs e)
{
First_Form myForm = new First_Form(); // <-- これが原因
if (myForm.variableA == 1) { /* … */ }
}
最短で直す:静的フィールド(方法1)
// First_Form
public partial class First_Form : Form
{
public static ushort VariableA = 0;
private void button1_Click(object sender, EventArgs e)
{
VariableA = 1;
new New_Form().Show();
}
private void button2_Click(object sender, EventArgs e)
{
VariableA = 2;
new New_Form().Show();
}
}
// New_Form
public partial class New_Form : Form
{
private void New_Form_Load(object sender, EventArgs e)
{
if (First_Form.VariableA == 1)
{
// 何か処理
}
}
}
この方法は「今すぐ動かす」には有効ですが、アプリ全体に広がるグローバル状態になるため拡張・テストの難易度が上がります。中長期の保守を想定するなら次以降の方法がおすすめです。
堅実に直す:値を明示的に渡す
方法2:コンストラクタ引数で渡す(推奨)
// First_Form
public partial class First_Form : Form
{
public ushort variableA = 0;
private void button1_Click(object sender, EventArgs e)
{
variableA = 1;
new New_Form(variableA).Show();
}
private void button2_Click(object sender, EventArgs e)
{
variableA = 2;
new New_Form(variableA).Show();
}
}
// New_Form
public partial class New_Form : Form
{
private readonly ushort _initialValue;
public New_Form(ushort initialValue)
{
InitializeComponent();
_initialValue = initialValue;
}
private void New_Form_Load(object sender, EventArgs e)
{
if (_initialValue == 1)
{
// 何か処理
}
}
}
コンストラクタで依存する値が必須であることを表現でき、「渡し忘れ」をコンパイル時に防げます。最も安全な基本形です。
方法3:プロパティ注入
// New_Form
public partial class New_Form : Form
{
public ushort InitialValue { get; set; } = 0;
private void New_Form_Load(object sender, EventArgs e)
{
if (InitialValue == 1)
{
// 何か処理
}
}
}
// First_Form
private void button1_Click(object sender, EventArgs e)
{
variableA = 1;
var f = new New_Form { InitialValue = variableA };
f.Show();
}
生成後に設定したいケース(フォームのサイズや位置設定と同時に値を渡したい等)で有効。ただし設定忘れのリスクがあるため、初期化メソッドを明示して呼ぶパターン(InitializeBy(value))にするのも一手です。
方法4:イベント/デリゲート(疎結合設計)
呼び出し側が値を持ち、開く先のフォームは「値の供給元」を意識しない設計です。Func<ushort> のような「値の取得方法」を渡すと、テストもしやすくなります。
// New_Form: 値を提供するコールバックを受け取る
public partial class New_Form : Form
{
private readonly Func<ushort> _valueProvider;
public New_Form(Func<ushort> valueProvider)
{
InitializeComponent();
_valueProvider = valueProvider;
}
private void New_Form_Load(object sender, EventArgs e)
{
var v = _valueProvider();
if (v == 1) { /* … */ }
}
}
// First_Form
private void button1_Click(object sender, EventArgs e)
{
variableA = 1;
var f = new New_Form(() => variableA);
f.Show();
}
フォーム間の依存が薄まり、ユニットテストで New_Form に任意の値を供給できます。
よりスケーラブルに:共有サービス(DI)で状態を一元管理
画面数が増えると「どの値をどこから渡すか」が複雑になります。アプリ内で共有する状態クラス(AppState)を DI(依存性注入)で配布すると、見通しが劇的に良くなります。
状態クラスと列挙型
public enum OperationMode : ushort
{
None = 0,
ModeA = 1,
ModeB = 2
}
public class AppState : INotifyPropertyChanged
{
private OperationMode _mode;
public OperationMode Mode
{
get => _mode;
set
{
if (_mode == value) return;
_mode = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Mode)));
}
}
public event PropertyChangedEventHandler? PropertyChanged;
}
.NET 6 以降の最小 DI セットアップ例
// Program.cs
using Microsoft.Extensions.DependencyInjection;
internal static class Program
{
[STAThread]
static void Main()
{
ApplicationConfiguration.Initialize();
var services = new ServiceCollection()
.AddSingleton<AppState>()
.AddTransient<First_Form>()
.AddTransient<New_Form>();
using var provider = services.BuildServiceProvider();
Application.Run(provider.GetRequiredService<First_Form>());
}
}
フォーム側の受け取り
// First_Form
public partial class First_Form : Form
{
private readonly AppState _state;
private readonly Func<New_Form> _newFormFactory;
public First_Form(AppState state, Func<New_Form> newFormFactory)
{
InitializeComponent();
_state = state;
_newFormFactory = newFormFactory;
}
private void button1_Click(object sender, EventArgs e)
{
_state.Mode = OperationMode.ModeA;
_newFormFactory().Show();
}
private void button2_Click(object sender, EventArgs e)
{
_state.Mode = OperationMode.ModeB;
_newFormFactory().Show();
}
}
// New_Form
public partial class New_Form : Form
{
private readonly AppState _state;
public New_Form(AppState state)
{
InitializeComponent();
_state = state;
// 例:バインドして UI に自動反映
this.labelMode.DataBindings.Add(
"Text", _state, nameof(AppState.Mode));
}
private void New_Form_Load(object sender, EventArgs e)
{
if (_state.Mode == OperationMode.ModeA)
{
// 何か処理
}
}
}
DI を使うと「新しいフォームを開いても、アプリ全体で共有する AppState は同一インスタンス」のまま保たれます。グローバル静的より安全で、テストもしやすいのが利点です。
マジックナンバーをやめる:ushort から enum へ
1 や 2 は意味が伝わらず、定義漏れも検出しづらい値です。列挙型でドメインを明示しましょう。
public enum OperationMode : ushort { None = 0, ModeA = 1, ModeB = 2 }
// 使用例
if (_initialValue == (ushort)OperationMode.ModeA) { /* … */ }
// または、構造ごと enum に切替えてコンストラクタ引数も enum にする
public New_Form(OperationMode mode) { /* … */ }
追加パターン:状況に応じて使えるテクニック集
Owner/ShowDialog パターン
ShowDialog() を用いると、モーダルダイアログの戻りと同時にプロパティで値を受け渡しできます。
// First_Form - 値を渡して結果を受け取る
using (var dlg = new New_Form { InitialValue = variableA })
{
if (dlg.ShowDialog(this) == DialogResult.OK)
{
// dlg.Result などで値を回収
}
}
Application.OpenForms を使う(要注意)
既存のフォームインスタンスを列挙して見つける方法もありますが、フォーム名やライフサイクルに強く依存するため脆い設計になりがちです。推奨はしませんが、既存資産の暫定対応としての例を示します。
var first = Application.OpenForms.OfType<First_Form>().FirstOrDefault();
if (first != null && first.variableA == 1) { /* … */ }
Tag プロパティの活用(軽量)
Control.Tag は「ちょっとした付帯情報」を運ぶのに便利ですが、型安全性が低く、意味の見えにくい実装になりがちです。短命の値や試作に限定して使いましょう。
避けたいアンチパターンと落とし穴
- 静的の乱用:手早い反面、並行実行やテストで想定外の副作用を生みます。最終手段として最小限に。
- イベントの解除忘れ:購読元が長生きだと メモリリーク の原因に。
FormClosed等で-=>解除やWeakEventパターンを検討。 - 別スレッドから UI を触る:非同期処理後にフォームを更新する際は
Invoke/BeginInvokeを必ず使用。 - 初期化順序の問題:プロパティ注入時に
Loadより前に設定される保証を設計で担保する(初期化メソッドを明示する等)。
テスト容易性と可読性を高める設計チェックリスト
| 観点 | 確認ポイント | OK の目安 |
|---|---|---|
| 依存の明確化 | フォームが必要とする値はコンストラクタ or 明示的メソッドで受け取る | 引数無しで生成できないなら「必須依存」 |
| 型安全性 | モード等は enum 化し、ushort を生値で使わない | マジックナンバーの排除 |
| 共有状態 | 静的ではなくサービス(DI)で注入・共有 | AppState を 1 箇所で管理 |
| ライフサイクル | イベント購読の解除・フォーム破棄の明示 | FormClosed でクリーンアップ |
| 将来の拡張 | 画面追加時に「値の受け渡し経路」が増えても影響最小 | コンストラクタ or DI で自然に拡張 |
実務に効くサンプル:最小限のリファクタで堅実に
既存コードへの影響を抑えつつ堅牢にする「コンストラクタ引数 + enum」版を提示します。
public enum OperationMode : ushort { None = 0, ModeA = 1, ModeB = 2 }
// First_Form
public partial class First_Form : Form
{
private OperationMode _mode = OperationMode.None;
private void button1_Click(object sender, EventArgs e)
{
_mode = OperationMode.ModeA;
new New_Form(_mode).Show();
}
private void button2_Click(object sender, EventArgs e)
{
_mode = OperationMode.ModeB;
new New_Form(_mode).Show();
}
}
// New_Form
public partial class New_Form : Form
{
private readonly OperationMode _mode;
public New_Form(OperationMode mode)
{
InitializeComponent();
_mode = mode;
}
private void New_Form_Load(object sender, EventArgs e)
{
switch (_mode)
{
case OperationMode.ModeA:
// A の処理
break;
case OperationMode.ModeB:
// B の処理
break;
}
}
}
MVVM 風にまとめる(WinForms でもできる)
WinForms でも INotifyPropertyChanged を使った ViewModel 共有は有効です。フォームが複数あっても、同一の ViewModel をデータバインドすれば表示や挙動を同期できます。
public class MainViewModel : INotifyPropertyChanged
{
private OperationMode _mode;
public OperationMode Mode
{
get => _mode;
set
{
if (_mode == value) return;
_mode = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Mode)));
}
}
public event PropertyChangedEventHandler? PropertyChanged;
}
// 共有 ViewModel を注入して使う
public partial class First_Form : Form
{
private readonly MainViewModel _vm;
private readonly Func _newFormFactory;
public First_Form(MainViewModel vm, Func<New_Form> newFormFactory)
{
InitializeComponent();
_vm = vm;
_newFormFactory = newFormFactory;
}
private void button1_Click(object sender, EventArgs e)
{
_vm.Mode = OperationMode.ModeA;
_newFormFactory().Show();
}
}
この方式なら New_Form 側は _vm.Mode にバインドするだけで状態を共有できます。UI と状態を分離できるため、ユニットテストは ViewModel に集中させられます。
よくある質問(FAQ)
Q. 静的フィールドで十分では?
小規模ツールでは有効です。ただし「一度動いた」あとに仕様が増えた瞬間、静的の副作用や初期化順序の問題でデバッグが難しくなります。長く使う可能性があるなら DI かコンストラクタ注入を検討しましょう。
Q. 値を返す方向(New → First)はどうする?ShowDialog() と戻り値/公開プロパティで回収する、イベントで通知する、共有 ViewModel に書き戻す等が実務的です。
Q. 複数フォームが同時に開く場合の整合性は?
共有サービス(AppState/ViewModel)を単一インスタンスで持ち、変更通知(PropertyChanged)で UI を同期させると保守性が高まります。
まとめ:キーは「同じインスタンス」を参照すること
- 値が失われるのは 新しいインスタンスを生成しているから。意図したのは「既存インスタンスの状態」。
- 最短は静的フィールド。ただし副作用が大きいため、基本はコンストラクタ引数やプロパティ注入で値を明示的に受け渡す。
- 画面が増える・長期運用では共有サービス(DI)やViewModel 共有で状態を一元管理。
- enum 化で可読性と安全性を高め、イベント解除・スレッド境界などの落とし穴を避ける。
「変数が 0 に戻る」を卒業する最短のコツは、“同じものを見続ける”設計に切り替えること。インスタンスの境界を意識した受け渡しに変えるだけで、WinForms の信頼性は大きく向上します。

コメント