Power Apps 編集グリッドのデータ保存|Patch・IsNew・削除・一括保存の実装パターン

編集グリッドの見た目を作るだけなら前の記事で完成しますが、実際に使えるアプリにするには保存・削除・一括保存のロジックが必要です。Patch関数とコレクション操作を組み合わせた実装パターンを一通り解説します。

コレクションを中間バッファとして使う設計の基礎は、編集グリッドの基本設計の記事で解説しています。この記事はその続きとして、データの書き込み操作に特化した内容です。

行ごとの保存:フロッピーアイコン+Patch

編集グリッドでよく使われる行ごとの保存パターンは、ギャラリーテンプレート内にアイコン(フロッピーディスク)を置き、OnSelect に Patch を書くというものです。

Patch(Tasks, ThisItem,
  {
    タスク名: TextInput_タスク名.Text,
    優先度: Dropdown_優先度.Selected.Value,
    担当者: TextInput_担当者.Text
  }
)

Patch の第1引数はデータソース(Tasks)、第2引数は更新対象のレコード(ThisItem)、第3引数は更新するフィールドと値のレコードです。ThisItem を指定することで、クリックした行のデータだけを更新できます。

Patch 関数の使い方の基本はPatch 関数の解説記事で詳しく触れているので、あわせて確認しておくとより理解が深まります。

Value 関数で数値型変換が必要な理由

TextInput から取り出した値は常にテキスト型です。数値列に Patch しようとするとエラーになります。

Patch(Tasks, ThisItem,
  {
    タスク名: TextInput_タスク名.Text,
    工数: Value(TextInput_工数.Text)
  }
)

Value(TextInput_工数.Text) のように Value 関数で数値に変換してから渡します。入力欄が空のとき Value 関数は Blank を返すため、空白のまま保存されます。空白を 0 として扱いたい場合は Coalesce(Value(TextInput_工数.Text), 0) とすると安全です。

IsNew フラグの役割

編集グリッドで行を追加するとき、既存のレコードと新規で追加した行を区別する仕組みが必要です。そのために IsNew フラグをコレクションに持たせます。

新規行を追加するボタンの OnSelect はこのようになります。

Collect(colTasks,
  {タスク名: "", 優先度: "中", 担当者: "", 工数: 0, IsNew: true}
)

IsNew: true を持つ行が新規追加行です。既存データを読み込んだ行には IsNew: false または IsNew フィールド自体がないため、Patch するときに新規か更新かを判断できます。

IsNew フラグを使った保存ロジックはこうなります。

If(ThisItem.IsNew,
  Patch(Tasks, Defaults(Tasks),
    {タスク名: TextInput_タスク名.Text, 優先度: Dropdown_優先度.Selected.Value}
  ),
  Patch(Tasks, ThisItem,
    {タスク名: TextInput_タスク名.Text, 優先度: Dropdown_優先度.Selected.Value}
  )
)

IsNew が true の場合は Defaults(Tasks) を使って新規レコードとして作成し、false の場合は ThisItem を指定して既存レコードを更新します。Defaults(Tasks) はデータソースのデフォルト値を持つ空のレコードを返します。これが新規作成のサインです。

削除ボタンの実装

削除も IsNew フラグで分岐します。新規行(まだデータソースに存在しない行)はコレクションから削除するだけで済みますが、既存行はデータソースからも削除が必要です。

If(ThisItem.IsNew,
  Remove(colTasks, ThisItem),
  RemoveIf(Tasks, ID = ThisItem.ID);
  Remove(colTasks, ThisItem)
)

IsNew が true なら Remove(colTasks, ThisItem) でコレクションから取り除くだけです。既存行は RemoveIf でデータソースから削除した後、コレクションからも Remove します。セミコロンで複数の処理をつなぐことで1つの OnSelect にまとめられます。

削除確認ダイアログを入れる

削除は元に戻せません。誤操作を防ぐために確認ダイアログを挟むのが安全です。コンテナのVisibleを変数で制御する方法が一般的です。

  1. 削除ボタンの OnSelect:UpdateContext({varDeleteTarget: ThisItem, varShowDeleteDialog: true})
  2. 確認ダイアログコンテナの Visible:varShowDeleteDialog
  3. はいボタンの OnSelect:上記の削除ロジック実行 → UpdateContext({varShowDeleteDialog: false})
  4. いいえボタンの OnSelect:UpdateContext({varShowDeleteDialog: false})

varDeleteTarget には削除対象の行(ThisItem)を入れておき、はいボタンの削除処理で varDeleteTarget.ID を参照して RemoveIf することで、正確な行を削除できます。

全件保存ボタンの実装

行ごとの保存ではなく、まとめて一括保存したい場合は ForAll を使います。コレクション全体をループして、IsNew フラグに応じて新規作成か更新かを切り替えます。

ForAll(
  Filter(colTasks, タスク名 <> ""),
  If(IsNew,
    Patch(Tasks, Defaults(Tasks),
      {タスク名: タスク名, 優先度: 優先度, 担当者: 担当者, 工数: 工数}
    ),
    Patch(Tasks, LookUp(Tasks, ID = ThisRecord.ID),
      {タスク名: タスク名, 優先度: 優先度, 担当者: 担当者, 工数: 工数}
    )
  )
)

ForAll の第1引数には Filter(colTasks, タスク名 <> "") を使って空白行を除外してから渡します。タスク名が空の行を保存対象から外すことで、新規行ボタンを押しただけでまだ何も入力していない行が誤って保存されるのを防げます。

既存行の更新には LookUp(Tasks, ID = ThisRecord.ID) で正確なレコードを取得してから Patch します。ForAll の内部では ThisItem の代わりに ThisRecord を使う点に注意が必要です。

保存後にコレクションを再取得する

一括保存が完了したら、コレクションをデータソースから再取得してリフレッシュします。

ForAll(Filter(colTasks, タスク名 <> ""), ...);
ClearCollect(colTasks, Tasks)

ForAll の後に ClearCollect でコレクションを上書きすることで、IsNew フラグがリセットされ、保存済みのデータが最新の状態で表示されます。この一行を忘れると、保存後も IsNew: true の行が残ってしまい、再度保存すると重複レコードが作られるという事故につながります。

まとめ:編集グリッドの保存ロジック全体像

操作実装
行ごと保存Patch(DataSource, ThisItem, {...}) with Value() for 数値
新規行追加Collect(colTasks, {IsNew: true, ...})
新規行保存Patch(DataSource, Defaults(DataSource), {...})
削除(新規行)Remove(colTasks, ThisItem)
削除(既存行)RemoveIf + Remove(colTasks, ThisItem)
一括保存ForAll(Filter(...)) + IsNew で分岐
保存後リフレッシュClearCollect(colTasks, DataSource)

編集グリッドは最初の設計さえ固まれば、後は同じパターンを繰り返すだけです。私も最初に実装したときは ForAll の中での ThisRecord の使い方に戸惑いましたが、一度動くものを作ると次からは迷わなくなります。コレクション操作の基礎についてはコレクション活用入門の記事もあわせて確認してみてください。地道に実装して、使いやすいグリッドUIを完成させましょう。

ギャラリーの使い方を一覧でまとめた記事はこちらからご覧ください。

Xでフォローしよう