Set up change handlers for application state

State change handlers allow applications to listen for changes to elements within application state, and respond accordingly.

The handlers come in two varieties: value changed handlers and child changed handlers. Both work the same way. The only difference is that value changed handlers are used for changes that occur to a single value, while child changed handlers are used for changes that occur at or below a specific path.

Steps

Register a handler

APIs:

XmlStateManager.AddValueChangedHandler()
C++ | .Net | Java | HTML5 | iOS | Android

XmlStateManager.AddChildChangedHandler()
C++ | .Net | Java | HTML5 | iOS | Android

Use AddValueChangedHandler to add a state changed handler that will fire when the specified value changes. Use AddChildChangedHandler to add a state changed handler that will fire any time the value of the specified path or its children changes.

Typically, these handlers are defined inside the application state initialization function. They won't fire if a value hasn't actually changed.

On the service

C++

// Adding a value changed handler
MyApp::StateManager().XmlStateManager().AddValueChangedHandler("/CustomProperty", Bind(this, &MyClass::OnValueChanged));

// Adding a child changed handler
MyApp::StateManager().XmlStateManager().AddChildChangedHandler("/ParentElement", Bind(this, &MyClass::OnParentOrChildValueChanged));

.Net

//Adding a value changed handler
Program.StateManager.XmlStateManager.AddValueChangedHandler("/CustomProperty", OnValueChanged);

//Adding a child changed handler
Program.StateManager.XmlStateManager.AddChildChangedHandler("/ParentElement", OnParentOrChildValueChanged);

Java

//Adding a value changed handler
stateManager.getXmlStateManager().addValueChangedHandler("/CustomProperty",OnValueChanged);

//Adding a child changed handler
stateManager.getXmlStateManager().addChildChangedHandler("/ParentElement",OnParentOrChildValueChanged);

On the client

HTML5

//Adding a value changed handler
pureweb.getFramework().getState().getStateManager().addValueChangedHandler("/CustomProperty",OnValueChanged);

//Adding a child changed handler
pureweb.getFramework().getState().getStateManager().addChildChangedHandler("/ParentElement",OnParentOrChildValueChanged);

iOS (Obj-C)

//Adding a value changed handler
[[PWFramework sharedInstance].state.stateManager addValueChangedHandler:[NSString stringWithFormat:@"/CustomProperty"]
        target:ObjectToNotifyOfEvent
        action:@selector(OnValueChanged)];


//Adding a child changed handler
[[PWFramework sharedInstance].state.stateManager addChildChangedHandler:[NSString stringWithFormat:@"/ParentElement"]
        target:ObjectToNotifyOfEvent
        action:@selector(OnParentOrChildValueChanged)];

Android

//Adding a value changed handler
framework.getState().getStateManager().addValueChangedHandler("/CustomProperty", new OnValueChanged());

//Adding a child changed handler
framework.getState().getStateManager().addChildChangedHandler("/ParentElement", new OnParentOrChildValueChanged());

You can also unregister these handlers using methods such as removeValueChangedHandler, removeChildChangedHandler, removeAllValueChangedHandlers, and removeAllChildChangedHandlers.


Define the handler

APIs:

ValueChangedEventArgs
C++ | .Net | Java | HTML5 | iOS | Android

The handling function takes the ValueChangedEventArgs object, which encapsulates the information about the change that triggered the handler, such as the path where the change occurred, the type of change (insertion, deletion, or modification), and the new value.

You can use this information to respond appropriately to the state change.

The syntax of the handler will look something like this:

On the service

C++

void MyApp::OnValueChanged(ValueChangedEventArgs args)
{
    // Do something useful
}

.Net

private void OnValueChanged(object Sender, ValueChangedEventArgs args)
{
    // Do something useful
}				

Java

private class OnValueChanged implements EventHandler<ValueChangedEventArgs>
{
    public void invoke(Object sender, ValueChangedEventArgs args)
    {
        // Do something useful
    }
}

On the client

HTML5

