The support for reflection of the data structures that persist in the database allows for data driven user interfaces to be implemented. This approach has the following charateristics:
In the examples below the UI is rendered using MFC (Microsoft Foundation Classes) on a Windows platform, and using Nanovg for the charts.
Note that the charts update in real time as changes are made on the dialogs, for example as sliders are moved. There is no flicker because the charts are displayed using double buffered graphics with OpenGL.
Since the data persists in local databases and is replicated and synchronised in real time using Operational Transformation, multiple users can view and edit the same data in these dialogs - i.e. they are fully interactive - at the rate that mouse and keyboard events are generated. If the network partitions, users can continue to edit their local copy of the data, and merging occurs automatically when network connections are reestablished.
By default an enumerated type is rendered using a drop down combo. For example:
$enum+ class EColour
{
black,
lightGrey,
white,
indianRed,
green,
blue : ["Sky blue"],
magenta,
cyan,
dark_yellow
};
$model X
{
EColour colour[6];
};
Identifiers tend to be mapped in a useful way to suitable display strings. For example the identifier
lightGrey
is mapped to the display string "Light grey".
Metadata can define the display string (such as "Sky blue" in the above example) to override the
default behaviour.
$model+ TimeFormats
{
assignable<ceda::TDateTime> t1;
assignable<ceda::TDateTime> t2 : [format.shortdatecentury];
assignable<ceda::TDateTime> t3 : [format.longdate];
assignable<ceda::TDateTime> t4 : [format.time];
};
$model+ Times : [elements.groupbox, columns(2), elements.horizalign("stretch"), elements.vertalign("stretch")]
{
TimeFormats h1 : ["normal"];
TimeFormats h2 : ["updown", elements.updown];
TimeFormats h3 : ["shownone", elements.shownone];
TimeFormats h4 : ["updown + shownone", elements.updown, elements.shownone];
TimeFormats h5 : ["align right", elements.align.right];
};
Assignable strings are strings that are updated using assignment operations (rather than insert/delete operations on the characters within the string). By default they are edited using a CEdit control. Metadata can provide various styles:
$model+ Strings
{
assignable<string8> s1 : ["normal"];
assignable<string8> s2 : ["border(false)", border(false)];
assignable<string8> s3 : ["number", number];
assignable<string8> s4 : ["password", password];
assignable<string8> s5 : ["lowercase", lowercase];
assignable<string8> s6 : ["uppercase", uppercase];
assignable<string8> s7 : ["autoHScroll(false)", autoHScroll(false)];
assignable<string8> s8 : ["align.center", align.center];
assignable<string8> s9 : ["align.right", align.right];
assignable<string8> s10 : ["width(200)", width(200)];
assignable<string8> s11 : ["lines(5)", lines(5)];
assignable<string8> s12 : ["lines(5), autoVScroll", lines(5), autoVScroll];
};
By default strings support insert/erase operations on the characters within the string and are edited using the Scintilla edit control
$struct+ TScintilla isa ceda::IPersistable :
model
{
string8 cpp : ["C++", width(500), height(200),
horizalign("stretch"), vertalign("stretch"), language("cpp"),
horizScrollBar(true), vertScrollBar(true),
scrollWidth(1), numMargins(0)];
string8 python : [width(500), height(200),
horizalign("stretch"), vertalign("stretch"), language("python"),
horizScrollBar(true), vertScrollBar(true),
scrollWidth(1), numMargins(0)];
string8 html : [width(500), height(200),
horizalign("stretch"), vertalign("stretch"), language("html"),
horizScrollBar(true), vertScrollBar(true),
scrollWidth(1), numMargins(0)];
}
{
};
Integer fields (of types int8
, int16
, int32
, int64
,
uint8
, uint16
, uint32
, uint64
) are
by default rendered in a CEdit control. The metadata 'slider' can be specified to
instead use a slider control.
$model+ Integers
{
int32 i1 : ["normal"];
int32 i2 : ["border(false)", border(false)];
int32 i3 : ["number", number];
int32 i4 : ["base(16), uppercase", base(16), uppercase];
int32 i5 : ["base(16), showbase(false), fill(\"0\"), widthChars(4), uppercase",
base(16), showbase(false), fill("0"), widthChars(4), uppercase];
int32 i6 : ["min(0), max(100)", min(0), max(100)];
int32 i7 : ["align.center", align.center];
int32 i8 : ["align.right", align.right];
int32 i9 : ["slider, min(0), max(100), ticks(20,40,60,80)",
slider, min(0), max(100), ticks(20,40,60,80)];
int32 i10 : ["slider, min(0), max(100), showTicks(false)",
slider, min(0), max(100), showTicks(false)];
int32 i11 : ["slider, min(0), max(100), direction(\"vert\"), width(50)",
slider, min(0), max(100), direction("vert"), width(50)];
};
Float fields (of types float32
and float64
) are
by default rendered in a CEdit control. The metadata 'slider' can be specified to
instead use a slider control.
$model+ Floats
{
float32 f1 : ["normal"];
float32 f2 : ["fixed", fixed];
float32 f3 : ["scientific", scientific];
float32 f4 : ["fixed, scientific", fixed, scientific];
float32 f5 : ["border(false)", border(false)];
float32 f6 : ["align.center", align.center];
float32 f7 : ["align.right", align.right];
float32 f8 : ["precision(2)", precision(2)];
float32 f9 : ["precision(2), widthChars(10)", precision(2), widthChars(10)];
float32 f10 : ["precision(2), fixed", precision(2), fixed];
float32 f11 : ["precision(2), fixed, widthChars(10)", precision(2), fixed, widthChars(10)];
float32 f12 : ["precision(2), scientific", precision(2), scientific];
float32 f13 : ["precision(2), scientific, uppercase", precision(2), scientific, uppercase];
float32 f14 : ["slider, min(0), max(100), ticks(20,40,60,80)",
slider, min(0), max(100), ticks(20,40,60,80)];
float32 f15 : ["slider, min(0), max(100), showTicks(false)",
slider, min(0), max(100), showTicks(false)];
float32 f16 : ["slider, min(0), max(100), direction(\"vert\"), width(50)",
slider, min(0), max(100), direction("vert"), width(50)];
};
A model that starts with an int32 member can be tagged with the metadata variant
to indicate that the int32 member represents a selector into the remaining members as
though they represent a variant data type.
The variant is rendered with a drop down combo on a group box. The combo selects which variant appears inside the group box.
A model that starts with an bool member can be tagged with the metadata optional
to indicate that the bool member represents a flag for whether the value exists.
The optional value is rendered with a checkbox on a group box. When unchecked all the controls inside the groupbox are disabled.
$model+ MPoint
{
float32 x;
float32 y;
};
$model+ MLine : [optional]
{
bool enabled;
assignable<string8> name;
assignable<ceda::MColourRGBA> colour;
MPoint startPoint;
MPoint endPoint;
};
$model+ MCircle
{
assignable<string8> name;
assignable<ceda::MColourRGBA> colour;
MPoint centre;
float32 radius;
};
$model+ MShape : [variant]
{
int32 selector;
MPoint point;
MLine line;
MCircle circle;
};
$struct+ TVariant isa ceda::IPersistable :
model
{
MShape shapes[4] : [columns(2), labels(false)];
}
{
};
Arrays of models can be rendered in grids. There are no restrictions, for example grids can contain grids.
$model+ TPoint : [columns(2)]
{
float32 x;
float32 y;
};
$model+ TCircle
{
assignable<string8> name;
assignable<MColourRGBA> colour;
string8 description : [lines(2)];
TPoint centre;
float32 radius;
};
$model+ Record
{
TCircle circle;
MImage image;
assignable<TCalendarDate> dateOfBirth;
};
$model+ Records : [labels(false)]
{
Record records[3] : [grid];
};
Tooltips can be specified with tooltip
metadata on fields of models.
For example:
$model+ MCircle
{
MPoint centre : [tooltip("Centre (x,y) position of the circle")];
float32 radius : [tooltip("Radius of the circle in metres")];
};
Tooltips appear automatically when the user pauses the mouse pointer over the associated UI element.
The Dependency Graph System (DGS) is a facility provided in CEDA which allows for dependent variables to be declared. The dependency graph forms automatically and allows for the dependent variables to be marked as dirty and calculated on demand. Unlike the usual observer pattern approach there is no need to attach and detach observers.
The dependent variables are reflected, allowing for the dependents to be automatically displayed
in data-driven user interfaces. In the following example, x
represents an independent variable
which persists in a database and can be edited by the user, whereas y1
and
y2
are read-only calculated variables.
$struct+ DependencyDemo isa ceda::IPersistable :
model
{
int32 x : ["x"];
}
{
$dep^ int32 y1 = 2*x;
$dep^ float64 y2 = y1/3.0 + x;
};
The system is extensible because the viewer for variables of a given type can be registered. This can even be done for pointers to abstract types.
$struct+ TColourWheel isa IPersistable :
model
{
int32 size : [slider, min(50), max(2000)];
float32 hue : [slider, min(0), max(360)];
}
{
$dep^ ptr<INanovgDrawing> colour = GetColourWheelDrawing(this);
};
This example involves a $cache function
named f
.
An invocation of the function f()
is associated with a node in the dependency graph.
$struct+ DependencyDemo isa IPersistable :
model
{
MImage : [maxSize.x(400), maxSize.y(300)] images[4] : [columns(2), labels(false)];
assignable<string8> s;
int32 x : [slider];
int32 year : [slider, min(1900), max(2040)];
int32 month : [slider, min(0), max(11)];
int32 day : [slider, min(1), max(31)];
}
{
$cache-^ <<async>> std::optional<int> f() const
with int v = x;
{
Sleep(250);
return v*10;
}
$dep^ string8 : [width(300), align.center] d = string8("hello") + s;
$dep^ string8 d1 = cxMakeString("x = " << x);
$dep^ string8 d2
calc
{
if (auto v = f())
d2 = cxMakeString("10x = " << *v);
else
d2 = "waiting...";
};
$dep^ string8 d3 = d1.read() + " " + d2.read();
$dep^ TCalendarDate date = TCalendarDate(year,(TMonth)month,day);
$dep^ string8 date_str = cxMakeString(date);
};
In this case there are no function input parameters. In cases where a function has input parameters, invocations with distinct input values represent distinct nodes in the dependency graph.
f
has the <<async>>
directive, marking it as
asynchronous. This means the body of the function is posted as a task to a thread
pool when it needs to be executed.
In this demonstration the body of the function sleeps for 250 milliseconds to simulate a time consuming
task. Nevertheless the user interface is extremely responsive.
The task is run by a worker thread without a transaction on the database. Therefore it cannot access the database (i.e. all the fields in the various $models).
The expression with int v = x
declares a variable v
of type int
which captures the input needed by the task, in a similar manner to variable capture in lambda closures.
The task is allowed to access v
while it is executed by a worker thread.
v
represents a dependent variable in the dependency graph.
So dependency edges from in-nodes are formed according to the variables which
are accessed in the expression used to initialise v
in the
with
expression.
In this case x
is the single in-node to v
.
If an in-node changes, a new asynchronous calculation may be posted to the thread pool.
In this example, if the user continually moves the slider for variable x
,
every 250 msec a task is posted to recalculate the value of f()
.
The invocation f()
also represents an independent node in the depencency
graph which spontaneously changes when the task completes and the function return
value is updated. When the output changes, out-nodes (i.e. downstream dependents) are
invalidated. The upshot is that the user interface automatically updates when asynchronous
calculations finish.
To avoid starvation, async calculations are never aborted, and the new output is always applied when the async calculation is finished, even if the captured input is dirty. An async task must complete before another one is posted to the thread pool.
Internationalisation simply involves use of UTF-8 text files which are read at run-time which map symbol names to display names in a given language.
namespace acme { model MPieChartSegment { label : "Étiquette" colour : "Couleur" labelColour : "Couleur de l'étiquette" labelOffset : "Décalage d'étiquette" frequency : "La fréquence" explode : "Exploser" } }
Note that (for English speaking developers) the English version can be generated automatically from the reflection information. This provides the input that needs to be given to the translators.
$model+ MPieChartSegment
{
bool enable;
assignable<string8> label;
assignable<MColourRGBA> colour;
assignable<MColourRGBA> labelColour;
float32 labelOffset : [slider, min(0), max(2)];
float64 frequency : [slider, min(0), max(1000)];
int32 explode : [slider, min(0), max(100)];
};
$model+ MStroke : [optional]
{
bool enable;
float32 width : [slider, min(0), max(5)];
assignable<MColourRGBA> colour;
};
$model+ MSegmentLabels : [optional]
{
bool enable;
float32 size : [slider, min(10), max(50)];
float32 offset : [slider, min(0), max(2)];
};
$model+ MPiechartGeometry
{
float32 startAngle : [slider, min(0), max(360)];
float32 innerRadiusProportion : [slider, min(0), max(1)];
float32 outerRadiusProportion : [slider, min(0.7), max(1)];
float32 size : [slider, min(50), max(1200)];
};
$model+ MPiechartParams : [labels(false), columns(2), elements.horizalign("stretch"), elements.vertalign("stretch")]
{
MPlotTitle title;
MPiechartGeometry geometry : [groupbox];
MStroke stroke;
MSegmentLabels labels;
};
$struct+ TPieChart isa IPersistable :
model
{
MPiechartParams params;
MPieChartSegment segments[7] : [grid];
} : [labels(false)]
{
$dep^ ptr<INanovgDrawing> : [stealLabelBox, horizalign("stretch")] piechart =
GetPieChartDrawing(this);
};
$model+ MScatterPlot
{
bool enable;
assignable<MColourRGBA> colour;
int32 numPoints : [slider, min(0), max(10000)];
float64 meanX : [slider, min(0), max(100)];
float64 meanY : [slider, min(0), max(100)];
float64 sigma : [slider, min(0.5), max(20)];
};
$model+ MLineSeries
{
bool enable;
bool showPoints;
assignable<MColourRGBA> colour;
int32 numPoints : [slider, min(0), max(200)];
float64 meanY : [slider, min(0), max(100)];
float64 sigma : [slider, min(0.1), max(5)];
};
$struct+ TPlot isa IPersistable :
model
{
MPlot2d plot2d : ["Axes", stealLabelBox, groupbox];
MScatterPlot scatterPlots[4] : [grid];
MLineSeries lineSeries[4] : [grid];
}
{
$dep^ ptr<INanovgDrawing> : [stealLabelBox, horizalign("stretch")] plot =
GetPlotDrawing(this);
};
$struct+ THistogram isa IPersistable :
model
{
MPlot2d plot2d : ["Axes", stealLabelBox, groupbox];
assignable<MColourRGBA> fillColour;
assignable<MColourRGBA> strokeColour;
float32 strokeWidth : [slider, min(0), max(5)];
}
{
$dep^ ptr<INanovgDrawing> : [stealLabelBox, horizalign("stretch")] histogram =
GetHistogramDrawing(this);
};
$model+ MNanoVGFont
{
assignable<xstring> face : [width(70)];
float32 size : [slider, min(10), max(100)];
assignable<MColourRGBA> colour;
};
$model+ MPlotTitle : [optional]
{
bool enabled;
assignable<xstring> text : [width(200)];
MNanoVGFont font : [groupbox];
float32 margin;
};
$model+ MAxisName : [optional]
{
bool enabled;
assignable<xstring> text : [width(150)];
MNanoVGFont font : [groupbox];
float32 position;
float32 margin;
};
$model+ MGridLines
{
bool enabled;
int32 steps : [width(50)];
assignable<MColourRGBA> colour;
};
$model+ MTicks
{
bool enabled;
int32 steps : [width(50)];
float32 length : [width(50)];
assignable<MColourRGBA> colour;
};
$model+ MTickLabels : [optional]
{
bool enabled;
int32 steps : [width(50)];
int32 precision : [width(50)]; // -1 means auto-calculate
MNanoVGFont font : [groupbox];
float32 margin : [width(50)]; // Margin between axis and labels in pixels
};
$model+ MRange
{
float64 min : [width(50)];
float64 max : [width(50)];
};
$model+ MRealAxis : [labels(false), columns(2), elements.horizalign("stretch"), elements.vertalign("stretch")]
{
MAxisName name;
MTickLabels tickLabels;
MGridLines gridLines[2] : [grid, labels("Minor", "Major"), groupbox];
MTicks ticks[2] : [grid, labels("Minor", "Major"), groupbox];
MRange range : [groupbox];
};
$model+ MCatLabels : [optional]
{
bool enabled;
float32 rotation; // Rotation applied to all the tick labels in degrees
float32 margin; // Margin between axis and labels in pixels
MNanoVGFont font : [groupbox];
};
$model+ MCatAxis : [columns(2)]
{
MAxisName name : [stealLabelBox];
MCatLabels labels : [stealLabelBox];
float32 beginMargin; // Margin before the first bar
float32 endMargin; // Margin after the last bar
float32 barWidth; // Width of each bar
float32 groupGap; // Spacing between adjacent bars of distinct groups
float32 categoryGap; // Spacing between adjacent bars of distinct categories
};
$model+ MCategory
{
bool enable;
assignable<xstring> month;
float64 perthSales;
float64 sydneySales;
float64 adelaideSales;
float64 melbourneSales;
};
$model+ MGroup
{
bool enable;
assignable<xstring> name;
assignable<MColourRGBA> fillColour;
assignable<MColourRGBA> strokeColour;
float32 strokeWidth : [slider, min(0), max(5)];
};
$model+ MGeneral
{
MPlotTitle title : [stealLabelBox];
int32 size : [slider, min(100), max(1200)];
bool horizontal;
bool stacked;
};
$model+ MBarChartTabs : [tabs, itemSize.x(80)]
{
MGeneral general;
MCatAxis categoryAxis;
MRealAxis valueAxis;
MCategory categorys[12] : [grid];
MGroup groups[4] : [grid];
};
$struct+ TBarChart isa IPersistable :
model
{
MBarChartTabs tabs : [stealLabelBox];
}
{
$dep^ ptr<INanovgDrawing> : [stealLabelBox, horizalign("stretch")] plot =
GetBarChartDrawing(this);
};
$model+ MPlotGeneral
{
MPlotTitle title : [stealLabelBox];
int32 size : [slider, min(100), max(1200)];
};
$model+ MPolynomial
{
bool enable;
assignable<MColourRGBA> colour;
float32 strokeWidth : [width(40)];
float64 a5 : [slider, min(-0.001), max(0.001), width(60)];
float64 a4 : [slider, min(-0.01), max(0.01), width(60)];
float64 a3 : [slider, min(-0.1), max(0.1), width(60)];
float64 a2 : [slider, min(-1), max(1), width(60)];
float64 a1 : [slider, min(-10), max(10), width(60)];
float64 a0 : [slider, min(-100), max(100), width(60)];
};
$model+ MPlotTabs : [tabs, itemSize.x(80)]
{
MPlotGeneral general;
MRealAxis xAxis;
MRealAxis yAxis;
MPolynomial polynomials[10] : [grid];
};
$struct+ TPlot isa IPersistable :
model
{
MPlotTabs tabs;
} : [labels(false)]
{
$dep^ ptr<INanovgDrawing> : [horizalign("stretch")] plot =
GetPlotDrawing(this);
};
The datatype cref<MFolder>
by default is rendered with a tree control on the left pane
and a view of the currently selected node on the right pane.
$model+ TwoPanedNav : [gridHorizAlign("stretch"), gridVertAlign("stretch"), labels(false)]
{
cref<ceda::MFolder> rootFolder : [horizalign("stretch"), vertalign("stretch")];
};
A very recent approach being explored by web developers is the idea of reactive relational databases. For example see Building data-centric apps with a reactive relational database.
The authors describe the complexity of traditional ways of developing applications:
We've found that state management tends to be a colossal pain. In a traditional desktop app, state is usually split between app’s main memory and external stores like filesystems and embedded databases, which are cumbersome to coordinate. In a web app, the situation is even worse: the app developer has to thread the state through from the backend database to the frontend and back. A “simple” web app might use a relational database queried via SQL, an ORM on a backend server, a REST API used via HTTP requests, and objects in a rich client-side application, further manipulated in Javascript.
The need to work across all these layers results in tremendous complexity. Adding a new feature to an app often requires writing code in many languages at many layers. Understanding the behavior of an entire system requires tracking down code and data dependencies across process and network boundaries. To optimize performance, developers must carefully design caching and indexing strategies at every level.
Then they describe a solution:
How might we simplify this stack?
We think one promising pattern is a local-first architecture where all data is stored locally on the client, available to be freely edited at any time, and synchronized across clients whenever a network is available. In addition to benefits for developers, a local-first architecture also helps end-users by giving them more ownership and control over their own data, and allowing apps to remain usable when the network is spotty or nonexistent.This architecture allows rich, low-latency access to application state, which could unlock totally new patterns for managing state. If an app developer could rely on a sufficiently powerful local state management layer, then their UI code could just read and write local data, without worrying about synchronizing data, sending API requests, caching, or other chores of app development.
This raises the question: what might such a powerful state management layer look like? It turns out that researchers and engineers have worked for decades on systems that specialize in managing state: databases! The word “database” may conjure an image of a specific kind of system, but in this essay we use the word expansively to refer to any system that specializes in managing state. A traditional relational database contains many parts: a storage engine, a query optimizer, a query execution engine, a data model, an access control manager, a concurrency control system, all of which provide different kinds of value. In our view, a system doesn't even need to offer long-term persistence to be called a database.We think that many of the technical challenges in client-side application development can be solved by ideas originating in the databases community. As a simple example, frontend programmers commonly build data structures tailored to looking up by a particular attribute; databases solve precisely the same problem with indexes, which offer more powerful and automated solutions. Beyond this simple example, we see great promise in applying more recent research on better relational languages for expressing computations, and incremental view maintenance for efficiently keeping queries updated.
This idea is implicit in the CEDA framework - CEDA promotes data driven UIs on data recorded in databases.
It is proposed that we tend to limit the amount of meta-data directly recorded on the data structures, and instead apply most of the meta-data using separate text files which are loaded at run-time when the application runs. This provides the following benefits:
It is proposed that we tend to use HTML syntax to allow static content to be defined in the UI. For example for headings and paragraphs. This is particularly advantageous when defining a web-based renderer because the HTML can be passed straight through.
The following example shows how metadata can be applied using the text file:
model MPoint
{
x : ["x coord", slider, min(-100), max(100)];
y : ["y coord", slider, min(-100), max(100)];
}
The following example says that a variable of type MPoint is rendered using a paragraph:
model MPoint
{
<p>The point has x = {x} and y = {y}</p>
}