Windows 11 と .NET 8 で C# WinForms アプリを開発していて、UWP/WinUI のコードを流用しようとして Windows.Storage.ApplicationData.Current.LocalSettings を呼び出した結果、起動直後に InvalidOperationException が発生してしまう……という相談が増えています。本記事では、この例外の正体と原因を丁寧に整理しつつ、WinForms で「正しく・安全に」ユーザー設定を保存するためのパターンを、具体的なコード例とともに解説します。
WinForms で ApplicationData.Current.LocalSettings を呼ぶと例外になる現象
今回の現象は、Windows 11/.NET 8/C# 11/Visual Studio 2022 で作成した 通常の Windows Forms(WinForms)アプリ で、起動時に次のコードを実行すると発生します。
var localSettings = Windows.Storage.ApplicationData.Current.LocalSettings;
この 1 行を実行した瞬間に、以下のような例外がスローされます。
- 例外の種類:
InvalidOperationException - メッセージ:
Operation is not valid due to the current state of the object.
一見すると「オブジェクトの状態が不正」としか書かれておらず、何が悪いのか分かりづらいメッセージです。ですが、原因はシンプルで、API が想定しているアプリの種類と、実際のアプリの種類が一致していない ことにあります。
原因:Windows.Storage.ApplicationData は UWP/WinUI・MSIX アプリ専用
まず押さえておきたいのが、Windows.Storage.ApplicationData は WinRT(Windows Runtime)の API であり、UWP/WinUI や Windows App SDK などの「パッケージ化アプリ」向けに設計された機能 だという点です。
具体的には、ApplicationData.Current は内部的に「アプリのパッケージ ID(Package Identity)」を前提として動作します。しかし、従来型の WinForms(非 MSIX、非パッケージ)アプリにはパッケージ ID が存在しません。そのため、WinForms から ApplicationData.Current にアクセスすると、API 側が「想定している状態ではない」と判断し、InvalidOperationException を投げてしまうわけです。
| アプリ種別 | 例 | パッケージ ID | ApplicationData.Current 利用可否 |
|---|---|---|---|
| UWP アプリ | Windows 10 ストアアプリ | あり(必須) | ◯ 利用可能 |
| WinUI 3 / Windows App SDK(MSIX) | 新しいデスクトップ アプリ | あり(MSIX パッケージ) | ◯ 利用可能 |
| 通常の WinForms / WPF(非パッケージ) | .exe をそのまま配布するアプリ | なし | × 利用不可(例外になる) |
つまり、今回のエラーは「WinForms のコードが間違っている」というより、そもそもその API を使える環境ではない ことが原因です。この前提を踏まえたうえで、WinForms で推奨される設定保存の方法に切り替えていく必要があります。
WinForms でユーザー設定を扱う「正解」の選択肢
WinForms アプリでユーザー設定を保存・読み込みする方法として、代表的な選択肢は次の 3 つです。
| 解決策 | 概要 | メリット | デメリット/注意点 |
|---|---|---|---|
| 解決策 A Properties.Settings | VS が自動生成する「型付き設定」を利用 | 標準機能・型安全・手軽・ツールサポートあり | 細かい制御がしづらい場面もある |
| 解決策 B 独自 JSON ファイル | settings.json などに自前で保存 | 構造を自由に設計できる/他言語でも読みやすい | クラス定義・シリアライズ処理を自分で書く必要 |
| 解決策 C MSIX パッケージ化 | WinForms をパッケージ化して WinRT API を利用 | UWP/WinUI と近い API 群を利用可能 | 配布形態が変わる・パッケージングの学習コスト |
一般的な業務アプリやツールであれば、まずは解決策 A:Properties.Settings の活用を検討する ことを強くおすすめします。理由は:
- Visual Studio が UI 付きで設定項目を管理できる
- 型安全にアクセスでき、リファクタリングしやすい
- .NET の標準的な方法であり、情報量が多い
以下では、それぞれの方法をもう少し掘り下げて解説します。
解決策 A:Properties.Settings(型付き設定)を使う
Settings.settings にユーザー設定を定義する
まずは Visual Studio の標準機能である 型付き設定(Properties.Settings) を利用する手順です。
- ソリューション エクスプローラーでプロジェクトを選択
- メニューから「プロジェクト」→「プロパティ」を開く
- 左側の「設定」タブ(
Settings.settings)を開く - 以下のような項目を追加し、「範囲」は ユーザー にする
MaxGridSize(int)MinGridSize(int)GridSize(int)ImageGridVisible(bool)SnapTo(bool)Upgraded(bool) ※後述のバージョンアップ用フラグ
これだけで、Properties.Settings.Default.MaxGridSize のように、型安全なプロパティとして設定が利用できるようになります。
起動時に設定を読み込むコード例
フォームの Load イベントなどで、設定を読み込んで画面や内部変数に反映します。ついでに、バージョンアップ時の設定引き継ぎもここで行うのがおすすめです。
private void MainForm_Load(object sender, EventArgs e)
{
// 初回起動時に、旧バージョンの user.config から値を引き継ぐ
if (!Properties.Settings.Default.Upgraded)
{
Properties.Settings.Default.Upgrade(); // 旧バージョンの設定をコピー
Properties.Settings.Default.Upgraded = true;
Properties.Settings.Default.Save(); // フラグを永続化
}
// 設定の読み込み
MaxGridSize = Properties.Settings.Default.MaxGridSize;
MinGridSize = Properties.Settings.Default.MinGridSize;
GridSize = Properties.Settings.Default.GridSize;
ImageGridVisible = Properties.Settings.Default.ImageGridVisible;
SnapTo = Properties.Settings.Default.SnapTo;
// 読み込んだ値を UI に反映(例)
numericUpDownGridSize.Maximum = MaxGridSize;
numericUpDownGridSize.Minimum = MinGridSize;
numericUpDownGridSize.Value = GridSize;
checkBoxImageGrid.Checked = ImageGridVisible;
checkBoxSnapTo.Checked = SnapTo;
}
このように、Properties.Settings.Default から読み込んだ値を、そのままフォーム上のコントロールに流し込むだけで、ユーザー設定の「復元」が実現できます。
終了時に変更を保存するコード例
設定を保存するタイミングとしては、フォームの FormClosing イベント で行うのが分かりやすく、典型的です。
private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
{
// 現在の UI の状態を Settings に書き戻す
Properties.Settings.Default.MaxGridSize = (int)numericUpDownGridSize.Maximum;
Properties.Settings.Default.MinGridSize = (int)numericUpDownGridSize.Minimum;
Properties.Settings.Default.GridSize = (int)numericUpDownGridSize.Value;
Properties.Settings.Default.ImageGridVisible = checkBoxImageGrid.Checked;
Properties.Settings.Default.SnapTo = checkBoxSnapTo.Checked;
// ディスクに保存
Properties.Settings.Default.Save();
}
これで、アプリを終了して再度起動しても、前回の状態がそのまま復元されるようになります。
App.config と user.config の関係を整理する
ここでよく誤解されるのが、「設定の変更が App.config に書き戻されない」という点です。実際には、.NET の設定システムは次のように役割分担しています。
| ファイル | 主な役割 | 保存される値 | 配置場所 |
|---|---|---|---|
App.config(ビルド後は appname.exe.config) | アプリ全体の既定値・構成 | Settings で定義したデフォルト値 | アプリの実行ファイルと同じフォルダー |
user.config | ユーザーごとの変更内容 | 実行時にユーザーが変更した設定値 | %LOCALAPPDATA% 配下の専用フォルダー |
つまり、ユーザー範囲の設定は App.config には二度と書き戻されない のが「仕様」です。
「App.config に保存されていないからおかしい」のではなく、「user.config に保存されるのが正しい」挙動だと理解しておきましょう。
ありがちな落とし穴(Properties.Settings 編)
- 型を変えたのに、古い値が残って例外になる
例:stringだった設定をintに変更した場合、既存のuser.configの値と整合が取れず例外になることがあります。
対策:Upgrade()を使う/問題の設定を一旦削除して作り直す/古いuser.configを削除するなど。 - 管理者として起動したら設定が初期化されたように見える
実際にはユーザー プロファイルが変わっているため、user.configの保存先が別になっているだけです。
対策:一般ユーザーでの利用を前提とする/必要ならば「設定のエクスポート/インポート機能」を用意する。 - 設定ファイルを直接コピーして配布しようとする
user.configはユーザーフォルダー内の深い場所にあり、バージョンごとにパスも変わるため、手作業でコピーするのはおすすめできません。
対策:アプリ内でバックアップ・復元機能を設ける方が安全です。
解決策 B:独自 JSON ファイルで設定を管理する
「既存のコードやサーバー側と JSON でやり取りしたい」「設定の構造が複雑で、Properties.Settings では扱いにくい」場合は、JSON ファイルで独自の設定クラスを定義する方法 が有力な選択肢になります。
設定クラスを定義する
まずは設定内容を表す C# クラスを用意します。初期値(デフォルト値)もここに記述しておくと分かりやすくなります。
using System.Text.Json;
public class AppSettings
{
public int MaxGridSize { get; set; } = 50;
public int MinGridSize { get; set; } = 1;
public int GridSize { get; set; } = 50;
public bool ImageGridVisible { get; set; } = false;
public bool SnapTo { get; set; } = false;
}
保存先フォルダー(パス)を決める
設定ファイルは、ユーザーごとに書き込み権限がある %LOCALAPPDATA%(ローカル環境用)や %APPDATA%(Roaming プロファイル)以下に置くのが定石です。ここでは %LOCALAPPDATA% 配下に YourCompany\YourApp フォルダーを作る例を示します。
string dir = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"YourCompany",
"YourApp");
Directory.CreateDirectory(dir); // 存在しなければ作成
string file = Path.Combine(dir, "settings.json");
JSON から読み込むコード
アプリ起動時に JSON ファイルがあれば読み込み、なければデフォルト値で初期化します。
AppSettings settings;
if (File.Exists(file))
{
string json = File.ReadAllText(file);
settings = JsonSerializer.Deserialize<AppSettings>(json) ?? new AppSettings();
}
else
{
settings = new AppSettings();
}
// 読み込んだ設定をアプリ側に適用
MaxGridSize = settings.MaxGridSize;
MinGridSize = settings.MinGridSize;
GridSize = settings.GridSize;
ImageGridVisible = settings.ImageGridVisible;
SnapTo = settings.SnapTo;
JSON へ保存するコード
アプリ終了時や「OK」ボタン押下時など、好きなタイミングで JSON に書き出します。
var settingsToSave = new AppSettings
{
MaxGridSize = MaxGridSize,
MinGridSize = MinGridSize,
GridSize = GridSize,
ImageGridVisible = ImageGridVisible,
SnapTo = SnapTo
};
var options = new JsonSerializerOptions
{
WriteIndented = true // 見やすい整形 JSON にする
};
string jsonOut = JsonSerializer.Serialize(settingsToSave, options);
File.WriteAllText(file, jsonOut);
これで、例えば settings.json を他の端末にコピーして設定を移行するといった運用も簡単に行えます。Web アプリや他の言語と設定を共有したい場合にも有利です。
Properties.Settings との比較
| 項目 | Properties.Settings | 独自 JSON |
|---|---|---|
| 手軽さ | ◎(VS の UI で設定) | ◯(コード記述が必要) |
| 柔軟性 | △(階層構造などは苦手) | ◎(自由な構造が取れる) |
| 他環境からの読み書き | △(.NET 依存) | ◎(JSON なら多言語対応しやすい) |
| 学習コスト | 低い | やや高い(シリアライズの理解が必要) |
小さめのツールや社内アプリであれば Properties.Settings、将来的に他システム連携を考えている場合は JSON といったように、アプリの性質に合わせて選択するとよいでしょう。
解決策 C:どうしても Windows.Storage を使いたい場合(MSIX パッケージ化)
「UWP アプリから WinForms に移植したが、ApplicationData.Current.LocalSettings をほぼそのまま使いたい」「既存の WinRT API をフル活用したい」といった事情がある場合は、WinForms アプリを MSIX でパッケージ化 し、アプリにパッケージ ID を与えることで Windows.Storage を利用できるようになります。
パッケージ化の大まかな流れ
Visual Studio 2022 の場合、代表的な手順は次のようになります。
- ソリューションに「Windows アプリケーション パッケージ プロジェクト」を追加する
- パッケージ プロジェクトから、既存の WinForms プロジェクトを参照として追加する
- パッケージ プロジェクトをスタートアップ プロジェクトに設定する
- アプリケーション マニフェストでパッケージ名や発行者情報を設定する
- MSIX パッケージとしてビルド/署名し、配布する
この形態で実行された WinForms アプリは、「パッケージ化されたデスクトップ アプリ」 として扱われるようになり、ApplicationData.Current を含む多くの WinRT API が利用可能になります。
ただし、この方法は以下のような注意点もあります。
- 配布方法が「MSIX インストーラー」前提になる(従来の単体 .exe 配布とは変わる)
- 企業内での配布の場合、MSIX の展開ルールや証明書管理などを理解する必要がある
- 一部の API は依然として UWP 専用だったり、パーミッション設定が必要だったりする
そのため、単に「設定を保存したい」という目的だけであれば、無理に MSIX 化するよりも Properties.Settings や JSON を使う方が、開発・運用ともにシンプル なことが多いです。
移行パターン別の考え方
最後に、よくあるシナリオ別に「どの選択肢を取るべきか」をざっくり整理してみます。
UWP/WinUI アプリから WinForms へ移植中の場合
ApplicationData.Current.LocalSettingsを使っていた部分は、WinForms ではProperties.Settingsまたは JSON に置き換える のが基本。- 設定キーの名前はそのまま流用しつつ、中身を C# のプロパティにマッピングすることで、移植の負担を減らせる。
- 将来的にも WinRT API を多用したい場合のみ、MSIX パッケージ化を検討する価値がある。
既存の WinForms アプリに、あとから設定機能を追加したい場合
- 特にこだわりがなければ、まずは
Properties.Settingsから始める のが圧倒的に楽。 - 複数のフォームで共通の設定を使いたい場合も、
Properties.Settings.Defaultを通じて簡単に共有できる。 - ユーザーに「設定の初期化」ボタンを提供する場合は、
Reset()や自前ロジックで既定値を書き戻す処理を用意する。
すでに独自 XML で設定を持っている場合
- 現状問題なく運用できているなら、必ずしも
Properties.Settingsに移行する必要はない。 - ただし、
ApplicationData.Currentなどの WinRT API と混在させるのは避け、保存場所・仕様を一本化しておくと保守が楽。 - XML が煩雑になり始めたら、次のバージョンで JSON など別形式への移行を検討するのも一案。
「よくある誤解」と防止メモ
最後に、今回の話題に関してありがちな誤解やハマりどころを、チェックリスト的にまとめておきます。
- 誤解 1:App.config に値が保存されないのはバグだ
→ 仕様どおりです。
ユーザー範囲(Scope: User)の設定はuser.configに保存され、App.configはあくまで「既定値の倉庫」としてのみ使われます。 - 誤解 2:
ApplicationData.Current.LocalSettingsは .NET アプリならどこでも使える
→ 実際は「パッケージ ID を持つアプリ限定」の WinRT API です。
非パッケージの WinForms/WPF アプリではInvalidOperationExceptionになって当然、と覚えておきましょう。 - 誤解 3:MSIX 化すればすべての UWP API が無条件で使える
→ ほとんどのストレージ関連 API は利用可能になりますが、機能によっては UWP 限定のもの、追加のマニフェスト設定が必要なものも存在します。 - 誤解 4:設定の引き継ぎは自動でやってくれる
→Properties.Settingsを使う場合でも、Upgrade()を自分で呼び出さないと、旧バージョンのuser.configは自動では読み込まれません。
初回起動時にきちんとUpgrade()を呼ぶコードを書いておきましょう。 - 誤解 5:ローミングプロファイル環境でも
user.configは安全にローミングする
→ 基本的にはユーザー プロファイルとともに移動しますが、大量の設定を書き込むとプロファイルサイズが増大します。
大きなデータは別ストレージに保存するなど、設計上の配慮が必要です。
まとめ:WinForms では「WinForms らしい」方法で設定を保存する
本記事の内容を、改めてポイントだけ箇条書きで整理します。
Windows.Storage.ApplicationData.Current.LocalSettingsは、UWP/WinUI などのパッケージ化アプリ専用の API である。- 非パッケージの WinForms アプリから呼び出すと、アプリにパッケージ ID がないため
InvalidOperationExceptionが発生する。 - WinForms では、次のいずれかを使うのが定石:
- 解決策 A:
Properties.Settings(型付き設定)でユーザー設定を管理する - 解決策 B:独自の JSON ファイル(
settings.jsonなど)で柔軟に管理する - 解決策 C:どうしても
Windows.Storageを使いたい場合だけ、MSIX でパッケージ化する
- 解決策 A:
App.configには「既定値」、実行時の変更はuser.configに保存されるのが正常な挙動であり、「App.config に書き戻されない」のは仕様である。- バージョンアップ時に設定を引き継ぎたい場合は、初回起動時に
Properties.Settings.Default.Upgrade()を呼ぶパターンが非常に有効。 - 既存の XML ベース設定や他システムとの連携を考える場合は、JSON での管理も視野に入れると設計の自由度が上がる。
これらを踏まえてコードを整理すれば、WinForms アプリでも UWP/WinUI に負けない快適な設定管理が実現できます。まずは ApplicationData.Current.LocalSettings の呼び出しを削除し、Properties.Settings または JSON ベースの実装に置き換える ところから取り掛かるのがおすすめです。

コメント