データの管理と表示

はじめに

要約:
  • RecordSet は、データをレコードとフィールドに編成します。
  • ConnectedRecordSet を使用すると、既存のデータベースからデータを取得できます。
  • RecordGrid では、RecordSet のデータを表形式で表示できます。
  • RecordForm では、フォームベースのデータを表示できます。
Curl® API パッケージ CURL.DATA-ACCESS.BASE に含まれているクラスを使用して、レコードとフィールドに編成されたデータを管理するアプリケーションを作成できます。エンド ユーザーは、フィルタやソートなど、データベースで行う操作の多くを直接クライアント マシンで実行できます。サーバー側のデータベース機能を呼び出す必要はありません。このパッケージにより、構造化されたデータを標準の CSV (カンマ区切りの値) 形式でクライアントに格納することもできます。
2 つ目のデータ アクセス パッケージ CURL.DATA-ACCESS.CONNECTED には、データベースに格納されたデータ、または単純なデータベースを構成するクライアント マシン上のファイルに格納されたデータにアクセスするためのクラスが含まれています。
Curl RTE には、データ表示用の GUI ツールキット オブジェクトも用意されています。RecordGrid はレコードベースのデータを行と列の形式で表示し、データ レコードのソートとフィルタ機能、および使用可能なデータ フィールドのサブセットの表示機能も備えています。RecordForm は、レコード フィールドの内容をフォームのコントロールにバインドして、データ レコードをフォームベースの形式で表示できるようにします。「データ レコードとグリッド」および「フォームとバインド データ」を参照してください。

データの管理

データ管理の主要クラスは RecordSet で、データをレコードとフィールドに編成し、データの変更に関するイベントを管理します。その他の重要なクラスには、RecordViewRecordRecordFieldRecordFilterRecordSort などがあります。
RecordSet および関連クラスによって作成されるオブジェクトはすべてメモリ内で存続します。 実行されている実際のアプリケーションのほとんどは、サーバー上のデータベースか他のリモート データ ソースから取得するデータに関連しています。ConnectedRecordSet クラスは、 外部のソースからデータを取得し更新できる RecordSet を作成します。 ConnectedRecordSet BasicConnection オブジェクトを使ってサーバーのデータベース管理システムのデータにアクセスします。 もしくは、FileConnection オブジェクトを使って、データベースとなるファイル セットのデータにアクセスします。 「データベースでのレコード セットの使用 」 または 「レコードセットとデータ ファイルの使用」 を参照してください。
RecordSet クラスは、データを Record オブジェクトのコレクションとして編成します。Record オブジェクトを直接作成しないでください。これらのオブジェクトは、RecordSet に対する操作の結果として返されます。各 Record のデータ フィールドを記述するには、RecordSet.default ファクトリの RecordFields クラスを使用します。RecordFields オブジェクトは、1 つ以上の RecordField オブジェクトで構成されます。
RecordField オブジェクトは、レコード セットのフィールドを定義する場合に使用します。RecordField により、多くのフィールド属性を指定できます。
RecordSet.select メソッドおよび RecordSet.select-one メソッドを使用すると、レコード セットからレコードを取得できます。RecordSet.select は、指定の RecordFilter で定義されている条件を満たすレコードの配列を返します。フィルタが null の場合、RecordSet.select はすべてのレコードを返します。RecordSet.select-one は 1 つのレコードを返します。
主なデータ アクセス クラスとそれらの間の関係を次に示します。

コンストラクタ内のデータ追加

RecordSet を作成する際にデータを組み込むことができます。RecordData クラスでフィールド名とデータ値の関連付けを行います。次の例では RecordSet を作成し、RecordData を使用してコンストラクタに 3 つのデータ レコードを含めています。
また、RecordGrid を使用して RecordSet を表示しています。「データ レコードとグリッド」を参照してください。RecordGrid は列見出しとして RecordField.name を使用します。RecordField.caption が指定されている場合は RecordField.caption を使用します。

