WinUI 3/UWP「The parameter is incorrect」エラーの原因と解決策:Assets と LocalFolder の正しい使い分けと StorageFile/GetFileAsync 実装ガイド

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.GetFileFromApplicationUriAsyncms-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-appx URI やコピー)が必須です。

例外の読み解きとデバッグの勘所

  • 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) は契約違反の合図。まず引数を見直す。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次