[ Siena 目次 ]

備忘録アプリ

複数のメモ書きを入力・保存できるアプリを試作します。
タイトルや本文をキーワードで検索できるようにして、利便性を上げます。
「ブログ型情報活用システム」の中で書いた情報活用の問題意識を、個人の最小レベルに置き換えて取り組みました。


実際の画面を確認

画面は、表紙・メイン・タグ管理の3面です。

シンプルなオープニング画面で始まります。数秒したら、自動でメイン画面に移ります。
このあたりのテクニックは、表紙画面のサンプルに詳細を記述していますので、興味のある方はご覧ください。
スクリーン1
オープニング画面の挿入には、初期処理を入れるタイミングとして使用する意味もあります。
スクリーン2
メイン画面です。画面右下の検索機能を使用して、タイトルや本文のキーワード検索や、タグによる絞込みができます。 今回は単一項目検索にしていますが、工夫次第で複合検索も可能だと思います。 画面上で New / Mod のどちらかのモードを選び、新規入力もしくは登録済のメモを更新します。
スクリーン3
これは新規入力中の画面です。タイトル・タグ・本文を入力して更新ボタンを押すと、データを登録します。 タグは直接入力も可能ですが、あらかじめタグ登録をしておけば画面右側に候補が表示され、追加ボタンでタグ文字列に追加できます。
スクリーン4
これは更新中の画面です。タイトル・タグ・本文を変更して更新ボタンを押すと、データを更新します。 更新したかどうかは、更新日時(タイムスタンプ)で確認できます。
スクリーン5
これはタグ管理画面です。ここで登録した文言が、タグ候補として表示されます。


以下に、作成時のポイントを記述します。

[ 目次 ]
  1. 実際の画面を確認
  2. 事前準備
  3. 仕様確認
  4. ストリング変数を使ったテキストボックスの初期化について
  5. 仕組みの解説
  6. ― 表紙画面 ― ― メイン画面 ― ― タグ管理画面 ―
  7. 実現を見送ったこと
  8. 感想



事前準備

画像も動画もエクセルテーブルも使用しないので、準備は不要です。



仕様確認

  1. テキストベースのメモを複数登録し、保存できます。
  2. 登録したメモの更新・削除ができます。
  3. キーワードを使用して、タイトルや本文を検索できます。
    検索結果はギャラリーに表示します。
  4. 登録されたタグにより、既存メモの絞り込みができます。
    絞り込み結果をギャラリーに表示します。
  5. タグ登録機能によりあらかじめ文言を登録すれば、新規登録時にその文言を選択できます。



ストリング変数を使ったテキストボックスの初期化について

ストリング変数を使ってテキストボックスを初期化したり、入力値を他の処理で利用する仕組みを、タイトルボックスを例に、簡単にまとめておきます。

  1. タイトルボックスの Default 属性に、その項目用のストリング変数(今回は StringTitle 変数)を指定します。
  2. 初期化したいタイミング(メモの登録ボタン押下後など)で、そのストリング変数に "" (空白) を転送します。 UpdateContext({StringTitle:""}) といった具合です。
  3. タイトルボックスの入力値をストリング変数に保存したいときは、
    OnChange 属性に、 UpdateContext({StringTitle:InputTitle!Text})
    と処理を指定し、値が変更されたときに転送処理を行います。
    これにより、ストリング変数に入力値が入ります。

これで初期化が実現できました。
検索・照会画面のサンプルの時より、少しわかりやすくなったと思っています。



仕組みの解説

ここでは、貼り付けた部品の属性を ExpressView で確認します。
部品を選んで右下の ExpressView ボタンをクリックすれば、画面右に開きます。
以下、画像をクリックすると拡大画面が表示されます。

― 表紙画面 ―
  1. タイマー
  2. タイマーが終了したら次画面に遷移するようにしていますが、同時に、このタイミングを利用して、いくつかの初期処理を行っています。
    その処理は、すべて TimerEnd 属性に記述しています。


    タイマー1
    Interval 属性を 0.4 にして、これまでよりカウントダウンの拍子を早くしてみました。この方が感触がいいです。

