tsc_DatabaseCustomType and custom records

Available in SC version 2.10 and higher.

An introduction to the subject of custom data can be found on the page Adding custom data to the job database. You may benefit from reading that before diving in here.

This page describes the tsc_DatabaseCustomType class, which is fundamental to all custom database operations.  Sub-pages to this one describe the other classes that are closely related.

tsc_DatabaseCustomType

Every time an item of custom data is added to the database, a tsc_DatabaseCustomType subclass is required to supply methods which describe the type and de/serialize the XML.  The subclass is effectively a database record and will generally contain member variables which hold the custom data variables.

The Custom Type is used by Survey Core to uniquely identify this type of custom record from other custom records written by this or another plugin.  It uses a type name and a GUID to identify the type.  The type is also used to specify some options and a version number.

Because the tsc_DatabaseCustomType class is abstract, it must be subclassed. One subclass is required for each different type of custom record that is required. The abstract methods should be overridden and return the appropriate values, which in most cases will be simple constants. These functions are described below.  Serialization and deserialization methods must also be provided, which serialize the class variables to XML, and deserialize XML back into class variables.

Custom types which are used for writing custom entities must also supply a string containing the XML path to the key value.  The key value is a string used to find the record in the database.  The key path identifies the path of XML element names (and possibly an attribute) that leads to the value.

When custom data is read from the database, the tsc_DatabaseCustomType class is populated by the read function calling the overridden Deserialize method.  The record's original custom type values are supplied to Deserialize to simplify XML parsing of different versions.

The first time a custom record is added to the database, a record of its type is also written to the database.  All subsequent records in a database reference this type; if a type is altered in a later version of the plugin, the change will not be reflected in an existing job database which already has a record of the type.  Note however that the type version number is written with the record itself, so each record will always reference the correct type record.

Changing an existing type in a later release

Types should rarely be changed, and never changed dynamically during execution.

When the requirements change for custom records, for instance adding a element to the custom XML or changing an option in the Type, consideration must be given to how the change will affect existing databases.  When the customer upgrades to a later plugin which writes and supports modified versions of the custom database records, it is possible their database will contain both old and new records. 

Therefore, if anything is changed, even if there is no need for code changes, the version number in the type must be incremented - there is no reason not to do this, since there is no shortage within the 2 billion numbers in a 32-bit integer.

Custom database processing logic must be designed to be backward compatible with all previous versions, even if it is only to issue a warning that old records will be ignored, when they are discovered in a database.

For a Custom Type that is used by custom entities, the path to the key must never be changed.  If that change is necessary then the type name (or GUID) should be changed so that the Type becomes a completely new one.  For instance, the name could be changed from "GeigerCounter" to "GeigerCounter-2".  Old records can then be bypassed.  On the other hand, the plugin could increment the version and simply ignore all old records.

The Version and Reviewable values in the Type may be changed at any time without any adverse effects. The version number must never decrease in value.

Changing the custom XML carries the same rules as changing any XML document; elements should be added rather than changed or removed, and the interpretation of the XML should be flexible enough to deal with both old and new schemas.  The version number in the Type supplied to the Deserialize  method may be used to advise the XML-reading code which schema to expect.

Public overridable methods

virtual const char* Type () const = 0;
Override this method and return a name for the custom type, eg: return "GeigerCounter";  The returned value must be constant.

virtual int TypeVersion () const = 0;
Override this method and supply a version number for this type.  All records written using this type will carry the version number, so if changes are made to the schema of the custom XML, the version may be determined when the record is read.  The version number is not used to uniquely identify the type; if all records of a type are requested, all matching the GUID and Type Name will be returned, regardless of the version.

Example: return 2;

virtual bool Reviewable () const = 0;
Override and return a boolean value which indicates if the record should be visible in the Job Review windows.  If this is false then the user will be unaware of these records unless the plugin exposes them in some other way.  If true is returned then the Job Review window will list all custom records of this type with a [+] to the left.  When the [+] is clicked the custom XML is presented in a simple Name/Value format.  It is also possible to provide a custom display; see the RegisterReviewable method in this class for details.

Example: return true;

virtual const char* Guid () const;
This override is optional. Override the method to supply a unique GUID for the type.  If no override is provided, then the base class supplies the GUID of the plugin's application.  There is usually only one application per plugin; however if the plugin supports more than one tsc_Application then this function must be overridden to make sure the correct GUID is used.

Specifying the GUID is typically used for sharing the type between different plugins.  If the same type name and GUID is supplied by two different plugins then they may also share the same custom records.  The GUID is case-insensitive and may contain only hex digits (0-9, a-f, A-F) and hyphens.

Example: return "4A4E91C5-089F-45ea-94F5-A70B70E7218B";   // NOTE: Please do not use this GUID. Make your own (try the MS Visual Studio Tools menu).

virtual const char* EntityKeyPath () const;
This override identifies what field is used as the key (ie, Name) of a custom entity. The returned value must identify which element or attribute within the custom entity's XML contains the search key. It is necessary only for Custom Entities, and can be omitted if the type is being used to write Custom Records or Custom Sub-records. If a path is supplied but not required, it is simply ignored.

Every XML fragment written as a CustomEntity must contain a valid key at the supplied XML path.  Failure to do so will result in an empty key (which is still valid but not particularly useful).

