Building Maintainable Dynamic Interfaces

Lou D’Andria, Wolfram Research Inc.

Join the Conversation #WolframTechConf

What this talk is about

The Wolfram Language provides a rich vocabulary of interface elements which can used, along with Dynamic, to produce new ways of interacting with the Wolfram System. But how best to put these elements together to build interfaces which are reliable and easy to maintain? In this talk, we’ll illustrate particularly useful techniques for developing Dynamic interfaces, which have been used when building functions like Manipulate, IntervalSlider, Audio, and many others. We’ll look specifically at issues related to versioning, compatibility and serialization.

Prerequisites

Consider this a 300-level course in generated-on-demand interfaces.

If you’re looking for other courses, including 100- and 200-level courses, please see previous talks, available on wolfram.com and elsewhere.

100-400: Wolfram Demonstrations Project: http://demonstrations.wolfram.com

100: Introduction to Dynamic: paclet:tutorial/IntroductionToDynamic

150: Manipulate Secrets Revealed (2014): https://www.youtube.com/watch?v=umE6bBZpVHk

220: Principles of Dynamic interfaces (2015): https://www.youtube.com/watch?v=6uDSRIYzsUw

250: Wormholes & Other Dynamic Curiosities (2013): https://www.youtube.com/watch?v=t-aTDlrq62o

301: Advanced Dynamic Tutorial: paclet:tutorial/AdvancedDynamicFunctionality

A solid grounding in typesetting is also fairly important.

Friday: Jason Harris, Typesetting: http://wtc16.pathable.com/meetings/443336

Recall…

Principles of Dynamic Interfaces

The Principle of Programmatic Generation

The Principle of Minimal Dynamics

The Principle of Maximum Encapsulation

The Principle of Evaluation Blocking

We will be digging into one

Programmatic Generation: Every interface should be generated by a program

Pre-generated

About box
Preferences
Help Viewer toolbar
Cloud Open / Save
Headers and footers
Palettes
lots more…

Generated on demand

Manipulate
WolframAlpha
SystemInformation
Predictive Interface
and more…

Motivating Example

Manipulate

Manipulate is built entirely in top level code.....

Column[{Slider[Dynamic[x]],Dynamic[x]}]

DynamicModule[{x=0},Panel[Column[{Grid[{{"x",Manipulator[Dynamic[x]]}},BaseStyle -> "Manipulator"],Item[Pane[Dynamic[x],ImageMargins -> 8],Frame -> True,FrameStyle -> LightGray,Background -> White]},Spacings -> 1],DefaultBaseStyle -> {}],BaseStyle->"Manipulate"]

OK, but…

If we burn all those boxes into notebook for every Manipulate output, what happens?

notManipulate[x,{x,0,1}]//ToBoxes

Consequences

File size bloat

Graphic design lock in

existing outputs wouldn't take advantage of layout changes, branding changes, …

Implementation lock in

existing outputs wouldn't take advantage of new technologies

new outputs that do take advantage of new technologies would cause all manner of trouble in older front ends

We can do better

Goals

Only store what's necessary to display the object, at render time

Graphic design should be responsive to changes in options, environment, platform, errors, …

Older outputs should work in all future versions of your rendering engine

Newer outputs should not choke older versions of your rendering engine

Examples

Manipulate

Manipulate[x,{x,0,1}]

Manipulate[x,{x,0,1}]//ToBoxes

IntervalSlider

IntervalSlider[Dynamic[int]]

IntervalSlider is built entirely in WL code, leveraging our graphics language for the display and LocatorPane for aspects of the control.

But you wouldn’t know it by looking at the boxes.

IntervalSlider[Dynamic[int]]//ToBoxes//InputForm

Free-form Input

The exact same cell expression which rendered like this in M8:

renders like this in M10 and later:

LinguisticAssistant

How is that possible?

WolframAlpha["int sinx","LinguisticAssistant"]//InputForm

Bonus: And others

This technique is how a two-minute piece of audio has a box representation which is simple and small:

Audio["ExampleData/bach.mp3"]