― メイン画面 ―
  1. ギャラリー
  2. 登録されているメモを表示します。
    Items 属性で、
    If(DispMode="All",Sort(Memos,TimeStamp,SortOrder!Descending),
    If(DispMode="SearchTitle",Sort(Filter(Memos,StringTitleSearch in Title),TimeStamp,SortOrder!Descending),
    If(DispMode="SearchTag",Sort(Filter(Memos,StringTagSearch in StrTags),TimeStamp,SortOrder!Descending),
    Sort(Filter(Memos,StringMemoSearch in Memo),TimeStamp,SortOrder!Descending))))

    と指定している部分がそれです。

    メモ一覧の表示モードによって、全件/タイトル検索後/指定タグによる絞込み後/本文検索後を使い分けて表示しています。

    表示用のコレクションを準備して、検索後のデータをそこに入れてもよかったのですが、
    そうすると、登録や更新の度に、データ本体と表示用コレクションの両方を更新しなければならないので、 そのアイディアは止めました。

    常にデータ本体を更新し、表示もデータ本体から行っています。
    データの二重管理を行わない発想と同様です。

    登録項目である TimeStamp で降順にしています。
    ギャラリー1
    表示順を変える機能を付けるつもりでしたが、これが意外にくせ者だったので、今回は諦めました。 そのあたりの経緯を「実現を見送ったこと」で振り返っています。

  3. タイトルラベル
  4. メモ書きのタイトルを表示します。
    Text 属性で、 ThisItem!Title と指定している部分がそれです。
    タイトルラベル1

  5. タイムスタンプラベル
  6. メモ書きのタイムスタンプを表示します。
    Text 属性で、 ThisItem!TimeStamp と指定している部分がそれです。
    タイムスタンプラベル1

  7. タグラベル
  8. メモ書きのタグ文字列を表示します。
    Text 属性で、 ThisItem!StrTags と指定している部分がそれです。
    タグラベル1

  9. イベント用部品
  10. ギャラリー内のメモがクリックされた時、選択されたメモの値をストリング変数に転送します。
    OnSelect 属性で、
    UpdateContext({StringTitle:GalleryMemos!Selected!Title,StringTags:GalleryMemos!Selected!StrTags,
    StringMemo:GalleryMemos!Selected!Memo,StringTime:GalleryMemos!Selected!TimeStamp})

    と指定している部分がそれです。
    各ストリング変数に、選択されたメモの項目を転送しています。

    この部品は、四角形の図形を透明にしているので姿は見えていませんが、クリックイベントを拾う役割をしています。 タイトル等のラベルより上の層に配置してください。
    イベント用部品1
    部品が重なっている場所で、部品を上層や下層に移動したいときは、画面下の Arrange ボタンの Order メニューを使って対応します。

  11. モード選択ドロップダウン
  12. モードを表す選択肢の値を指定しています。
    Items 属性で ["New","Mod"] と指定している部分がそれです。
    初期値は Default 属性で Mode と指定し、ストリング変数の値を使用します。

    選択肢が変更されたときに、モード変数を変更し、入力ボックスを初期化しています。
    OnChange 属性で、
    If(Mode<>DropdownMode!Selected!Value,If(DropdownMode!Selected!Value="New",UpdateContext({Mode:"New"}), UpdateContext({Mode:"Mod"}));UpdateContext({ StringTitle:"",StringTags:"",StringMemo:"",StringTime:""}))
    と指定している部分がそれです。
    モード選択ドロップダウン1

  13. 登録ボタン
  14. 新規入力のメモを登録します。
    OnSelect 属性で、
    Collect(Memos, {TimeStamp:Text(Now(),"yyyy/mm/dd hh:mm:ss") ,
    Title:StringTitle,Memo:StringMemo,StrTags:StringTags});

    と指定している部分がそれです。

    また、登録後のメモデータ全件を保存しています。
    OnSelect 属性で、 SaveData(Memos,"MemosDataMemos");
    と指定している部分がそれです。

    登録後、編集用のストリング変数を初期化しています。
    OnSelect 属性で、 UpdateContext({StringTitle:"",StringTags:"",StringMemo:"",StringTime:""})
    と指定している部分がそれです。

    空データが登録されないようにしています。
    Disabled 属性で、 Or(StringTitle="",StringMemo="")
    と指定している部分がそれです。この式が true のとき、登録ボタンが無効になります。

    登録ボタンが有効の時と無効の時で、ボタンの文字色を変えています。
    Color 属性で、
    If(ButtonAdd!Disabled,RGBA(255, 255, 255, 0.3),RGBA(255, 255, 255, 1))
    と指定している部分がそれです。

    登録ボタンは、モードが" New "の時に表示し、" Mod "のときは非表示にします。
    Visible 属性で、 Mode="New" と指定している部分がそれです。
    登録ボタン1

  15. 更新ボタン
  16. 更新したメモの該当データを更新します。
    OnSelect 属性で、
    UpdateContext({StringTime:Text(Now(),"yyyy/mm/dd hh:mm:ss")});
    UpdateIf(Memos,TimeStamp=GalleryMemos!Selected!TimeStamp,
    {Title:StringTitle,StrTags:StringTags,Memo:StringMemo,TimeStamp:StringTime});

    と指定している部分がそれです。更新前に更新日時の値を編集してから処理しています。
    タイムスタンプをキーにして更新します。

    データが更新されたので、メモデータの保存をします。
    OnSelect 属性で、
    SaveData(Memos,"MemosDataMemos");
    と指定している部分がそれです。

    空データに変更されたまま登録されないようにしています。
    Disabled 属性で、 Or(StringTitle="",StringMemo="")
    と指定している部分がそれです。この式が true のとき、更新ボタンが無効になります。

    更新ボタンが有効の時と無効の時で、ボタンの文字色を変えています。
    Color 属性で、
    If(ButtonMod!Disabled,RGBA(255, 255, 255, 0.3),RGBA(255, 255, 255, 1))
    と指定している部分がそれです。

    更新ボタンは、モードが" Mod "の時に表示し、" New "のときは非表示にします。
    Visible 属性で、 Mode="Mod" と指定している部分がそれです。
    更新ボタン1

  17. 削除ボタン
  18. 該当のメモを削除します。
    OnSelect 属性で、
    RemoveIf(Memos,TimeStamp=GalleryMemos!Selected!TimeStamp);
    と指定している部分がそれです。

    また、削除後のメモデータ全件を保存しています。
    OnSelect 属性で、 SaveData(Memos,"MemosDataMemos");
    と指定している部分がそれです。

    削除後、編集用のストリング変数を初期化しています。
    OnSelect 属性で、
    UpdateContext({StringTitle:"",StringTags:"",StringMemo:"",StringTime:""})
    と指定している部分がそれです。

    削除ボタンが有効の時と無効の時で、ボタンの文字色を変えています。
    Color 属性で、
    If(ButtonDel!Disabled,RGBA(255, 255, 255, 0.3),RGBA(255, 255, 255, 1))
    と指定している部分がそれです。

    削除ボタンは、モードが" Mod "の時に表示し、" New "のときは非表示にします。
    Visible 属性で、 Mode="Mod" と指定している部分がそれです。
    削除ボタン1

  19. タイトルボックス
  20. 編集用のストリング変数を表示します。
    Default 属性で、 StringTitle と指定している部分がそれです。
    イベント用部品によって、このストリング変数に値が転送される仕組みです。

    新規入力の時は、空白になっています。
    更新時は、選択された既存メモを表示します。
    新規入力の時も、既存のメモから加工して新規作成したいケースを考慮し、選択された既存メモを表示するようになっています。

    値が変更されたときに、入力値をストリング変数に転送しています。
    OnChange 属性で、 UpdateContext({StringTitle:InputTitle!Text})
    と指定している部分がそれです。

    更新時、ギャラリーのメモが0件の時は、入力不可にします。
    Disabled 属性で、 And(Mode="Mod",CountRows(GalleryMemos!AllItems)=0)
    と指定している部分がそれです。
    タイトルボックス1

  21. タグボックス
  22. 編集用のストリング変数を表示します。
    Default 属性で、 StringTags と指定している部分がそれです。

    値が変更されたときに、入力値をストリング変数に転送しています。
    OnChange 属性で、 UpdateContext({StringTags:InputTags!Text})
    と指定している部分がそれです。

    更新時、ギャラリーのメモが0件の時は、入力不可にします。
    Disabled 属性で、 And(Mode="Mod",CountRows(GalleryMemos!AllItems)=0)
    と指定している部分がそれです。
    タグボックス1

  23. タグ消去ボタン
  24. タグボックスの値を消去します。
    OnSelect 属性で、 UpdateContext({StringTags:""})
    と指定している部分がそれです。
    タグ消去ボタン1

  25. 本文ボックス
  26. 編集用のストリング変数を表示します。
    Default 属性で、 StringMemo と指定している部分がそれです。

    値が変更されたときに、入力値をストリング変数に転送しています。
    OnChange 属性で、 UpdateContext({StringMemo:InputMemo!Text})
    と指定している部分がそれです。

    更新時、ギャラリーのメモが0件の時は、入力不可にします。
    Disabled 属性で、 And(Mode="Mod",CountRows(GalleryMemos!AllItems)=0)
    と指定している部分がそれです。

    本文は複数行にわたるので、複数行の属性を指定します。
    Mode 属性で、 TextMode!MultiLine と指定している部分がそれです。
    本文ボックス1

  27. タイムスタンプラベル(表示)
  28. 編集用のストリング変数を表示します。
    Default 属性で、 StringTime と指定している部分がそれです。
    タイムスタンプは入力値ではないので、ラベルを使っています。
    タイムスタンプラベル(表示)1

  29. ギャラリー(タグ候補用)
  30. タグ登録されている文言の一覧を表示します。
    Items 属性で、 Sort(TagsTBL,Word,Ascending)
    と指定している部分がそれです。
    ギャラリー(タグ候補用)1
    ソートしているつもりなのですが、並び順が今ひとつしっくりこないのは、英語版だからかもしれません。

  31. タグ追加ボタン
  32. 選択されたタグを、タグボックスの値に追加します。
    OnSelect 属性で、
    UpdateContext({StringTags:StringTags & GalleryTagsChoices!Selected!Word & ","})
    と指定している部分がそれです。
    タグ追加ボタン1

  33. キーワードボックス(タイトル用)
  34. 検索用のストリング変数を表示します。
    Default 属性で、 StringTitleSearch と指定している部分がそれです。

    入力値が変更されたら、その値をストリング変数に転送しています。
    OnChange 属性で、 UpdateContext({StringTitleSearch:InputTitleSearch!Text})
    と指定している部分がそれです。
    キーワードボックス(タイトル用)1
    上記の「ストリング変数を使ったテキストボックスの初期化について」に、初期化の仕組みを簡単に記しています。

  35. 検索ボタン(タイトル用)
  36. 入力されたキーワードを含むタイトルのメモ一覧を表示します。
    また、今回はその他の項目との複合検索をしていないので、その他の項目の検索値をクリアします。
    OnSelect 属性で、
    UpdateContext({DispMode:"SearchTitle", StringMemoSearch:"",StringTagSearch:""});
    と指定している部分がそれです。

    更新モード時は、編集用のストリング変数を初期化します。
    OnSelect 属性で、
    If(Mode="Mod",UpdateContext({StringTitle:"",StringTags:"",StringMemo:"",StringTime:""}))
    と指定している部分がそれです。

    検索値がない場合はボタンを押せないようにしています。
    Disabled 属性で、 InputTitleSearch!Text=""
    と指定している部分がそれです。この式が true であれば、ボタンを使用することができません。

    検索値がない場合にボタンが押せないことを示すため、文字色を変えています。
    Color 属性で、
    If(ButtonSearchTitle!Disabled,RGBA(255, 255, 255, 0.4),RGBA(255, 255, 255, 1))
    と指定している部分がそれです。
    検索ボタン(タイトル用)1
    今回は単一項目検索にしましたが、複合検索も十分に対応可能だと思います。ただ、コーディング量がどんどん増えてしまうので、 Project Siena の考え方からずれてしまう、という意味もあります。

  37. キーワードボックス(本文用)
  38. 検索用のストリング変数を表示します。
    Default 属性で、 StringMemoSearch と指定している部分がそれです。

    また、入力値が変更されたら、その値をストリング変数に転送しています。
    OnChange 属性で、 UpdateContext({StringMemoSearch:InputMemoSearch!Text})
    と指定している部分がそれです。
    キーワードボックス(本文用)1
    上記の「ストリング変数を使ったテキストボックスの初期化について」に、初期化の仕組みを簡単に記しています。

  39. 検索ボタン(本文用)
  40. 入力されたキーワードを含む本文のメモ一覧を表示します。
    また、今回はその他の項目との複合検索をしていないので、その他の項目の検索値をクリアします。
    OnSelect 属性で、
    UpdateContext({DispMode:"SearchMemo",StringTitleSearch:"",StringTagSearch:""});
    と指定している部分がそれです。

    更新モード時は、編集用のストリング変数を初期化します。
    OnSelect 属性で、
    If(Mode="Mod",UpdateContext({StringTitle:"",StringTags:"",StringMemo:"",StringTime:""}))
    と指定している部分がそれです。

    検索値がない場合はボタンを押せないようにしています。
    Disabled 属性で、 InputMemoSearch!Text=""
    と指定している部分がそれです。この式が true であれば、ボタンを使用することができません。

    検索値がない場合にボタンが押せないことを示すため、文字色を変えています。
    Color 属性で、
    If(ButtonSearchMemo!Disabled,RGBA(255, 255, 255, 0.4),RGBA(255, 255, 255, 1))
    と指定している部分がそれです。
    検索ボタン(本文用)1

  41. ドロップダウン(タグ絞込用)
  42. ドロップダウンに、検索用のタグを表示します。
    Items 属性で、 Sort(wkTags,Word,Ascending)
    と指定している部分がそれです。

    初期化しやすいように、ストリング変数を指定しています。
    Default 属性で、 StringTagSearch と指定している部分がそれです。

    選択値が変更されたら、その値をストリング変数に転送しています。
    OnChange 属性で、 UpdateContext({StringTagSearch:DropTags!Selected!Value})
    と指定している部分がそれです。
    ドロップダウン(タグ絞込用)1
    無選択用の空白は、表紙画面の処理の中で登録しています。

  43. タグ絞込ボタン
  44. 選択されたタグを含むメモ一覧を表示します。
    また、今回はその他の項目との複合検索をしていないので、その他の項目の検索値をクリアします。
    OnSelect 属性で、
    UpdateContext({DispMode:"SearchTag", StringTitleSearch:"",StringMemoSearch:""});
    と指定している部分がそれです。

    更新モード時は、編集用のストリング変数を初期化します。
    OnSelect 属性で、
    If(Mode="Mod",UpdateContext({StringTitle:"",StringTags:"",StringMemo:"",StringTime:""}))
    と指定している部分がそれです。

    無選択の場合はボタンを押せないようにしています。
    Disabled 属性で、 DropTags!Selected!Value=""
    と指定している部分がそれです。この式が true であれば、ボタンを使用することができません。

    無選択の場合にボタンが押せないことを示すため、文字色を変えています。
    Color 属性で、
    If(ButtonSearchTag!Disabled,RGBA(255, 255, 255, 0.4),RGBA(255, 255, 255, 1))
    と指定している部分がそれです。
    タグ絞込ボタン1

  45. 検索条件クリアボタン
  46. 入力された検索条件をクリアし、メモ一覧を全件表示にします。
    OnSelect 属性で、
    UpdateContext({DispMode:"All",StringTitleSearch:"",StringTagSearch:"",StringMemoSearch:""});
    と指定している部分がそれです。

    更新モード時は、編集用のストリング変数を初期化します。
    OnSelect 属性で、
    If(Mode="Mod",UpdateContext({StringTitle:"",StringTags:"",StringMemo:"",StringTime:""}))
    と指定している部分がそれです。

    検索条件クリアボタン1
    とりあえず全件表示したい時に押せるよう、使用不可等の設定はしません。

  47. タグ管理画面遷移ボタン
  48. タグ管理画面に遷移します。
    OnSelect 属性で、
    Navigate(ManageTags, ScreenTransition!Fade,{StringTag:""})
    と指定している部分がそれです。
    引数で、遷移先で使用するストリング変数の初期化を指定しています。

    タグ管理画面遷移ボタン1

