Power Appsで学習記録アプリを作る

概要

学習記録などの実績を記録するアプリの作り方のヒントです。
予め設定されたマスター項目(例:宿題)にそって実績(終わった/終わっていない)を記録できるような設計のアプリです。
データベース構造やアプリ設計の工夫が必要です。

学習記録アプリの作成をお手伝いする機会がありましたので、
ポイントを絞って解説してみたいと思います。

これが理解できると、以下のようなアプリに応用できます。

  • 生徒の学習記録アプリ(宿題や項目に沿って実績登録)
  • 従業員の人事評価アプリ(部署内の人員に対して評価)
  • 作業の点検記録アプリ(既定の項目に沿って点検)

例題アプリと利用シナリオ

今回は、学校などで利用想定されるアプリを例にします。
先生が予め設定した宿題に沿って、各生徒が完了報告登録をしていくようなアプリを想定します。
シナリオは以下です。

  • ①事前に、先生は教科ごとの宿題をマスターテーブルに登録する。
  • ②生徒がアプリにアクセスして、教科を選ぶ。
  • ③宿題一覧画面が表示され、生徒は終わった宿題についてチェックを入れて保存する。

①のイメージ(宿題の一覧)

②のイメージ(アプリのホーム画面)

③のイメージ(宿題完了登録画面)



アプリ開発のポイント

工夫が必要なポイントは以下です。

外部結合の考え方

事前に登録したマスターに基づいて、データを登録していくようなアプリ構造になります。
宿題リスト(マスターテーブル)と実績記録リスト(トランザクションテーブル)の2つのテーブルを、アプリ内で外部結合する考え方が必要になりますね。
設定された列に沿って入力していくだけの単純なアプリとは違うところです。

完了実績の登録

ギャラリーコントロールの中にトグルボタンを配置しておき、終わった宿題には生徒がチェックを入れます。
そして、保存ボタンを押したタイミングで一括保存を行います。
一括保存の際は、チェックを入れたレコードのみ保存を行うように工夫します。
※最後の方でチェックしたタイミングで保存する例も載せます。

必要になるテーブル

SharePointリストを使って、次の3つのテーブルを用意します。

科目マスター 科目ごとに完了登録したいため、マスターを用意します。

宿題マスター 宿題マスターとなるテーブルです。
宿題事項の一覧は、このリストに登録しておきます。
科目Idは参照列で、科目マスターを参照しておきます。

宿題完了記録(トランザクション)
- 宿題Id列:参照列として、宿題マスターのId列の参照 - 教科Id列:参照列として、科目マスターの教科名を参照。

アプリの解説

全て解説すると大変なので、ポイントを絞ります。
基本的な部分は自身で調べてください。

AppStart

OnStartでユーザー情報を変数にセットします。
User()関数で取得できるEmailはM365のログインIDであり、必ずしもメールアドレスとは一致しない場合があるので365コネクタを利用します。

// OnStart
Set(varUserInfo,
    {
        Email:Office365ユーザー.MyProfileV2().mail,
        DisplayName:Office365ユーザー.MyProfileV2().displayName
    }
)

ホーム画面

ギャラリーコントロールに科目マスターを設定。

OnSelectで画面遷移し、第三引数で教科名をパラメータとして渡す。

// OnSelect
Navigate(完了状態入力画面,ScreenTransition.Cover,{_kyokaMei:ThisItem.教科名})

実績登録画面

流れ

  • 宿題マスターと宿題完了記録を結合してコレクションにセットしておく。
    これは画面を開いたとき、保存を行った後、の共通処理になるため、ボタンのOnSelectに関数を書いて共通化する。
    テーブルの結合はLookUp関数で。ただしデータソースに対してLookUpしないことがポイント
  • ギャラリーに宿題一覧とトグルボタンに実績情報を表示。オンにすると「未保存」と表示される。 -- 保存ボタンを押すと未保存レコードのみ保存処理が実行される。

データ更新ボタン(共有処理)

「Button_データ更新」という名前でボタンを配置し、OnSelectに関数を入力します。
主に、宿題マスターと宿題完了記録を結合してコレクションにセットする処理です。

①画面を開いたとき、②保存を行った後の共通処理になるため、ボタンの一か所に関数を書いておいてOnVisibleや保存後にボタンをSelect()することで、
同じ関数をそれぞれに書く必要が無くなるというテクニックです。

// 宿題一覧レコードを取得
With(
    {
        // 自分の宿題記録を一時変数に取得
        _achievements:Filter(
            R28_宿題完了記録,
            ThisRecord.ユーザーメール = varUserInfo.Email && ThisRecord.教科Id.Value = Text(_kyokaMei)
        )
    },
    ClearCollect(
        colHomeworks,
        AddColumns(
            // 宿題マスターを取得
            Filter(R28_宿題マスター,ThisRecord.教科Id.Value = Text(_kyokaMei)) As Outer,
            // 宿題記録を結合
            "宿題完了記録",
            LookUp(_achievements As Inner,Inner.宿題Id.Value = Text(Outer.ID))
        )
    )
);
// ボタン有効化
UpdateContext({_canExecute:true});