Return the full path to the XML element or attribute that contains the record's key, relative to the root element of the custom XML. Path element names are separated by '/', and the attribute name by '@'. Because the path is relative to the root element, the root element's own name should not be included in the path. Some examples follow: 

  // For xml like this: <MyXml><Location><Place City="New York" /></Location></MyXml>
    return "Location/Place@City";     

  // For xml like this: <MyXml><Equipment><SerialNumber>GC100321</SerialNumber></Equipment></MyXml>
  return "Equipment/SerialNumber";  

  // For xml like this: <Operator Name="Homer"><Age>37</Age></Operator>
  return "@Name";                   

virtual tsc_XmlElement Serialize () = 0;
This method must be overidden.  It should take class variables which contain the custom data fields for the type, and create an XML tree suitable for writing to the database.  Returning an empty tree is an error.

virtual tsc_XmlElement DeSerialize (tsc_XmlElement& xml, tsc_DatabaseCustomType& originalType) = 0;
Override with a method to convert the supplied XML tree into member variables.  originalType will return the same values as the type that was originally used to write the record.  Serialize is called when the type subclass is passed into a custom record Read function.

void RegisterReviewable (tsc_IReviewable* review);
Registers this instance of the type as the provider of items to populate the Job Review window for instances of this custom type.  To enable the review system to work correctly even while the plugin is not the running application, it is important to supply an instance of tsc_IReviewable which exists for the life of the plugin.  A good place to create the instance is in the application's subclass of tsc_Application. Alternatively, the instance could be defined in the tsc_DatabaseCustomType subclass, created and registered in the constructor, and disposed of by the destructor.

The tsc_DatabaseCustomType subclass must also subclass from tsc_IReviewable, and supply "this" as the argument to the RegisterReviewable method. See tsc_IReviewable, and the example below, for more information.

void DeRegisterReviewable ();
Removes the registration previously added with RegisterReviewable.  This also occurs automatically during destruction of the instance.

Example

In order to keep this example smallish and simple, some niceties such as error checking have been omitted. It should also be mentioned that we don't know the first thing about geiger counters, except that they measure ionizing radiation and probably don't have an offset.

// A subclass defining a custom database type. 

// Note: The GC1031 geiger counter is entirely fictional, and a dosimeter would be way more practical.

class GeigerConfigType : public tsc_DatabaseCustomType, tsc_IReviewable 

public:     

    

    // Return basic facts about this type.     

    virtual const char*  Type () const 

    { 

        return "GeigerConfig";

    }     

    

    virtual int TypeVersion() const 

    { 

        return 1; 

    }

    

    virtual bool Reviewable () const 

    { 

        return true;

    }

    

    virtual const char*  EntityKeyPath () const 

    { 

        return "Model"; 

    }      

    

    // Construct an instance suitable for writing.

    GeigerConfigType (const char* model, double offset) : 

        Model (model),

        Offset (offset)

    {

    }


    // Construct instance suitable for reading into.

    GeigerConfigType () :

        Model (""),

        Offset (0.0)

    {

    }


    // The variables to be serialized to and from the custom record.

    tsc_String Model;

    double     Offset;


    // Converts the class members to XML.

    tsc_XmlElement Serialize () 

    {

        tsc_XmlElement xml ("GeigerConfig");

        xml.AddChild ("Model").SetValue(Model);

        xml.AddChild ("Offset").SetValue(tsc_Format::Scalar (Offset, 2));

        return xml;

    }


    // Populates the instance from an XML fragment.

    void DeSerialize (tsc_XmlElement& xml, tsc_DatabaseCustomType& originalType)

    {

        Model = xml["Model"].StringValue();

        Offset = xml["Offset"].DoubleValue();

    }


    // We have not implemented a review details form.

    virtual bool HasReviewForm ()

    {

        return false;

    }


    virtual tsc_ReviewActions  ShowReviewForm ()

    {

        // With no special form, there is nothing to do.

    }


    // Format a small tree to display our information in the Job review window.

    virtual void GetReviewTree (class tsc_UiTree& tree, bool expanded)

    {

        tsc_UiTreeItem row = tree.AddRow ();

        row.Texts ("Geiger Counter", Model);

        tsc_UiTreeItem addr = row.AddChild ();


        // This causes an expandable top row.

        if (expanded)

        {

            addr.Font (tsc_Font::SubtreeFont);

            addr.Texts ("Model", Model);

            addr = row.AddChild ();

            addr.Texts ("Offset", Offset);

        }

    } 

};


// Writing a custom entity.

tsc_Database db = tsc_CurrentJob::Get().Database();

tsc_JobCustomEntities dbe = db.CustomEntities();

GeigerConfigType gConfig ("GC1031", 4.1);

dbe.Append (gConfig); 

...


// Get a list of all records for model GC1031 in the database.

// Note that any instance of GeigerConfigType (like gc2) may be used in Snapshot()...

GeigerConfigType gc2;


tsc_CustomEntityList ce2 = dbe.Snapshot (gc2, "GC1031");


for (int ci = 0; ci < ce2.Count(); ci++)

{

    ce2[ci].Read (gc2);

    tsc_MessageBox::Show (

              "Geiger counter",

              tsc_String::Format("The model %s's offset is %2.2f", gc2.Model, gc2.Offset));

}

...


// Registering the type for Job Review (perhaps in the tsc_Application constructor).

GeigerConfigType ReviewGeiger;

...

ReviewGeiger.RegisterReviewable (&ReviewGeiger);