例: RecordSet コンストラクタにデータを含める
{define-proc {show-recordset rs:RecordSet}:Table
    let t:Table = {Table columns=rs.fields.size}
    {for f in rs.fields do
        {t.add {header-cell {value f.name}}}
    }
    {for r in rs do
        {for f in rs.fields do
            {t.add r[f.name]}
        }
    }
    {return t}
}

{let people:RecordSet =
    {RecordSet
        {RecordFields
            {RecordField 
                "First", caption = "First Name", domain = String
            },
            {RecordField 
                "Last", caption = "Last Name", domain = String
            },
            {RecordField 
                "Age", domain = int
            }
        },
        {RecordData First = "John", Last = "Smith", Age = 25},
        {RecordData First = "Jane", Last = "Smith", Age = 29},
        {RecordData First = "Jane", Last = "Jones", Age = 28}
    }
}

{show-recordset people}

RecordSet にデータを追加

次の例では RecordSet.append メソッドを使ってレコード セットにデータを追加します。append には、データ値を含まない RecordData がパラメータとして渡されています。例にある [append record] ボタンをクリックすると、RecordGrid ディスプレイに空のレコードが表示されます。各フィールドに適切な文字列を入力して、追加レコードにデータ値を挿入してください。次のフィールドに移動するには Tab キーを使います。各フィールドに指定された Domain は、フィールドのデータの検証も行います。この例では、レコード セットを表示する RecordGrid は無効な入力を無視します。データ検証における Domain の役割の詳細については、「データの検証」を参照してください。
この例では、RecordSet の作成時に各 RecordField で既定のデータ値を指定しています。RecordSet.append への RecordData 引数にデータを追加して、新規レコードのデータを指定することもできます。append の呼び出しで渡されるデータは、既定のデータより優先されます。例の次のコードを
{people.append {RecordData}}
次のように変更してみてください。
{people.append {RecordData First = "F", Last = "L", Age = 0}}
次に、例の ボックスの下にある [実行] ボタンをクリックし、ポップアップの [append record] をクリックします。
[commit record] ボタンは RecordSet.commit を呼び出し、レコードを RecordSet にコミットします。[revert record] ボタンは変更内容を破棄します。コミットと復帰の詳細は、「レコード データの変更」を参照してください。

例: ユーザーによるデータ追加
{let people:RecordSet =
    {RecordSet
        {RecordFields
            {RecordField 
                "First", domain = String, default-value = "FIRST"
            },
            {RecordField 
                "Last", domain = String, default-value = "LAST"
            },
            {RecordField 
                "Age", domain = int, default-value = 99
            }
        },
        {RecordData First = "John", Last = "Smith", Age = 25},
        {RecordData First = "Jane", Last = "Smith", Age = 29},
        {RecordData First = "Jane", Last = "Jones", Age = 28}
    }
}
{let rg:RecordGrid = 
    {RecordGrid
        height = 3cm, width = 10cm, record-source = people
    }
}
{value
    {VBox
        rg,
        {HBox
            {CommandButton 
                width = {make-elastic}, label = "append record",
                {on Action do
                    {people.append {RecordData}}
                }
            },
            {CommandButton 
                width = {make-elastic}, label = "commit records",
                {on Action do {people.commit}}
            },
            {CommandButton 
                width = {make-elastic}, label = "revert records",
                {on Action do {people.revert}}
            }
        }
    }        
}

RecordView の作成

RecordSet 内の Record データをソートおよびフィルタして、RecordView を作成できます。RecordSort および RecordFilter オブジェクトは、ビューの作成に使用されるソートおよびフィルタ操作を定義するものです。
RecordGrid を使用してグリッド ディスプレイ内に直接 RecordView を作成することができます。「データ レコードとグリッド」を参照してください。

レコードのソート

