Dashboards, Panels, and Refreshing

posted Nov 14, 2011, 5:51 PM by Eric Patrick   [ updated Apr 24, 2012, 8:51 AM ]
Dashboards usual comprise several panels which needs to pay attention to each other. For example, a Debt dashboard may contain:
  • A summary of Debts by Status (the 'Status' panel)
  • A summary of Debts by User Assigned (the 'User' panel)
  • A list of Debts displayed when a user clicks on a summary line (the 'Search' panel)
    • The ability to reassign Debts from this list
These panels should communicate with each other through event binding. For example:
  • when a user clicks on a 'Delinquent' status in the Status panel, fireEvent('search', ['Search', {Status: 'Delinquent'}]) is called
  • when a user clicks on a 'jdoe@quandis.com' user in the User panel, fireEvent('search', ['Search', {AssignedPersonID: 12}]) is called
  • the 'Search' panel listens for 'Search' events
    • when a user chooses the Reassign menu option in the 'Search' panel and clicks save, fireEvent('Reassign') is called
  • the User panel listens for 'Reassign' events, and refreshes itself
To wire all this binding, the ObjectBind behavior (in qbo.ObjectBind.js) uses the following patterns:

// In Debt.Home.xslt
// Add a 'listen' array to the User panel with one event to listen for: 'Reassign'
<div id="User" data-behavior="ObjectBind" data-objectbind-options="{ 'class': 'qbo3.DebtObject', 'method': 'Dashboard', 'listen': ['reassign'], 'data': {...} }">.</div>

// Add a listen array to the Search panel with one event to listen for: 'Search'
<div id="Search" data-behavior="ObjectBind" data-objectbind-options="{ 'class': 'qbo3.DebtObject', 'listen': ['search'] }">.</div>

// In Debt.Summary.xslt (used by the User panel)
// Note that the event is raised on the qbo3.behavior, which all behaviors are bound to.
<a onclick="qbo3.behavior.fireEvent('search', ['Search', { User: '12', 'Title': 'Debts Assigned to {Dimension}' }]);">...</a>

The heavy lifting for these methods happens in qbo.ObjectBind.js and qbo3.js:

// qbo.ObjectBind.js
// Objects can listed for events that cause invokcation or refresh.
// E.g.: on Debt Home, a Debt search panel listens for a 'DebtSearch' event
// E.g.: on Person Home, a Role Summary listens for 'SystemRoleEdit', to refresh itself whenever roles are edited.
if (options.listen) {
options.listen.each(function (l) {
api.addEvent(l, function (method, data) {
if ((method == null) && data)
qboObject.refresh(data, true);
else if (method && data)
qboObject.invokeHtml(method, data);
else if (typeOf(method) == "object")
qboObject.refresh(method, true);

// qbo3.js' qbo3.AbstractObject
// in invoke*'s onSuccess handler
this.fireEvent('success', method);

Filters and Global Filters

In the example above, we have a hyperlink click handler fire an event to cause a search to happen. In some use cases, we may wish to change the results in a dashboard and in a search panel at the same time. For example, we may allow the user to 'filter' results to 'Active' or 'Delinquent'. Any statement that includes {Where.Filters} can use a generic parameter called 'SqlFilters' to limit results.  For example:
  • Debt.ashx/Dashboard?Dimension=ClientID,DebtType&SqlFilters=Active, or
  • Debt.ashx/Dashboard?Search=ClientID=12&DebtType=Credit Card&SqlFilters=Delinquent
In this case, we essentially want to append {SqlFilters:'Active'} to both the Dashboard and Search panels simultaneously. 

To achieve this, we can have both panels listen for a global filter event called 'filterDebt'.  When the following javascript is fired, both panels with refresh with the appropriate filter applied:

qbo3.behavior.fireEvent('filterDebt', {SqlFilters: 'Delinquent'});

Note in this example, there is only one parameter passed to fireEvent. The ObjectBind behavior is smart enough to call 'refresh' if you don't specify a method and data.

In fact, this pattern is common enough that ObjectBind automatically wires each object to listen for a 'filter{Object}' event raised anywhere in the qbo3.behavior. Thus, the following markup work the same way:

<div id="Search" data-behavior="ObjectBind" data-objectbind-options="{ 'class': 'qbo3.DebtObject', 'listen': ['search', 'filterDebt'] }">.</div>
<div id="Search" data-behavior="ObjectBind" data-objectbind-options="{ 'class': 'qbo3.DebtObject', 'listen': ['search'] }">.</div>

Caching Data on the Browser

HTML5 supports extended storage capabilities via sessionStorage and localStorage. QBO3 leverages HTML5 storage to speed draw times of containers, and to lighten the load on the server. This functionality is implemented by default for all qbo3.AbstractObject-derived classes.
  • First page draw for a user's session:
    • nothing is in storage
    • invokeHtml is called, and if successful, saves the results to storage
    • each subsequent call to invokeHtml will reset storage with the current data, so the 'last' rendering of the panel will be what's stored
  • Subsequent page draws for a user's session:
    • the most recent data in storage will be display
    • the user can use the UI to render different flavors of the panel
For example:
  • A dashboard page will 'remember' the user's last dimension options chosen
  • A search page will 'remember' the user's last results
  • A message panel will 'remember' the user's last sort order, page number, and such
If the rendering HTML include a <span class="rendertime"></span> tag, the cached date and time will be displayed until the data is refreshed from the server.

Caching can be overridden by setting options.remember to false:

<div data-behavior="ObjectBind" data-objectbind-options="{{ remember: false }}">.</div>

Explicit control of the cacheKey enables you to "share" the value across pages. For example, a personal Smart Worklist dashboard panel could be used on Short Sale, Foreclosure, and Bankruptcy dashboard containers, displaying a user's work queue summary.

<div data-behavior="ObjectBind" data-objectbind-options="{{ cacheKey: 'MyWorklist.Summary' }}">.</div>

Implementation follows:
  • After getting data via an AJAX call: in the success handler for invokeHtml, writeStorage() is called, which saves the method, data, html and javascript to sessionStorage.  See qbo3.js.
  • Upon rendering a panel with ObjectBind: in the behavior setup, readStorage() is called, and if it returns true, no further action is taken.  See qbo.ObjectBind.js.
  • On demand, qbo3.clearStorage(path) can be called to clear items out of storage. This is useful during development, when you want to force clearing of buggy storage items.
    • in Chrome's debugger window, simply enter qbo3.clearStorage('/') to clear all storage; qbo3.clearStorage('/qbo3/Debt') would clear storage for all Debt-related pages.
Relevant methods include:

From qbo3:

// Clears storage of any keys beginning with path; if path is null, document.location.pathname is used.
clearStorage: function (path) { ... }

From qbo3.AbstractObject:

// Gets a storage key for use by sessionStorage
cacheKey: function (method) {
return this.options.cacheKey || document.location.pathname + this.options.target.id + (method || this.method);

// Reads content from HTML5 storage, if available
readStorage: function (method) { ... }

// Writes content to HTML5 storage, if available
writeStorage: function (html, script) { ... }

// Clears content from HTML5 storage, if available
clearStorage: function (method) { ... }

From qbo.ObjectBind.js:

// Load data from storage if allowed, otherwise fetch from the server.
if (!qboObject.options.remember || !qboObject.readStorage(method))
qboObject.invokeHtml(method, data);