Using the Data API

A good looking UI is merely good looking if it isn't connected to something. This document will step you through using the Data API to connect the UI to your application code.

Hooking up a Source node

Let's start off with one of the simplest examples you can get. Black text on a white background.

The Source node is the integration point between your application code and Ink's node system. Let's connect that 'text' property to a Source node.

Writing a value

In order pass a value to the Source node we need to do two steps:

  1. Obtain a reference to the Data object associated with the Source node.
  2. Set a value on that data object

See the reference documentation for the Data (C#, C++) class for a full list of types available.

Reading a value

Connections in Quill are bi-directional. If a view has its 'editable' property set, the user can modify the text. Booleans can be flipped with the Toggle node. These changes are then propagated backwards to the point that the value originated from. At any point, we can read that modified value with a getter method.

When does the value change? (Callbacks)

In some applications you may only need the user modified values at a specific point in time. In other applications it may work fine to constantly read the value. But in most applications you will want to be notified of changes.

The bindings for higher level languages provide methods for registering a callback when a change on a particular field occurs.

You can also add a callback for listening for any field change on a Data object.

Most language bindings also provide a lower level Listener (C#, C++) interface for listening for changes. In some cases it may be a better choice than setting callbacks on individual Data objects. See Change Listeners for more information.

Instantaneous user actions (Impulses)

Many user interactions don't involve editing a value. Clicking on something may need to result in executing code.

For example, in this screenshot a view's Mouse Down event is connected to an impulse field of a Source node. Clicking on the view will fire an impulse, which can be handled in code:

var appRoot = new Ink().Source("app");
appRoot.OnFieldChange("action", (ev) => {
    Console.WriteLine("The user wants the action performed!");


Not every user interaction that involves clicking should be handled in code. If you have a checkbox, you don't want to handle an impulse in code. Hook a boolean value to a Toggle node instead.

Triggers: somewhat the opposite of impulses

Impulses are used for handling an instantaneous action from the user. Triggers are used for communicating an instantaneous event to the user. Using code to fire a trigger is useful when you want to start an animation that isn't associated with a value changing.

Since triggers and impulses are designed for different purposes they have different behavior beyond which direction they flow.

Lists and Nested Data

Although many user interfaces only need one of something -- a camera app only needs one shutter button -- you'll likely need lists of things.

Let's imagine an application that displays a user's personal music library. The list property of a list view is connected to a source node.

The Song template has two inputs for the song title and artist.

The simplest way to add entries to this list is to use the Data.Append (C#, C++) method. It will add a new entry to the list and return an associated Data object. Values set on this Data object are matched with the inputs to the Song template.

(Of course in a real application you wouldn't have variables like song1 and song2 and hard coded strings. You would write a loop over your application specific data structure.)

Updating nested Data objects

Most user interfaces will need to modify previously supplied values at some point. For values at the top level of a source node, this is easy. A value can be modified in the same way that it was originally set.

However, updating values nested inside a list is a little more involved. There are a number of ways to accomplish this. Which pattern is best depends on the situation.

Holding a reference to the nested Data objects

Let's assume you have a stock ticker app built using an object oriented approach with a class instance for each stock symbol. When a new quote arrives, you call a public method set_quote() on the instance. A simple way to integrate with the UI would be to hold a reference to the Data object inside of the class.

This pattern is often the simplest to implement if you are already maintaining a class for the things you want to display in the UI.

Querying for the nested Data objects when needed

There are some cases where you may not want hold a reference to the Data object. Here, the previous example is modified to use Data.Query (C#, C++) instead of holding a reference.

This particular example isn't really an improvement over simply holding a reference. But, using Data.Query (C#, C++) can be useful if you don't want to add UI specific members to the class (because it's shared with non-UI components,) or if you aren't mantaining a data structure at all.

In this example, the UI for the stock ticker is implemented in two simple functions.

This pattern is often best when you already have a system in place to dispatch events related to the things you want to display in the UI.

Clearing and rebuilding the list

The previous examples all assume that there is other code in the application to keep track of which stock symbols are added or removed. But if you don't already have such code in place for other parts of the application, there may be no need to write it just for the UI.

Imagine that the stock quotes are fetched as a complete list. If you are maintaining your own data structures you will need to write logic to track which symbols are newly added to the list and which symbols are no longer present and must be removed. This kind of code is tedious and error prone to write. Why not just recreate the list every time it is updated?

This example is the simplest way to update a list of data. In many situations this works perfectly fine, but it has one downside. From Ink's point of view every quote is added and removed every time. This prevents the designer from immplementing animations for adding, removing, or changing quotes.

Using the Rebuild API

In order for ink to trigger animations when a stock symbol is added, removed, or changed, the Data objects need to persist from one update to the next. We can use Data.Rebuild (C#, C++) to accomplish this while still structuring our code in a way that is similar to the previous pattern.

Using Data.Rebuild (C#, C++) and Data.Query (C#, C++) the code is almost as simple as the example that clears and rebuilds the list, but Ink now tracks which objects are new, which are modified, and which are no longer needed.

This pattern can also be used to structure your code in a way similar to using an immediate mode UI library.

Instanced Source nodes

Source nodes are "global". All Source nodes in the system with the same name refer to the same Data object. Code can set values on it at any time, even before the Source node is instantiated. The same values can be accessed by different templates, without requiring code to set them multiple times.

In most cases this is exactly what you want. But there are some cases where you will want to provide different values to different template instances.

Instanced Source nodes create a separate Data object each time they are instantiated. If it is in a template for a list, one Data object will be created for each entry in that list. If it is in a template for a Scene node, the Data object will be created when the scene is opened and removed when the scene is closed.

TODO document the following:
    reasons to use:
        creating a reusable component
        structuring code
        event when it is created


TODO turn this section into prose

Data interactions are thread safe.

There is no single UI thread for data interactions.

Read / Write data on any thread.

poll on Listener (C#, C++) objects on any thread.

callbacks are called on the same thread that calls Ink.Step (C#, C++)

C refcounting considerations