function OnValueChanged(e) {
{
    // Do something useful
}

iOS (Obj-C)

- (void)OnValueChanged:(PWValueChangedEventArgs *)args
{
    // Do something useful
}				

Android

private class OnValueChanged implements EventHandler<ValueChangedEventArgs>
{
    public void invoke(Object sender, ValueChangedEventArgs args)
    {
        // Do something useful
    }
}

In some cases, you will need to explicitly set a state lock at the start of your handler. For example, if you define a value changed handler that checks the validity of the value before writing it to application state, you should lock application state at the start of the handler.

Special cases

Responding to node deletions

The APIs are designed so that value changed handlers only trigger for changes resulting in a valid value in application state. In the case of a node deletion, the value no longer exists, which means the handler will not be called.

Similarly, child changed handlers only fire if a descendant of the watched path changes; when a node at the watched path is removed, this will work only if no child has changed.

To take an action in response to the deletion of a node in application state, attach a child changed handler to the parent of the path.

Any element that you want to watch for deletions must have a parent.

For example, given application state path /A/B/C, in order to be notified of the deletion of C, place a child changed handler on the path /A/B. This will catch all changes below the path /A/B; to know which ones are deletions, use the methods in the ValueChangedEventArgs object received back in the handler, such as:

  • getPath for knowing which path was impacted.
  • isDeletion for knowing whether the change was a deletion.

In some scenarios, listening to the parent element for updates can significantly increase the number of callbacks. In this case, an alternate option instead of actually deleting the node is to set it to a value agreed-upon to mean deletions, such as null or -1 (this will only work if the selected value is not otherwise used).


Nesting handlers

It is possible to nest handlers, so that when the value for /A changes, the corresponding handler responds by changing the value of /B, which in turns triggers the handler for /B, which could be used to change the value of /C, and so on.

When nesting handlers, special care must be taken to avoid some common pitfalls:

  • Infinite loops get created when a handler in the sequence changes a value that appeared earlier in that sequence (if the handler for /C was used to change the value of /A again, for instance). This may seem like an obvious mistake to avoid, but is harder to detect when making batch changes to large numbers of elements.
  • A similar loop will result if you create a child handler for a parent, and a separate handler for one of the children under this parent. Also, because order of callbacks is not guaranteed (see Under the hood), nested child changed handlers can return unexpected results. Best practice is to create a single handler, at the parent level, which provides the logic for responding to changes to any child value of that parent.

    If you must have separate handlers for parent/child elements, consider revisiting your tree hierarchy so that the elements are siblings instead,


Enforcing a callback order

Because of the way PureWeb handles application state under the hood, the order in which state change handlers are called cannot be guaranteed. For this reason, it is best to avoid creating situations where a specific callback order must be enforced.

However, if you have a special use case and are keen to manually enforce a given order of operations for your application state callbacks, there is a workaround solution: create a special element in application state exclusively for this purpose. The state change handler for that element can then carry out the operations on the other elements in the order that you would like.

Example

Using a change handler for the Scribble pen color

The Scribble application uses a value changed handler to keep the color in sync between the client and the service.

On the client

Whenever the end user changes the pen color, the new color is written to an element in application state called ScribbleColor.

HTML5

In the user interface, there is a color drop-down box, which triggers the onChange function called changeScribbleColor when the end user makes a selection from that box. This function sets the value for the pen color:

function changeScribbleColor(e){
    pureweb.getFramework().getState().setValue('ScribbleColor', document.getElementById('color').value);
}

iOS (Obj-C)

In the user interface, there is a Color button, which can be used to display the text entry view. Whenever the end user enters a new color in this field, this triggers the function updateAppStateWithColor. This function sets the value for the pen color:

func updateAppStateWithColor(chosenColor : UIColor) {
    let colorName = reverseColorDict[chosenColor];
    PWFramework.sharedInstance().state().setAppStatePathWithValue("/ScribbleColor", value: colorName!);
}

Android

In the user interface, there is a Color option in the menu, which displays a list of available colors. Whenever the end user selects a new color from the drop-down, this trigger an onItemClick function. This function sets the value for the pen color:

public void onItemClick(AdapterView<?> parent, android.view.View v, int position, long id) {
    lastColor = position;
    framework.getState().setValue("/ScribbleColor", colorNames[lastColor]);
    view.refresh();
    dialog.dismiss();
}

On the service

Whenever the value of the ScribbleColor element changes in the client-side application state, the pen color changes on the service. This is achieved by registering and defining a value changed handler for this element. The handler also provides the logic to use a default color if the new color is not recognized.

C++

// Register a value changed handler to listen for color changes
CScribbleApp::StateManager().XmlStateManager().AddValueChangedHandler("ScribbleColor", Bind(this, &CScribbleView::OnScribbleColorChanged));
					
// Define the handler
void CScribbleView::OnScribbleColorChanged(ValueChangedEventArgs args)
{
    String color = args.NewValue().As<String>();
    NamedColors c = black;
    if (!(c.TryParse(color, c)))
    {
        CScribbleApp::StateManager().XmlStateManager().SetValue("/ScribbleColor", "black");
    }
    m_cColor = c;
}

.Net

// Register a value changed handler to listen for color changes
Program.StateManager.XmlStateManager.AddValueChangedHandler("ScribbleColor", OnScribbleColorChanged);
			
// Define the handler
private void OnScribbleColorChanged(object Sender, ValueChangedEventArgs args)
{
    Color newColor = Color.FromName(args.NewValue);
    if (!newColor.IsKnownColor)
    {
        // reset app state to old pen color name
        Program.StateManager.XmlStateManager["ScribbleColor"] = m_pen.Color.Name;
        return;
    }

    m_pen = new Pen(newColor, PenWidth);
    if (m_offscreen != null)
    {
        PaintStrokes(Graphics.FromImage(m_offscreen), this.Size);
    }

    Invalidate();
}				

Java

// Register a value changed handler to listen for color changes
framework.getState().getStateManager().addValueChangedHandler("/ScribbleColor", new ColorChangedHandler());

// Define the handler
private class ColorChangedHandler implements EventHandler<ValueChangedEventArgs> {
    @Override
    public void invoke(Object sender, ValueChangedEventArgs args) {
        PureWebKnownColor[] colors = PureWebKnownColor.values();
        for (int i = 0; i < colors.length; i++) {
            if (colors[i].name().equalsIgnoreCase(args.getNewValue())) {
                lastColor = i;
            }
        }
    }
}