レコードをソートしたビューを作成するには、RecordView.sort プロパティを RecordSort に設定します。これはレコードを比較するプロシージャ、またはソートするフィールドをリストした文字列を指定することにより、暗黙的に作成することができます。プロシージャには、RecordSort.compareと同じシグネチャと構造が必要です。RecordSortdefault コンストラクタを参照してください。文字列は指定された複数のフィールド名の複合ソートを示します。RecordSortfrom-string コンストラクタを参照してください。
次の例では、コマンド ボタンを使ってレコードを州、市、姓、名によってソートしています。各コマンド ボタンはレコード ビューの sort プロパティを、ソートを指定する文字列に設定しています。フィールド名に続く DESC は降順ソートを表します。既定のソートは昇順で、これは キーワード ASC で明示的に指定できます。
コード サンプルを簡潔にするために、この例、そしてこのセクションのほかの例でも、サポート ファイルのコードを評価して RecordSet を作成しています。

例: Sorting Records
{let staff:RecordSet = 
    {evaluate 
        {url "../../default/support/data.scurl"}
    }
}
{let rv:RecordView = 
    {RecordView staff}
}
{let rg:RecordGrid = 
    {RecordGrid
        record-source = rv, height = 10cm, width = 14cm
    }
}
{value 
    {VBox
        rg,
        {HBox
            {CommandButton 
                width = {make-elastic},
                tooltip = {Tooltip "Sort by State, City, Last, First"},
                label = "Sort Ascending",
                {on Action do
                    set rv.sort = "State, City, Last, First"
                }
            },
            {CommandButton 
                width = {make-elastic},
                tooltip = {Tooltip "Sort by State, City, Last, First"},
                label = "Sort Descending",
                {on Action do
                    set rv.sort = "State DESC, City DESC, Last DESC, First DESC"
                }
            }
        }
    }        
}

レコードのフィルタリング

レコードをフィルタしたビューの作成方法はレコードをソートしたビューの作成に似ています。RecordView.filter プロパティを RecordFilter に設定します。これはレコード、RecordData、または RecordState を選択する機能を指定することにより、暗黙的に作成できます。プロシージャには、RecordFilter.includes? と同じシグネチャと構造が必要です。RecordFilterdefault コンストラクタを参照してください。RecordData はフィルタに含まれるフィールド名と値の組を指定します。RecordFilterfrom-RecordData コンストラクタを参照してください。RecordState はフィルタに含まれるレコードの状態を指定します。RecordFilterfrom-state コンストラクタを参照してください。また、レコードの状態についての詳細は、「レコード データの変更」を参照してください。
次の例では、コマンド ボタンを使用してフィルタを RecordView に適用します。フィルタは、Last フィールドの値を指定する RecordData から作成されます。

例: レコードのフィルタリング
{let staff:RecordSet =
    {evaluate 
        {url "../../default/support/data.scurl"}
    }
 }
{let rv:RecordView = 
    {RecordView staff}
}
{let rg:RecordGrid = 
    {RecordGrid
        record-source = rv, height = 5cm, width = 14cm
    }
}
{VBox
    rg,
    {HBox
        {CommandButton 
            width = {make-elastic}, label = "Show Abrams",
            {on Action do
                set rv.filter = {RecordData Last = "Abrams"}
            }
        },
        {CommandButton 
            width = {make-elastic}, label = "Show Frankel",
            {on Action do
                set rv.filter = {RecordData Last = "Frankel"}
            }
        }
    }
}

レコード データの変更

Curl データ アクセス パッケージでは、データ変更および変更のコミットまたは復帰が可能です。RecordRecordView、および RecordSet の各クラスは、commit メソッドおよび revert メソッドを実装しています。Record で実装され、いずれのメソッドも 1 つの関連レコードを操作します。RecordSetRecordView で実装され、元の RecordSet で変更されたすべてのレコードを操作します。RecordView.commitRecordView.revert の場合、ビューに含まれているレコードだけではなく、レコード セット内の全レコードが操作の対象になります。
delete メソッドは、Record クラスだけで実装されています。一度に複数のレコードを削除する場合は、RecordGrid.delete-selection を使用します。「データ レコードとグリッド」を参照してください。RecordSet.delete-all メソッドはレコードセット内のすべてのレコードを削除します。
RecordGrid を右クリックして表示されるポップアップ メニューから、レコードのコミットや復帰を実行できます。「データ レコードとグリッド」を参照してください。
RecordSet 内の各 Record には、RecordState が関連付けられ、レコードで行われた操作が反映されます。有効な状態は次のとおりです。
次の例では people という RecordSet を作成し、コンストラクタには 3 件のレコードがあります。これらのレコードの状態は RecordState.original です。レコードがもう 1 件追加されていますがコミットは行われていません。このレコードの状態は RecordState.appended です。
RecordState 列はレコードの状態を表示するために特化されたレコード グリッド セル StateDisplayCell を使用します。カスタム レコード グリッド セルについての詳細は、「カスタム セルの作成」を参照してください。
例にあるコマンド ボタンを使用して、現在のレコードを削除、コミットまたは復帰できます。RecordState フィールドには、各レコードの状態が表示されます。
この例でレコードの変更、コミット、復帰を実行し、状態の変化を確認してみてください。appended 状態のレコードを復帰または削除すると、ビューからレコードが消えます。これは、操作により状態が appended から new に変わるためです。appended 状態のレコードのデータを変更しても、その状態は appended のままです。