― タグ管理画面 ―
  1. 文言入力ボックス
  2. 編集用のストリング変数を表示します。
    Default 属性で、 StringTag と指定している部分がそれです。

    また、入力値が変更されたら、その値をストリング変数に転送しています。
    OnChange 属性で、 UpdateContext({StringTag:InputTag!Text})
    と指定している部分がそれです。
    文言入力ボックス1

  3. タグ登録ボタン
  4. 入力された文言をタグ一覧に登録し、タグデータ全件を保存します。
    OnSelect 属性で、
    Collect(TagsTBL, {Word:StringTag});SaveData(TagsTBL,"MemosDataTags");
    と指定している部分がそれです。

    また、文言入力ボックスを初期化します。
    OnSelect 属性で、 UpdateContext({StringTag:""});
    と指定している部分がそれです。

    さらに、ドロップダウン(タグ絞込用)で使用している表示用コレクションを入れ替えます。
    OnSelect 属性で、
    Clear(wkTags);Collect(wkTags,TagsTBL);Collect(wkTags,{Word:""})
    と指定している部分がそれです。
    タグ登録ボタン1

  5. ギャラリー(登録タグ用)
  6. タグ登録されている文言の一覧を表示します。
    Items 属性で、 Sort(TagsTBL,Word,Ascending)
    と指定している部分がそれです。
    ギャラリー(登録タグ用)1

  7. タグ削除ボタン
  8. 選択されたタグを一覧から削除し、タグデータ全件を保存します。
    OnSelect 属性で、
    Remove(TagsTBL, ThisItem);SaveData(TagsTBL,"MemosDataTags")
    と指定している部分がそれです。
    タグ削除ボタン1