解説

  1. まず一時変数「_achievements」に「R28_宿題完了記録」から自分の実績情報を取得してセットします。
    一度に複数行データ取得を行います。既定では500行までしか取得できないので注意してください。
  2. AddColumns関数を使って、R28_宿題マスターに列を追加します。
    「宿題完了記録」の列名でLookUp関数を使って「R28_宿題完了記録」を結合します。
    宿題完了記録列には宿題Idが一致する「レコード」型のデータが格納されています。
    注意:LookUpの第一引数に直接データソースを指定してしまうと、一行ずつデータを取りに行くため処理が遅くなりますし、ネットワークの調整制限がかかる恐れがあります。
  3. 式中のAs Outer、As InnerはFilterやLookUpといった関数を入れ子にして使用する場合、フィルター文の○○=△△が、どちらのデータソースを指すかPower Appsが判断できないため、ThisRecordではなく任意の名前に置き換えるといった処理です。
  4. これで、「colHomeworks」変数に結合されたデータが格納されました。
  5. 最後の「_canExecute」は、後でデータ更新処理中に保存ボタンを無効化しますが、それを再有効化するための変数です。

画面のOnVisible

以下のように入力します。

//トグルコントロールをリセット(false→true)
UpdateContext({_resetCheck:false});
UpdateContext({_resetCheck:true});

// ボタン無効化
UpdateContext({_canExecute:false});
// データ更新ボタン選択
Select(Button_データ更新);

解説
「_resetCheck」:
ギャラリーコントロールの中のコントロール、今回はトグルスイッチをリセットする処理です。

_canExecute:
保存ボタンを無効化するための変数です。

Select(Button_データ更新):
データ更新ボタンを押して更新処理を実行します。 このボタンは非表示でも動きます。

左のギャラリー

ギャラリーコントロール Items:「colHomeworks」コレクション変数をセット
Visible:「_canExecute」変数をセット

レイアウトは以下

トグルコントロール
プロパティは以下です。
Defaultには先ほど結合した宿題完了記録の完了状態を反映します。
ただし、Default値が反映されるのは一度きりです。 コントロールをユーザーが操作すると次回以降はDefaultの値が反映されません。
なので、上記のように画面のOnVisibleでResetプロパティをfalse→trueに切り替える工夫が必要です。



未保存ラベル:
テキストラベルを配置します。
名前は「Label_完了状況」とします。
Textには以下を入力します。
データソースの宿題完了状態と、トグルの状態を比較して、異なっていれば「未保存」と表示します。
宿題完了記録がない場合はBlankになりますので、その場合はトグルがtrueの場合だけ「未保存」と表示します。

// 未保存状態を表示する
If(!IsBlank(ThisItem.宿題完了記録),
    // 
    If(ThisItem.宿題完了記録.TaskState = Toggle_宿題完了状態.Value,"","未保存"),
    If(Toggle_宿題完了状態.Value=true,"未保存")
)

保存ボタン

DisplayMode:

// 未完了のレコードが1件以上かつ_canExecuteがtrueなら編集モード
If(
    CountRows(Filter(Gallery_宿題完了状況.AllItems,ThisRecord.Label_完了状況.Text = "未保存")) >= 1 && _canExecute,
    DisplayMode.Edit,
    DisplayMode.Disabled
)


OnSelect:

Notify("完了状況を保存中です。画面を閉じないでください",NotificationType.Information);
// ボタン無効化
UpdateContext({_canExecute:false});
// 未保存のレコードを一件ずつ宿題記録に保存する
ForAll(
    // 未保存のレコードにフィルター
    Filter(Gallery_宿題完了状況.AllItems,Label_完了状況.Text = "未保存") As Outer,
    
    // 一行ずつレコードを登録
    If(!IsBlank(Outer.宿題完了記録),
        //既存レコードの場合、編集登録
        Patch(R28_宿題完了記録,Outer.宿題完了記録,
            {
                完了状態:Outer.Toggle_宿題完了状態.Value
            }
        ),
        // 新規レコードの場合、新規登録
        Patch(R28_宿題完了記録,Defaults(R28_宿題完了記録),
            {
                宿題Id:{Id:Outer.ID,Value:Outer.ID},
                教科Id:{Id:Outer.教科Id.Id , Value:Outer.教科Id.Value},
                完了状態:Outer.Toggle_宿題完了状態.Value,
                ユーザーメール:varUserInfo.Email
            }
        )
    )

);

// レコード再取得
Select(Button_データ更新);

解説
ForAll関数を使って、ギャラリーコントロールのデータを元に一行ずつデータの保存を行っていきます。

「 未保存のレコードにフィルター」の行では、Filter関数によりギャラリーコントロールのラベルに「未保存」と表示されている行にフィルターします。
これにより編集を行ったレコードのみ保存処理を行います。

「 一行ずつレコードを登録」の行では、宿題完了記録がBlankかどうかで「新規」、「編集」を分岐し、登録モードに応じたPatch関数によりデータソースにデータを登録します。

最後に「Button_データ更新」ボタンを押して、更新後のデータを再取得してギャラリーに表示します。
この間、「_canExecute」変数の操作により、保存ボタンの二度押しを防いだり、ギャラリーコントロールを非表示にする処理を行っています。

ポイント解説は以上です。
外部結合の考え方を使ったアプリの開発、ご参考になれば幸いです。