例: レコード状態の変化
{let people:RecordSet =
    {RecordSet
        {RecordFields
            {RecordField "ID", domain = int},
            {RecordField "Name", domain = String},
            {RecordField "Age", domain = int}
        },
        {RecordData ID = 1, Name = "John Smith", Age = 25},
        {RecordData ID = 2, Name = "Jane Smith", Age = 29},
        {RecordData ID = 3, Name = "Jane Jones", Age = 28}
    }
}
{define-class public StateDisplayCell {inherits StandardStringCell}
  
  {method public open {get-formatted-data}:(String, bool)
    {if-non-null rec = self.record then
        {return rec.state.name, true}
     else
        {return "", false}
    }
  }
}
{people.append
    {RecordData ID = 4, Name = "John Jones", Age = 26}

} 
{let rg:RecordGrid = 
    {RecordGrid 
        record-source = people, 
        sort = "ID", 
        width = 9cm,
        height = 4cm,
        {RecordGridColumn width = 1cm, "ID"}, 
        {RecordGridColumn width = 1cm, "Age"},
        {RecordGridColumn width = 3cm, "Name"},
        {RecordGridColumn width = 3cm, "RecordState", 
            cell-spec = StateDisplayCell}
    }
}
{value
    set rg.records.include-deleted-records? = true
    {VBox
        rg, 
        {HBox width = 9cm,
            {CommandButton 
                width = {make-elastic}, label = "Delete Current",
                {on Action do
                    {if-non-null rec = rg.current-record then
                        {rec.delete}
                    }
                }
            },
            {CommandButton 
                width = {make-elastic}, label = "Commit Current",
                {on Action do
                    {if-non-null rec = rg.current-record then
                        {rec.commit}
                    }
                }
            },
            {CommandButton 
                width = {make-elastic}, label = "Revert Current",
                {on Action do
                    {if-non-null rec = rg.current-record then
                        {rec.revert}
                    }
                }
            }
        }
    }
} 

レコード状態を基準にしてレコードをフィルタできます。次の例では、コマンド ボタンを使用してレコード状態のフィルタを適用します。表示されるレコード ビューには、original 状態のレコードと appended 状態のレコードが両方含まれています。modified 状態のレコードを作成するには、1 つ以上のレコードを編集する必要があります。アプリケーションでもこのようなフィルタを使い、エンド ユーザーが変更をコミットする前にすべての変更データを確認できるようにすることができます。