Audio["ExampleData/bach.mp3"]//ToBoxes//InputForm

DynamicGeoGraphics:

DynamicGeoGraphics[GeoRange->Quantity[1,"Miles"]]

DynamicGeoGraphics[GeoRange->Quantity[1,"Miles"]]//ToBoxes//InputForm

This technique even predates TemplateBox

Binomial[n,m]//TraditionalForm

1/0

Standard Techniques: Overview

Storing state:
Any permanent state is stored in a DynamicModule.

Overall structure:
First argument to DynamicModule is essentially just Dynamic of a function that generates the interface at render time.

Outer structure:
Custom interpretation for the can be handled via TagBox / InterpretationBox

Implementation techniques:
Loading your code.
Passing variables by reference instead of by value.
Function is implemented in its own context to avoid variable naming conflicts.
Responsive to the changes in the user’s settings, environment, etc.
Communicates with the outside world as needed.
Detecting when the kernel quits.
Switching to a fall-through display if the user’s system or kernel is insufficient in some way.
Support for custom error states.

Storing state

Some interfaces require no state at all. A reference to the user’s Dynamic variable is enough.

IntervalSlider[Dynamic[int]]

But it’s very common to need to store state, almost always via DynamicModule.

Manipulate[x,{x,0,1}]

Overall structure

The target boxes are roughly:

DynamicModuleBox[ {local storage} ,
	Dynamic[code to generate interface] ]

Simple example

MyInterface[Dynamic[var_]] := DynamicModule[{a=0},Dynamic[display[a,var]]]

display[a_,var_]:=Panel[{Slider[Dynamic[a]],Slider[Dynamic[var]],Dynamic[{a,var}]}]

MyInterface[Dynamic[x]]

But notice that this doesn’t actually work, because the variables were passed by value.

Passing variables by reference

SetAttributes[display2,HoldAll];
display2[a_,var_]:=Panel[{Slider[Dynamic[a]],Slider[Dynamic[var]],Dynamic[{a,var}]}]

DynamicModule[{a},Dynamic[display2[a,var]]]

Another idiom

Instead of using holding attributes manually, we can use the same approach used by Slider, Checkbox, and all other control syntax: Rely on the holding attributes of Dynamic.

display3[Dynamic[a_],Dynamic[var_]]:=Panel[{Slider[Dynamic[a]],Slider[Dynamic[var]],Dynamic[{a,var}]}]

DynamicModule[{a},Dynamic[display3[Dynamic[a],Dynamic[var]]]]

Utility versioning

With this model, interfaces can be updated even after they’re created.

Starting point

SetAttributes[utility,HoldAll];
utility[1,a_,var_]:=Panel[{Slider[Dynamic[a]],Slider[Dynamic[var]],Dynamic[{a,var}]}]

DynamicModule[{a=0},Dynamic[utility[1,a,var]]]

Forward compatibility

We can support old outputs -- those which use utility[1, a, var] -- forever, while taking advantages of new boxes and bug fixes along the way.

