Data Management and Display

Introduction

Summary:
  • RecordSet organizes data into records and fields.
  • ConnectedRecordSet lets you get data from an existing database.
  • RecordGrid supports tabular display of data in a RecordSet
  • RecordForm supports form-based data display.
The Curl® API package CURL.DATA-ACCESS.BASE contains a number of classes that enable you to write applications that manage data organized in records and fields. It enables the end user to perform many database-like operations on structured data, such as filtering and sorting, directly on the client machine without needing to invoke database functions on the server. This package also includes support for storing structured data on the client in the standard CSV (comma separated value) format.
A second data access package, CURL.DATA-ACCESS.CONNECTED provides access to data stored in a database or in a collection of files on the client machine that comprise a simple database.
The Curl RTE also provides specialized GUI toolkit objects for data display. RecordGrid displays record-based data in a row and column format, and provides the ability to sort and filter the data records, and view a subset of available data fields. RecordForm provides the ability to view data records in a form-based format by binding the content of fields in a record to controls in a form. See Data Records and Grids and Forms and Bound Data.

Managing Data

The primary class for data management is RecordSet, which organizes data in terms of records and fields, and manages events having to do with changes in the data. Other important classes include RecordView, Record, RecordField, RecordFilter, and RecordSort.
The objects created by RecordSet and related classes exist entirely in memory. Most actual working applications involve data obtained from a database on a server or other remote data source. The class ConnectedRecordSet creates a RecordSet that can obtain and update data from a an external source. ConnectedRecordSet uses a BasicConnection object to access data from a database management system on a server, or a FileConnection object to access data from a set of files acting as a set of databases. See Using Record Sets with a Database and Using Record Sets with Data Files
The RecordSet class organizes data as a collection of Record objects. You do not create the Record objects directly, they are returned to you as a result of operations on a RecordSet. You use the RecordFields class in the RecordSet.default factory to describe the data fields in each Record. A RecordFields object is made up of one or more RecordField objects.
Use the RecordField object to define fields in the record set. RecordField enables you to specify a number of attributes of the field.
The methods RecordSet.select and RecordSet.select-one enable you to retrieve records from a record set. RecordSet.select returns an array of records that meet criteria defined in the supplied RecordFilter. If the filter is null, RecordSet.select returns all records. RecordSet.select-one returns a single record.
The following list summarizes the major data access classes and the relationships between them.

Adding Data in the Constructor

You can incorporate data into a RecordSet when you construct it. The RecordData class associates field names with data values. The following example creates a RecordSet and uses RecordData to include three data records in the constructor.
The example also uses RecordGrid to display the RecordSet. See Data Records and Grids. Note that RecordGrid uses RecordField.name as the column heading in the display, or RecordField.caption, if one is supplied.

Example: Including Data in the RecordSet Constructor
{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}

Appending Data to a RecordSet

The following example uses the method RecordSet.append to add data to the record set. The parameter passed to append is a RecordData that contains no data values. When you click the append record button in the example, an empty record appears in the RecordGrid display. Fill in data values for the appended record by typing an appropriate string in each field. You can use the tab key to move to the next field. The Domain specified for each field also handles validation of data in the field. The RecordGrid that displays the record set in this example simply ignores invalid input. See Validating Data for more information on the role of Domain in data validation.
This example supplies default data values in each RecordField when creating the RecordSet. You can also supply data for the new record by adding it to the RecordData argument to RecordSet.append. Data supplied in the call to append overrides the default data. Try changing this line of code:
{people.append {RecordData}}
to:
{people.append {RecordData First = "F", Last = "L", Age = 0}}
then click the Execute button at the bottom of the example box, and click append record in the example popup.
The commit record button calls RecordSet.commit and commits the record to the RecordSet. The revert record button discards your changes. For more on commit and revert, see Modifying Record Data.

Example: Data from User
{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}}
            }
        }
    }        
}

Creating a RecordView

You can sort and filter data Records in a RecordSet to create a RecordView. The RecordSort and RecordFilter objects define the sort and filter operations used to create the view.
You can use RecordGrid to create RecordView directly in the grid display. See Data Records and Grids.

Sorting Records

