データと永続化を追加する

この手順では、アプリの非ビジュアル部分をアシスタントに依頼します。 この変更を XAML から分離すると、コンパイラ エラーや設計の問題を分離しやすくなります。

モデルとストレージ サービスを要求する

次のプロンプトを使います。

Add the data layer for TaskTally.

- Create a TaskItem model with an ID, title, and completion state.
- Use MVVM Toolkit observable properties.
- Create a TaskStorage service that serializes the task list to JSON.
- Store tasks in ApplicationData.Current.LocalFolder because this is
  a packaged WinUI 3 app.
- Return an empty list when the file doesn't exist.
- Serialize save operations so rapid changes don't write the file concurrently.
- Surface real read and write errors to the caller.
- Build the project after the change.

生成されたモデルを次の実装と比較します。

using CommunityToolkit.Mvvm.ComponentModel;

namespace TaskTally.Models;

public partial class TaskItem : ObservableObject
{
    public Guid Id { get; init; } = Guid.NewGuid();

    [ObservableProperty]
    public partial string Title { get; set; } = string.Empty;

    [ObservableProperty]
    public partial bool IsComplete { get; set; }
}

[ObservableProperty]属性は、パブリック プロパティと変更通知を生成します。 MVVM Toolkit ソース ジェネレーターがそのコードを追加できるように、クラスを partial する必要があります。

ストレージ サービスは次のようになります。

using System.Text.Json;
using System.Threading;
using TaskTally.Models;
using Windows.Storage;

namespace TaskTally.Services;

internal static class TaskStorage
{
    private const string FileName = "tasks.json";
    private static readonly SemaphoreSlim SaveLock = new(1, 1);

    public static async Task<IReadOnlyList<TaskItem>> LoadAsync()
    {
        StorageFile? file =
            await ApplicationData.Current.LocalFolder.TryGetItemAsync(FileName) as StorageFile;

        if (file is null)
        {
            return [];
        }

        string json = await FileIO.ReadTextAsync(file);
        return JsonSerializer.Deserialize<List<TaskItem>>(json) ?? [];
    }

    public static async Task SaveAsync(IEnumerable<TaskItem> tasks)
    {
        await SaveLock.WaitAsync();
        try
        {
            string json = JsonSerializer.Serialize(tasks, new JsonSerializerOptions
            {
                WriteIndented = true
            });

            StorageFile file = await ApplicationData.Current.LocalFolder.CreateFileAsync(
                FileName,
                CreationCollisionOption.ReplaceExisting);

            await FileIO.WriteTextAsync(file, json);
        }
        finally
        {
            SaveLock.Release();
        }
    }
}

ApplicationData.Current.LocalFolder は、アプリにパッケージ ID があるため、ここでは適切です。 SaveLock では、追加、削除、および完了操作で同じファイルが同時に書き込まれるのを防ぎます。 後で配布モデルを変更する場合は、このストレージの選択を自動的にコピーするのではなく、パッケージ ID の前提条件をもう一度確認してください。

詳細情報: 設定とその他のアプリ データとApplicationData.LocalFolder を格納および取得します。

ビュー モデルを要求する

次に、次のプロンプトを使用します。

Add MainPageViewModel.

- Expose an ObservableCollection<TaskItem>.
- Add observable properties for the new-task text and status message.
- Add asynchronous commands to add and delete tasks.
- Disable AddTaskCommand when the title is blank.
- Load tasks once when the page starts.
- Save after add, delete, or completion changes.
- Expose a summary and empty-state property for x:Bind.
- Catch storage exceptions only where the UI can report them.
- Build and fix all warnings.

応答で検査する主なパターンは次のとおりです。

public ObservableCollection<TaskItem> Tasks { get; } = [];

[ObservableProperty]
public partial string NewTaskTitle { get; set; } = string.Empty;

public string Summary =>
    Tasks.Count == 0
        ? "No tasks yet"
        : $"{Tasks.Count(task => !task.IsComplete)} of {Tasks.Count} remaining";

[RelayCommand(CanExecute = nameof(CanAddTask))]
private async Task AddTaskAsync()
{
    string title = NewTaskTitle.Trim();
    Tasks.Add(new TaskItem { Title = title });
    NewTaskTitle = string.Empty;
    RefreshTaskState();
    await SaveAsync($"Added \"{title}\".");
}

[RelayCommand]
private async Task DeleteTaskAsync(TaskItem task)
{
    Tasks.Remove(task);
    RefreshTaskState();
    await SaveAsync($"Deleted \"{task.Title}\".");
}

private bool CanAddTask() => !string.IsNullOrWhiteSpace(NewTaskTitle);

partial void OnNewTaskTitleChanged(string value)
{
    AddTaskCommand.NotifyCanExecuteChanged();
}

アシスタントが .Wait() または .Resultでの非同期作業をブロックしていないことを確認します。 ストレージ操作は待機する必要があります。

ビュー モデルは、ストレージ例外をページのメッセージに変換するときにキャッチできます。

private async Task SaveAsync(string successMessage)
{
    try
    {
        await TaskStorage.SaveAsync(Tasks);
        ShowStatus(successMessage);
    }
    catch (Exception ex)
    {
        ShowStatus($"Changes couldn't be saved: {ex.Message}");
    }
}

これはサイレント フォールバックではありません。ユーザーはエラーを受け取り、ビュー モデルは保存が成功したと主張しません。

ビルドと検査

x64 用のビルド。 警告を作業項目として扱います。特にバインディングと null 許容参照の警告:

dotnet build -p:Platform=x64

追加したすべてのパッケージについて説明するようにアシスタントに依頼します。 このアプリでは、テンプレート内の Windows アプリ SDK および MVVM Toolkit 参照を超える新しいパッケージは必要ありません。