例: レコード状態のフィルタリング
{let staff:RecordSet =
    {RecordSet
        {RecordFields
            {RecordField 
                "id", caption = "User ID", domain = int,
                index-type = RecordFieldIndexType.unique
            },
            {RecordField 
                "First", domain = String
            },
            {RecordField 
                "Last", domain = String
            }
        },
        {RecordData id = 1, First = "Gene", Last = "Smith"},
        {RecordData id = 2, First = "Fred", Last = "Smith"},
        {RecordData id = 3, First = "Mike", Last = "Smith"},
        {RecordData id = 4, First = "Ben", Last = "Smith"}
    }
}
{staff.append 
    {RecordData id = 5, First = "Ben", Last = "Abrams"}}
{staff.append 
    {RecordData id = 6, First = "Sam", Last = "Jones"}}
{staff.append 
    {RecordData id = 7, First = "Nigel", Last = "Stevens"}}
{staff.append 
    {RecordData id = 8, First = "Bert", Last = "Stevens"}}
{let rv:RecordView = {RecordView staff}}
{let rg:RecordGrid = 
    {RecordGrid record-source = rv, width = 9cm, height = 5cm}
}
{VBox
    rg,
    {HBox
        {CommandButton 
            width = {make-elastic}, label = "Original State",
            {on Action do
                set rv.filter = RecordState.original
            }
        },
        {CommandButton 
            width = {make-elastic}, label = "Appended State",
            {on Action do
                set rv.filter = RecordState.appended
            }
        },
        {CommandButton 
            width = {make-elastic}, label = "Modified State",
            {on Action do
                set rv.filter = RecordState.modified
            }
        }
    }
}

データの検証

Domain は、RecordField の有効なデータ値セットを記述するクラスの基本クラスです。値の検証、比較、および文字列との変換を行うプロシージャが含まれています。すべての基本 Curl データ型に標準ドメインが用意されています。
次の例では、Age フィールドで使用できる整数の最大値、最小値、および既定値を設定します。無効な値を入力した場合にスローされる例外をキャッチし、例外で返されたメッセージを表示します。Domain で設定された既定値は、空白レコードを追加するときの Age フィールドの値になります。

例: Domain を使用したデータの検証
{let limit-age:StandardIntDomain = 
    {StandardIntDomain
        default-value = 25,
        max-allowable = 30,
        min-allowable = 20
    }
}
{let people:RecordSet =
    {RecordSet
        {RecordFields
            {RecordField 
                "First", caption = "First Name", domain = String
            },
            {RecordField 
                "Last", caption = "Last Name", domain = String
            },
            {RecordField 
                "Age", domain = limit-age
            }
        },
        {RecordData First = "John", Last = "Smith", Age = 25},
        {RecordData First = "Jane", Last = "Smith", Age = 29},
        {RecordData First = "Jane", Last = "Jones", Age = 28}
    }
}
{let rg:RecordGrid = 
    {RecordGrid
        height = 3cm,
        record-source = people
    }
}
{VBox
    rg,
    {HBox
        {CommandButton 
            width = {make-elastic}, label = "append invalid record",
            {on Action do
                {try
                    {people.append 
                        {RecordData First = "George", Last = "Jones", Age = 55}
                    }
                 catch e:ValidationException do
                    {popup-message e.message}
                }
            }
        },
        {CommandButton 
            width = {make-elastic}, label = "append blank record",
            {on Action do
                {try
                    {people.append 
                        {RecordData}
                    }
                 catch e:ValidationException do
                    {popup-message e.message}
                }
            }
        }
    }
}

データの選択

RecordSet からレコードを選択するには、RecordSet.select メソッドを使用します。RecordFilter を指定して、選択するレコードを定義する必要があります。
次の例では、海上信号旗の画像と、それらに関連付けられている文字および単語を格納する RecordSet を作成します。次に、関連付けられている文字を使用して RecordSet から "CURL" とつづる旗のレコードを選択し、画像と単語を表示します。
この例では RecordGrid を使用していません。

例: RecordSet からレコードを選択
{let maritime-signal-flags:RecordSet =
    {evaluate {url "../../default/support/flag-data.scurl"}}
}
{define-proc public {find-letter-flag l:char}:(word:String, flag:Frame)
    let selected:{Array-of Record} = 
        {maritime-signal-flags.select filter = 
            {RecordData letter = l}
        }
    let r:Record = selected[0]
    let word:String = r["phonetic"]
    let flag:Frame = 
        {Frame
            height = 42px,
            width = 53px,
            background = {Background.from-url {url r["flag"]}}
        }
    {return word, flag}
}
{let (word-c:String, letter-c:Frame) = {find-letter-flag 'C'}}
{let (word-u:String, letter-u:Frame) = {find-letter-flag 'U'}}
{let (word-r:String, letter-r:Frame) = {find-letter-flag 'R'}}
{let (word-l:String, letter-l:Frame) = {find-letter-flag 'L'}}
{HBox
    font-size = 12pt,
    spacing = 5px,
    {VBox halign = "center", 'C', word-c, letter-c},
    {VBox halign = "center", 'U', word-u, letter-u},
    {VBox halign = "center", 'R', word-r, letter-r},
    {VBox halign = "center", 'L', word-l, letter-l}
}