実現を見送ったこと

今回、 Project Siena にしては大げさに作った感がありますが、 それでも、見送った機能があります。それは、メモ一覧のソート機能です。

ギャラリー内をソートして表示すること自体は十分に可能なのですが、 そのことと、更新時の振る舞いがかみ合わなかったのです。

具体的には、ギャラリーにソート機能を盛り込んでおくと、メモの更新ボタンを押したときに、 該当データは正常に更新されるのですが、表示内容が別のメモに切り替わってしまうのです。 想像するに、ギャラリーに結びついたデータを更新すると、一旦 Selected 状態が解除され、ギャラリーの先頭のデータが Selected 状態になるのかもしれません。

本来、タイトル名とタイムスタンプの両方でソートできるようにするつもりでしたが、上記の振る舞いを踏まえ、 固定でタイムスタンプ(降順)にすることとしました。



感想

色々機能を試したり、途中で設計を変えたりしたため、思いのほか時間がかかってしまいました。 作成後に使用して気づいた点があります。

それは、 _sessionState.json なるファイルが、容量を増し続けたことです。

保存しているメモデータ・タグデータは、それとは別に存在しており(データの保存場所については「SaveData の保管場所」を参照)、 中身を見る限り、ログのようなものかと推察しましたが、くわしいことはよくわかりません。

保存目的以外のデータがいたずらに容量を増すのは望ましくないので、わざわざ出力しなくてもいいのにな、と思った次第です。 Project Siena Publish した後のデータは html JavaScript らしいので、 詳しい方であれば、余計なファイルの出力・更新を避けることができるかもしれません。

もう一つ、保存データを扱うアプリを作ると、アプリのバージョンアップの時にデータの引き継ぎが発生します。 本来であれば、そこまで考えてアプリを作成しないといけませんが、 思うに、 Project Siena でそこまで考えて作るのは、どうも筋違いな気がします。

データの保存・読込機能があるのはとても便利に思えるのですが、より広範に、それこそストアアプリで公開したいと思うような方は、 この SaveData / LoadData 機能や、 Export / Inport 機能の使用方法を慎重に考えた方が賢明かもしれません。