MS Learn の WinUI Notes チュートリアルに沿ってファイル読み書きを実装したところ、アプリからは The parameter is incorrect 例外で失敗する――この現象は、WinUI/UWP のストレージ API の想定と引数の与え方が噛み合っていないときに必ず起こります。本記事では原因と直し方を、Assets の読み取り・LocalFolder での編集・初回コピーの実装まで、実運用で役立つコードとともに詳解します。
現象の整理:The parameter is incorrect が出る条件
MS Learn の「WinUI Notes」サンプルをベースに、Assets フォルダーに置いたファイルをアプリで開こうとしたところ、次のような例外が発生します。
System.ArgumentException: The parameter is incorrect.
(HRESULT: 0x80070057 E_INVALIDARG)
開発者がよくやってしまうのは、フォルダーを指す StorageFolder と、ファイル名(パス)を 1 本の文字列でまとめて扱おうとする書き方です。例えば次のように fileName にフルパスを入れてしまうと失敗します。
// ❌ よくある間違い(例)
private StorageFolder storageFolder = ApplicationData.Current.LocalFolder;
private string fileName = @"C:\repo\MyApp\Assets\note.txt";
noteFile = await storageFolder.CreateFileAsync(
fileName, CreationCollisionOption.OpenIfExists); // --> E_INVALIDARG
このとき OS の「ファイル名を指定して実行(run コマンド)」やエクスプローラーでは開けてしまうため、なぜアプリ経由でだけ落ちるのか混乱しがちです。
結論(原因と対処の要点)
- 原因: WinUI/UWP のストレージ API は 「フォルダー」 と 「その直下(または相対パス)のファイル名」 を分けて受け取る設計。
StorageFolder.GetFileAsync/CreateFileAsyncに フルパス を渡すとE_INVALIDARG(The parameter is incorrect)。 - 基本対処:
ApplicationData.Current.LocalFolderに対して ファイル名のみ(例:"note.txt")を渡す。Assets の既存ファイルはms-appx:///URI で取得し、編集したい場合は初回に LocalFolder へコピーする。 - 理由: サンドボックスおよびパッケージ リソースの前提により、アプリ内部のリソース(Assets) と アプリ データ(LocalFolder) は取得手段が異なる。文字列の絶対パスを Storage API に直接渡す設計になっていない。
WinUI のストレージ API は「場所」と「名前」を分ける
WinUI/UWP では、パス文字列を直接つなぐのではなく、オブジェクト + 相対名 の組み合わせで場所を解決します。
StorageFolder // 具体的な「場所」(例:LocalFolder)
└── GetFileAsync("note.txt") // その場所の中の「相対名」
└── CreateFileAsync("memo.txt", {...}) // 同上
逆に、アプリ パッケージ内の静的ファイルは ms-appx:/// URI で取得します。こちらは 読み取り専用 です。書き込みが必要な場合は LocalFolder にコピーしてから操作しましょう。
正しい修正コード(最小構成)
以下は、LocalFolder に note.txt を作成・読み込みする最小コードです。ファイル名だけを渡している点がポイントです。
<!-- C# -->
public sealed partial class NotePage : Page
{
private StorageFolder storageFolder = ApplicationData.Current.LocalFolder;
private StorageFile? noteFile = null;
private string fileName = "note.txt"; // ★ ファイル名だけにする
private async Task LoadNoteAsync()
{
noteFile = await storageFolder.CreateFileAsync(
fileName, CreationCollisionOption.OpenIfExists);
string text = await FileIO.ReadTextAsync(noteFile);
// 読み込んだテキストを UI へ反映...
}
private async Task SaveNoteAsync(string text)
{
if (noteFile == null)
{
noteFile = await storageFolder.CreateFileAsync(
fileName, CreationCollisionOption.OpenIfExists);
}
await FileIO.WriteTextAsync(noteFile, text);
}
}
Assets を読み取り専用で使う場合(ms-appx)
パッケージに含めた静的ファイルは ms-appx:///Assets/... で取得します。編集はできないため、参照だけに使います。
<!-- C# -->
var uri = new Uri("ms-appx:///Assets/note.txt");
StorageFile file = await StorageFile.GetFileFromApplicationUriAsync(uri);
string text = await FileIO.ReadTextAsync(file);
Assets → LocalFolder に初回コピーして編集する
アプリ初回起動時に Assets から LocalFolder にテンプレートをコピーし、その後は LocalFolder 版を編集します。既にコピー済みなら上書きしません。
<!-- C# -->
private async Task<StorageFile> EnsureNoteFileAsync()
{
var local = ApplicationData.Current.LocalFolder;
const string fileName = "note.txt";
// 既に存在するならそれを返す
var existing = await local.TryGetItemAsync(fileName) as StorageFile;
if (existing != null) return existing;
// 初回:Assets からコピー
var src = await StorageFile.GetFileFromApplicationUriAsync(
new Uri("ms-appx:///Assets/note.txt"));
return await src.CopyAsync(local, fileName, NameCollisionOption.ReplaceExisting);
}
ユースケース別の正しい API と入力値
| やりたいこと | 使う API | 渡す値の形 | 書き込み |
|---|---|---|---|
| LocalFolder のファイルを開く | StorageFolder.GetFileAsync | ファイル名のみ(例:"note.txt"、サブフォルダーは "Sub\\note.txt") | 可 |
| LocalFolder に新規作成 | StorageFolder.CreateFileAsync | ファイル名のみ | 可 |
| Assets の既存ファイルを読む | StorageFile.GetFileFromApplicationUriAsync | ms-appx:///Assets/... の URI | 不可(読み取り専用) |
| Assets を LocalFolder にコピー | StorageFile.CopyAsync | コピー先は ApplicationData.Current.LocalFolder、ファイル名のみ | 可(コピー後は編集可能) |
| 任意フルパスのファイルを開く | StorageFile.GetFileFromPathAsync | 絶対パス(権限が必要。UWP では制約が多い) | 環境依存 |
「run コマンドやエクスプローラーでは開ける」のにアプリでは落ちる理由
- シェルはフルパス前提: Windows のシェル(ファイル名を指定して実行やエクスプローラー)は絶対パスを受け取り、ユーザーの権限で直接 OS に解決させます。
- アプリは API 前提: WinUI/UWP の Storage API は フォルダー オブジェクト + 相対名 というモデルでリソースを扱うため、フルパス文字列を渡すと API の契約違反で
E_INVALIDARGを返します。 - サンドボックスの違い: パッケージ内の
Assetsは読み取り専用、ユーザーデータはLocalFolder。この境界を越えるには所定の API(ms-appxURI やコピー)が必須です。
例外の読み解きとデバッグの勘所
- HResult:
0x80070057(E_INVALIDARG)は「引数が契約どおりではない」サイン。パスのスペルミスより、API の受け取り方そのものを疑うのが早道です。 - 監視ポイント: 例外が出る直前の
storageFolder.PathとfileNameをウォッチ。片方がフォルダー、もう片方が相対名になっているか確認します。 - 相対セパレーター: 相対指定は
Sub\\note.txtのように\\を使います(/ではなく\)。 - 予約文字:
< > : " / \ | ? *はファイル名に使えません。Path.GetInvalidFileNameChars()で検証しましょう(System.IO)。
現場でそのまま使えるユーティリティ(例)
再発を防ぐために、ファイル名の扱いと場所の責務をユーティリティに閉じ込めるのがおすすめです。
<!-- C# -->
public static class NoteStorageService
{
private static readonly StorageFolder Local = ApplicationData.Current.LocalFolder;
public static async Task<string> LoadFromLocalAsync(string fileName)
{
ValidateFileName(fileName);
var file = await Local.CreateFileAsync(fileName, CreationCollisionOption.OpenIfExists);
return await FileIO.ReadTextAsync(file);
}
public static async Task SaveToLocalAsync(string fileName, string content)
{
ValidateFileName(fileName);
var file = await Local.CreateFileAsync(fileName, CreationCollisionOption.OpenIfExists);
await FileIO.WriteTextAsync(file, content ?? string.Empty);
}
public static async Task<StorageFile> GetFromAssetsAsync(string assetsRelativePath)
{
if (string.IsNullOrWhiteSpace(assetsRelativePath))
throw new ArgumentException("assetsRelativePath is required.");
var uri = new Uri($"ms-appx:///Assets/{assetsRelativePath.Replace("\\\\", "/")}");
return await StorageFile.GetFileFromApplicationUriAsync(uri);
}
public static async Task<StorageFile> EnsureFromAssetsToLocalAsync(
string assetsRelativePath, string localFileName)
{
ValidateFileName(localFileName);
var existing = await Local.TryGetItemAsync(localFileName) as StorageFile;
if (existing != null) return existing;
var src = await GetFromAssetsAsync(assetsRelativePath);
return await src.CopyAsync(Local, localFileName, NameCollisionOption.ReplaceExisting);
}
private static void ValidateFileName(string fileName)
{
if (fileName.Contains(":") || fileName.Contains("\\\\") || fileName.Contains("/"))
throw new ArgumentException(
"fileName にはフルパスやディレクトリ区切りを含めないでください。ファイル名のみを渡します。");
}
}
「よくある間違い」と「正解」の対比
| よくある間違い | なぜダメか | 正しい書き方 |
|---|---|---|
ApplicationData.Current.LocalFolder.CreateFileAsync(@"C:\repo\Assets\note.txt", ...) | フォルダーとフルパスを二重に指定している | CreateFileAsync("note.txt", ...) |
GetFileAsync(@"C:\repo\Assets\note.txt") | 相対名ではなく絶対パス | GetFileAsync("note.txt") または GetFileFromApplicationUriAsync(ms-appx) |
FileIO.WriteTextAsync(assetFile, ...)(Assets 直書き) | Assets は読み取り専用 | 初回に LocalFolder にコピーしてから編集 |
GetFileFromPathAsync でユーザープロファイル直叩き | 権限やパッケージの制約に該当しやすい | LocalFolder を基本に。どうしても必要ならピッカーや権限を設計 |
堅牢化のテクニック
- Try パターンを活用:
StorageFolder.TryGetItemAsyncは存在チェックと取得を例外なしで行えます。 - I/O は await で直列化: 同じファイルを同時に開いて書くと衝突しやすいので、書き込み処理は 1 箇所に集約します。
- バックアップ:
CopyAsyncで.bakを作るだけでも復旧が容易。 - ファイル名のバリデーション: UI 層で「パス風文字列」を入力させない。ファイル名のみを求める説明とプレースホルダーを用意。
最低限のエラーハンドリング例
<!-- C# -->
try
{
var note = await NoteStorageService.LoadFromLocalAsync("note.txt");
// 反映...
}
catch (ArgumentException ex) when (ex.HResult == unchecked((int)0x80070057))
{
// 文字列引数の不備(E_INVALIDARG 相当)
// 入力の見直し・ログ
}
catch (Exception ex)
{
// ログ出力とユーザー向けメッセージ
}
実用サンプル:Assets テンプレート → 編集 → 保存
<!-- C# -->
public sealed partial class NotePage : Page
{
private const string FileName = "note.txt";
private StorageFile? _noteFile;
public NotePage()
{
this.InitializeComponent();
_ = InitializeAsync();
}
private async Task InitializeAsync()
{
_noteFile = await NoteStorageService.EnsureFromAssetsToLocalAsync("note.txt", FileName);
NoteTextBox.Text = await FileIO.ReadTextAsync(_noteFile);
}
private async void SaveButton_Click(object sender, RoutedEventArgs e)
{
if (_noteFile == null)
{
_noteFile = await ApplicationData.Current.LocalFolder.CreateFileAsync(
FileName, CreationCollisionOption.OpenIfExists);
}
await FileIO.WriteTextAsync(_noteFile, NoteTextBox.Text);
StatusTextBlock.Text = "保存しました";
}
}
UWP/WinUI 3(Windows App SDK)での考え方の共通項
- パッケージ内リソースは
ms-appx: 読み取り専用。編集は Local へコピー。 - ユーザーデータは Local:
ApplicationData.Current.LocalFolderを起点に、相対名で操作。 - フルパス直叩きは最終手段: 権限や配布形態により動かないケースが多い。まずは Storage API の流儀を守る。
チェックリスト(再発防止)
- Storage API に渡しているのは ファイル名のみか?(
note.txt) - Assets を開くときに
ms-appx:///を使っているか? - 編集前に Assets → LocalFolder へコピーしているか?
- 相対セパレーターは
\か?(/ではない) - 例外の HResult が
0x80070057(E_INVALIDARG)なら、引数の形を再点検する。
まとめ
The parameter is incorrect は、ほぼ確実に「フォルダー + ファイル名」という WinUI/UWP のストレージ設計に反した引数を渡したサインです。フルパスを渡す習慣から脱して、ApplicationData.Current.LocalFolder にはファイル名のみ、Assets は ms-appx:/// 経由で取得、編集は Local へコピー――この 3 点を守れば、Notes チュートリアルのような基本アプリでも堅牢に動きます。実際の現場では本記事のユーティリティやチェックリストを活用し、「開けるのに保存できない」「run では動くのにアプリで落ちる」といった時間のかかるトラブルを未然に防ぎましょう。
付録:サブフォルダーを使う場合の書き方
サブフォルダーに保存したいときも、やはり相対指定です。
<!-- C# -->
var local = ApplicationData.Current.LocalFolder;
var sub = await local.CreateFolderAsync("Notes", CreationCollisionOption.OpenIfExists);
var file = await sub.CreateFileAsync("note.txt", CreationCollisionOption.OpenIfExists);
await FileIO.WriteTextAsync(file, "Hello");
付録:TryGetItemAsync での存在チェック
<!-- C# -->
var item = await ApplicationData.Current.LocalFolder.TryGetItemAsync("note.txt");
if (item is StorageFile f)
{
var text = await FileIO.ReadTextAsync(f);
}
付録:入力の受け取りでフルパスを拒否する UI 例
<!-- C# -->
// ファイル名入力 TextBox の変更時にチェック
private void FileNameBox_TextChanged(object sender, TextChangedEventArgs e)
{
var s = FileNameBox.Text;
bool looksLikePath = s.Contains(":") || s.Contains("\\\\") || s.Contains("/");
ErrorTextBlock.Text = looksLikePath
? "ファイル名だけを入力してください(例: note.txt)"
: string.Empty;
SaveButton.IsEnabled = !looksLikePath;
}
要点の再掲
- 原因は フルパスを渡したこと。Storage API は相対名のみを受け取る。
- LocalFolder には ファイル名のみ、Assets は ms-appx URI で取得。
- 編集したい Assets は 初回コピーして Local で書き換える。
0x80070057 (E_INVALIDARG)は契約違反の合図。まず引数を見直す。

コメント