utility[1,a_,var_]:=Panel[{Slider[Dynamic[a]],Slider[Dynamic[var]],Dynamic[{a,var}]}//Column]

Or if there are fundamentally new features, bump the version number, or otherwise change the calling signature.

utility[2,a_,b_,var_]:=Panel[Column[{Slider2D[Dynamic[{a,b}]],Manipulator[Dynamic[var]],Dynamic[{a,b,var}]}]]

DynamicModule[{a=0, b=0},Dynamic[utility[2,a,b,var]]]

Backward compatibility

New outputs can even avoid displaying a mass of pink boxes when used with older versions of your code.

utility[___] := Framed["You'll need a newer version to display this."]

DynamicModule[{a, b,c,d,e},Dynamic[utility[3,a,b, c,d,e,var]]]

Other examples

Manipulate[x,{x,0,1,ControlType -> IntervalSlider}]

IntervalSlider[Dynamic[int],FutureOption -> FutureValue]

Other compatibility issues

Layout details can change over time, or platform (Windows vs Mac, Desktop vs Cloud, …), or user preference, or even things like handedness.

Example: DefaultControlPlacement

CurrentValue[$FrontEnd,DefaultControlPlacement]=Bottom;

Manipulate[x,{x,0,1}]

CurrentValue[$FrontEnd,DefaultControlPlacement]=Inherited;

You almost always get this for free, just by using Dynamic.

Loading your code

Use conditions inside your Dynamic to react to when your code is not yet loaded.

DynamicModule[{},Dynamic[If[TrueQ[myCode`loaded],"ready",Button["Refresh",myCode`loaded=True]]]]

Initialization

Use DynamicModule’s Initialization option to always make sure your code is loaded. This gives you much greater control than SaveDefinitions.

DynamicModule[{initdone},Dynamic[If[TrueQ[initdone],"ready","not ready"]],Initialization:>(loadYourCode[];Pause[1];initdone=True),SynchronousInitialization->False,UnsavedVariables:>{initdone}]

Please wait…

You can get arbitrarily clever in the “not ready” display.

DynamicModule[{initdone},Dynamic[If[TrueQ[initdone],"ready",Column[{ProgressIndicator[Appearance->"Percolate"],"loading…"}]]],Initialization:>(loadYourCode[];Pause[2];initdone=True),SynchronousInitialization->False,UnsavedVariables:>{initdone}]

This is how Manipulate’s SynchronousInitialization option works, for instance:

Manipulate[x,{x,0,1},Initialization:>(Pause[2];x=0.5),SynchronousInitialization->False]

Bonus: More info about wait indicators

See extensive discussion in “The Principle of Evaluation Blocking” here.

Advanced: Inner vs Outer state

Keep “inner” utility variables in sync with user variables.

This technique is used in the implementation of IntervalSlider.

Sharing inner state

Pushing state out of an interface is simple: Use Dynamic’s second argument.

outer = 1; Slider[Dynamic[outer],{0.5,1},Appearance ->"Labeled"]

DynamicModule[{inner=1/outer},Slider[Dynamic[inner,(inner=#;outer=1/#)&],{1,2},Appearance ->"Labeled"]]

Tracking outer state

Importing state into an interface requires a bit more effort: Use DynamicWrapper.

DynamicModule[{inner=1/outer},DynamicWrapper[
Slider[Dynamic[inner,(inner=#;outer=1/#)&],{1,2},Appearance ->"Labeled"],inner=1/outer]]

Advanced: When bad things happen to good kernels

Used by message formatting features in version 11.

Bonus

Kernel session-dependent features

DynamicModule[{sessionid=$SessionID},Dynamic[If[sessionid===$SessionID,"OK",Row[{"Not OK…", Button["Refresh",sessionid=$SessionID]}]]]]

Quit

Advanced: Realities of debugging

Sometimes, it is very important to be able to see the actual boxes being rendered, not just the abbreviated boxes which were burned into notebook files.

Bonus

Getting rid of all Dynamics

Evaluation menu > Convert Dynamic to Literal

The Setting function is also useful here.

?Setting

Getting rid of a single Dynamic

If you want to see what a particular DynamicBox turns into, use the DynamicBox option DestroyAfterEvaluation -> True.

For instance, evaluate this:

IntervalSlider[Dynamic[int]]

select the output cell, choose Show Expression from the Cell menu, and add that option setting to the options of the DynamicBox. If you toggle Show Expression off and then back on, you will see the expanded boxes.

Manipulate usage

Viewing the actual boxes for Manipulate was so important, there’s a Method option for toggling the behavior.

Manipulate[x,{x,0,1}]

Manipulate[x,{x,0,1},Method -> {"TemplateExpand" -> True}]

Conclusion

Store only what you absolutely must

Simplify what gets written to notebook files

Create the interface from kernel code at render time

Respond to state changes

Support forward and backward compatibility

Reflect the state of your code, and of the kernel

Future techniques

Attach interfaces to pieces of a notebook without being stored in the notebook file at all

predictive interface

image editors

color editors

…

Extend TemplateBox to make it a more natural choice for simple interfaces