RecordSet のイベント

RecordSet は、それ自体でイベントを発生させてデータの内容または構成の変更を通知します。
重要なイベント クラスを次に説明します。
次の例では、plan という RecordSet に、予定されている仕事のデータが格納されています。仕事には 3 つのフェーズがあり、それぞれのフェーズは期間の異なるいくつかのステップで構成されています。SummaryTable_source レコード セットと _summarized を保持するクラスで、ソース内の情報を要約して各フェーズの合計時間を算出します。この例では、ソース レコード セットは plan です。plan の基本データが変更された場合は、_summarized レコード セットの内容を再計算する必要があります。plan のイベント ハンドラは、RecordModified イベントを受け取ると再計算を実行します。

例: RecordModified イベントの使用
{let public plan:RecordSet = 
    {RecordSet
        {RecordFields
            {RecordField "task", 
                domain = String
            },
            {RecordField "phase", 
                domain = String
            },
            {RecordField "weeks", 
                domain = int
            }
        },
        {RecordData task = "step 1", phase = "I", weeks = 2},
        {RecordData task = "step 2", phase = "I", weeks = 1},
        {RecordData task = "step 3", phase = "I", weeks = 3},

        {RecordData task = "step 1", phase = "II", weeks = 2},
        {RecordData task = "step 2", phase = "II", weeks = 4},
        
        {RecordData task = "step 1", phase = "III", weeks = 2},
        {RecordData task = "step 2", phase = "III", weeks = 3},
        {RecordData task = "step 3", phase = "III", weeks = 4},
        {RecordData task = "step 4", phase = "III", weeks = 6}
    }
}
{define-class public SummaryTable
  let constant summary-fields:RecordFields = 
      {RecordFields
        {RecordField "phase", domain = String},
        {RecordField "weeks", domain = int}
      }
  field public _source:RecordSet
  field public _summarized:RecordSet
  field public tasks:StringArray = {StringArray}

  {constructor public {default
                          source:RecordSet
                      }
    set self._source = source
    set self._summarized = 
        {RecordSet SummaryTable.summary-fields}
  }
  {method public {recalc}:void
    {self.accumulate-by-phase "I"}
    {self.accumulate-by-phase "II"}
    {self.accumulate-by-phase "III"}
  }
  {method private {accumulate-by-phase phase:String}:void
    let week-sum:int = 0
    let rv:RecordView = 
        {RecordView 
            self._source, 
            filter = {RecordData phase = phase}
        }
    {for r:Record in rv do
        set week-sum = week-sum + r["weeks"]
    }
    let rec:#Record = 
        {self._summarized.select-one filter = {RecordData phase = phase}}
    {if rec == null then
        {self._summarized.append {RecordData phase = phase, weeks = week-sum}}
     else
        set rec["weeks"] = week-sum
    }
  }
}
{let summary:SummaryTable = {SummaryTable plan}}
{summary.recalc}
{plan.add-event-handler
    {on RecordModified at rs:RecordSet do
        {summary.recalc}
    }
}
{HBox
    {RecordGrid
        background = "white",
        width = 6cm,
        height = 6cm,
        record-source = summary._source,
        {RecordGridColumn "task", width = 2cm},
        {RecordGridColumn "phase", width = 1.5cm},
        {RecordGridColumn "weeks", width = 1.5cm}
    },
    {RecordGrid
        background = "white",
        width = 4cm,
        height = 6cm,
        record-source = summary._summarized,
        {RecordGridColumn "phase", width = 1.5cm},
        {RecordGridColumn "weeks", width = 1.5cm}
    }
}