Skip to main content

Runtime Properties

A runtime property is defined at runtime. It can be computed from other properties in the document.

Overview

Since Runtime Properties can be computed from other properties at runtime, they do not need to be stored when the document is saved. Furthermore it is perfectly possible to define a runtime property which depends on another runtime property, a useful design pattern allowing you to build complex chains of dependencies. Such dependencies are conveniently implemented using event triggers.

Typical Problems, and Solutions

Runtime properties can and should be used for any state in the model that can be computed from the state of other properties. The range of uses is both broad and deep.

Range Filter Change

Consider a range filter: When the user moves the slider, the state of the node that represents the range filter is changed. This filtering change propagates to the data underlying visualizations.

Should both the filtering and the data of the visualization stored on the undo stack? The answer is no. Only the value of the range filter must be restored to recompute the rest.

Just-in-time computation

Consider a Summary Table: It needs to compute the measures whenever the filtering is changed. On the other hand, if the page with the summary table is not visible the computation is not needed until the user moves to that page. In short, as long as it remains unused, the property should not be recalculated.

How can the value of the runtime property be computed at the proper moment? The answer is that the value of the runtime property is cleared when any event trigger defining the dependency fire. The cleared property is invalidated. When read, an invalidated property is recomputed and cached.

Implementation Pattern

Suppose you have a document node with two properties, X and Y. Furthermore, assume that you also want a Sum property that is the sum of X and Y. First, as for undoable properties:

  1. Define a property name.
  2. Define a private readonly field of type RuntimeProperty<T> where T is the type of the property, and mark it with the NonSerialized attribute.
  3. Define a public property with a get accessor.
    Runtime properties have no set accessor since they are computed from other properties.

Next create an InitRuntimeProperties method taking an out parameter, the field where the property will be stored.

Finally create your runtime property by calling the CreateRuntimeProperty method with the following parameters:

  1. The property name
  2. A delegate returning an event trigger specifying when the property shall be invalidated and recomputed.
    The delegate is executed when the document node is attached and can access other nodes in the document.
  3. A delegate computing the value of the property.
    The delegate is called the first time the value of the property is read. The value is cached until the event trigger fires, which results in a re-computation the next time the property is read.
public class Example : DocumentNode
{
    public new class PropertyNames : DocumentNode.PropertyNames
    {
        public static readonly PropertyName X = CreatePropertyName("X");
        public static readonly PropertyName Y = CreatePropertyName("Y");
        public static readonly PropertyName Sum = CreatePropertyName("Sum");
    }

    private readonly UndoableProperty<int> x;
    private readonly UndoableProperty<int> y;

    [NonSerialized]
    private readonly RuntimeProperty<int> sum;

    public int X
    {
        get { return this.x.Value; }
        set { this.x.Value = value; }
    }

    public int Y
    {
        get { return this.y.Value; }
        set { this.y.Value = value; }
    }

    public int Sum
    {
        get { return this.sum.Value; }
    }

    public Example()
    {
        CreateProperty(PropertyNames.X, out this.x, 0);
        CreateProperty(PropertyNames.Y, out this.y, 0);

        InitRuntimeProperties(out this.sum);
    }

    internal Example(SerializationInfo info, StreamingContext context) : base(info, context)
    {
        DeserializeProperty(info, context, PropertyNames.X, out this.x);
        DeserializeProperty(info, context, PropertyNames.Y, out this.y);

        InitRuntimeProperties(out this.sum);
    }

    protected override void GetObjectData(SerializationInfo info, StreamingContext context)
    {
       base.GetObjectData(info, context);

       SerializeProperty(info, context, this.x);
       SerializeProperty(info, context, this.y);
    }
    
    private void InitRuntimeProperties(out RuntimeProperty<int> sum)
    {
        CreateRuntimeProperty<int>(
          PropertyNames.Sum,
          out sum,
          delegate()
          {
              return
                Trigger.CreatePropertyTrigger(
                          this, 
                          PropertyNames.X, 
                          PropertyNames.Y);
          },
          delegate()
          {
              return this.X + this.Y;
          });
    }
}

Events from Runtime Properties

It is possible to define event handlers that are triggered when the value of a runtime property is invalidated. The event is raised when the property is invalidated, not when it is changed. This has two consequences:

  • An event is raised even if the value of the runtime property remains the same after it has been recomputed.
    Since the property is computed lazily there is no way to determine whether the value has actually changed.
  • If a runtime property is invalid, it cannot be invalidated. No event is raised if any dependency changes again.
    This is important for performance in many scenarios since it limits the number of events.

This can be translated into the following design guidelines:

  • External event handlers listening to the invalidation of runtime properties are common in good design.
  • Internal event handlers listening to the invalidation of runtime properties are likely to result in fragile design.

Development Pitfall Using External Event Handlers

Suppose you develop a UI component. A common approach is to first make a skeleton for your component, including event handlers to be called when your component needs to be updated. If you leave an event handler empty, except for a breakpoint, and start debugging, the handler may not be called when you expect it. But when you implement the handler, it gets called when you expect it to be called.

Usually this behavior is caused by a kind of vicious circle: Your event handler both listens to and reads the runtime property. When the event handler reads the property it also forces it to be evaluated. The property will be invalidated when anything that it depends on changes. When the event handler is empty, the runtime property is invalid at all times and no event is sent.