To create a view that sorts records, set the RecordView.sort property to a RecordSort, which you can create implicitly by supplying either a procedure that compares records, or a string that lists fields to be sorted. The procedure must have the same signature and semantics as RecordSort.compare; see the default constructor for RecordSort. The string specifies a compound sort on the supplied field names; see the from-string constructor for RecordSort.
The following example uses command buttons to sort records by state, city, last name and first name. Each command button sets the sort property of the record view to a string that specifies the sort. The string DESC following a field name indicates a descending sort. The default sort is ascending, which you can specify explicitly with the string ASC.
In order to keep the code examples compact, this example, and others in this section, create a RecordSet by evaluating code in a support file.

Example: 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"
                }
            }
        }
    }        
}

Filtering Records

Creating a view that filters records is similar to creating a view that sorts them. You set the RecordView.filter property to a RecordFilter, which you can create implicitly by supplying a function that selects records, a RecordData, or a RecordState. The procedure must have the same signature and semantics as RecordFilter.includes?; see the default constructor for RecordFilter. The RecordData specifies field name/value pairs included in the filter; see the from-RecordData constructor for RecordFilter. The RecordState specifies the state of records included in the filter. see the from-state constructor for RecordFilter. Also see Modifying Record Data for more information on record state.
In the following example, command buttons apply filters to a RecordView. The filters are derived from a RecordData that specifies a value for the field Last.

Example: Filtering Records
{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"}
            }
        }
    }
}

Modifying Record Data

The Curl data access package enables you to modify data and commit or revert those modifications. The classes Record, RecordView, and RecordSet all implement both the commit and revert methods. As implemented by Record, both methods act on the single associated record. As implemented by both RecordSet and RecordView, the methods act on all modified records in the underlying RecordSet. Note that RecordView.commit and RecordView.revert act on all records in the record set, not just those contained in the view.
The delete method is implemented only by the Record class. If you want to delete more than one record at a time, use RecordGrid.delete-selection. See Data Records and Grids. The method RecordSet.delete-all deletes all records in the record set.
The right-click pop-up menu provided by RecordGrid enables you to commit and revert records. See Data Records and Grids.
Each Record in a RecordSet has an associated RecordState, which reflects operations done on the record. Possible states are:
The following example creates a RecordSet called people and includes three records in the constructor. These records have the state RecordState.original. It then appends one record but does not commit it. That record has the state RecordState.appended.
The column RecordState uses the specialized record grid cell StateDisplayCell to display the record state. See Creating a Custom Cell for more information on custom record grid cells.
Command buttons supplied in the example let you delete, commit, or revert the current record. The "Record State" field shows the state of each record.
Use the example to experiment with modifying, committing, and reverting records, and note the changes in state. If you revert or delete the appended record, it disappears from view. This is because these operations change the state from appended to new. If you modify the data in the appended record, the state remains appended.

Example: Changes in Record State
{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}
                    }
                }
            }
        }
    }
} 

You can filter records by record state. The following example uses command buttons to apply record state filters. The record view it displays contains both original and appended records. You need to edit one or more records to create modified records. An application might use such filters to enable the end user to examine all the changed records before committing the changes.

Example: Filtering for Record State
{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
            }
        }
    }
}

Validating Data

Domain is a base class for classes that describe a valid set of data values for a RecordField, and provide procedures that validate, compare values, and convert them to and from strings. Standard domains are provided for all basic Curl data types.
The following example sets maximum, minimum, and default values for integers allowed in the Age field. It catches the exception thrown when you try to enter an invalid value, and shows the message returned in the exception. Note that the default value set in the Domain supplies a value for the Age field when you append a blank record.

Example: Using Domain to Validate Data
{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}
                }
            }
        }
    }
}

Selecting Data

You can use the RecordSet.select method to select records from a RecordSet. You need to supply a RecordFilter that defines the records to be selected.
The following example creates a RecordSet containing images of maritime signal flags, and the letters and words associated with them. Then it uses the associated letters to select from the RecordSet the records for flags that spell "CURL" and displays the images and words.
Note that this example does not use RecordGrid in any way.

Example: Selecting Records from a 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 Events

RecordSet fires events at itself to signal changes in data content or organization.
This list describes some of the important event classes:
In the following example, the RecordSet plan contains data about a planned task that involves three phases with several steps of varying duration for each phase. SummaryTable is a class that maintains a _source record set, and _summarized. which summarizes information in the source to give the total time for each phase. In this example, the source record set is plan. When the underlying data in plan changes, the content of the _summarized record set needs to be recalculated. An event handler on plan performs the recalculation when it receives a RecordModified event.

Example: Using the RecordModified Event
{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}
    }
}