When developing Ki for SproutCore, the main goals I aim for are the following:

  1. Follow the principals laid out in David Harel’s seminal paper Statecharts: A Visual Formalism for Complex Systems.
  2. Make it intuitive to take a statechart diagram and translate it into code, and vice versa.
  3. Help reflect code that is simple, modular, extensible and maintainable.

In addition to the above, Ki needed to integrate well with SproutCore itself, meaning that it contained the all necessary functionality allowing a statechart to respond to actions and events propagated from various parts of SproutCore, and, in order to function, required the minimal amount of effort to add a statechart to an application.

Although a lot of work has been put in to meeting each of those goals, there always ends up being something that can be done to get Ki even closer, and in the last few weeks I worked on improving Ki to do just that. The improvements I set out to meet were to:

  1. Make statecharts easier to create
  2. Allow statecharts to automatically be part of a responder chain
  3. Allow an object to easily delegate work to a statechart’s current states

Let’s see how these improvements have been met with the latest version of Ki.

Make Statecharts Easier to Create

When you build a statechart, you start with defining an explicit root state and then build your other states out from there, like in the following:

MyApp.statechart = Ki.Statechart.create({

  rootState: Ki.State.design({

    initialSubstate: 'stateA',

    stateA: Ki.State.design({ ... }),

    stateB: Ki.State.design({ ... })

  })

});

The code above is easy to write, but with that being said, there are certain things that could be done to make it easier to construct a statechart. Look at the root state. While needed, it, for the most part, is not something that everyone needs to explicitly define. Therefore, removing that step would be helpful. And with the root state not explicitly defined, that means that the property initialSubstate can instead be called initialState. This also means that the property substatesAreConcurrent can be instead be called statesAreConcurrent. With those changes, the statechart can now be written like so:

MyApp.statechart = Ki.Statechart.create({

  initialState: 'stateA',

  stateA: Ki.State.design({ ... }),

  stateB: Ki.State.design({ ... })

});

Of course you can still define the root state explicitly like before, if you so choose to, but with this new approach, the statechart will implicitly create a root state for you and set states A and B as the root state’s substates. Let’s see what the code looks like when the states are concurrent to each other:

MyApp.statechart = Ki.Statechart.create({

  statesAreConcurrent: YES,

  stateA: Ki.State.design({ ... }),

  stateB: Ki.State.design({ ... })

});

If you just want to define the class to be used for the root state, you simply need to set the rootStateExample property, like so:

MyApp.FooState = Ki.State.extend({ ... });

MyApp.statechart = Ki.Statechart.create({

  rootStateExample: MyApp.FooState,

  initialState: 'stateA',

  stateA: Ki.State.design({ ... }),

  stateB: Ki.State.design({ ... })

});

You can alternatively use the Ki.State.plugin feature to set the root state example.

Although statecharts are typically viewed as something that you use to manage an entire application, it is equally valid to apply statecharts to individuals objects that require their own independent, special states. To do this, you can simply add the Ki.StatechartManager mixin to any object and then add states to the object like you’ve seen above:

MyApp.MyView = SC.View.extend(
  Ki.StatechartManager,
{

  initialState: 'stateA',

  stateA: Ki.State.design({ ... }),

  stateB: Ki.State.design({ ... })

});

Nothin’ to it, eh?

Automatically be Part of a Responder Chain

Knowing that any object can become a statechart in order to manage a set of its own states is good to keep in mind, especially when the concept it applied to views, or any object that happens to derive from the SC.Responder class for that fact. Why? Views, and responder objects in general, can be part of a responder chain. A responder chain is an ordered set of responders that is iterated over from start to end to find an object that will respond to an action or event. The most common responder chains are those created to handle user events, such as mouse down, mouse up, and so on. As an example, let’s say you’ve created a custom view that can handle the mouse down and mouse up events:

MyApp.MyView = SC.View.extend({

  mouseDown: function(evt) { ... },

  mouseUp: function(evt) { ... }

});

If the mouse down event was passed through the responder chain and an instance of MyApp.MyView was part of it, then so long as a responder before it didn’t handle the event, the instance of MyView would. If MyView didn’t handle the event either because it didn’t have the function or the function returned false, then the next responder would be given the opportunity, which, by default, would be the view’s parent view.

Great, so what does this have to do with statecharts? Well, it would be nice if the view’s states could also be given a chance to handle an event before trying the next responder in the responder chain, and this would then allow you to better manage how a view reacts to events. This is now easy to do, as can be seen with the following:

MyApp.MyView = SC.View.extend(
  Ki.StatechartManager,
{

  initialState: 'stateA',

  stateA: Ki.State.design({
    mouseDown: function(evt) {
      ...
      this.gotoState('stateB');
    }
  }),

  stateB: Ki.State.design({
    mouseUp: function(evt) {
      ...
      this.gotoState('stateA');
    }
  })

});

With the updates made to Ki, there is no fussing with having to get a view’s states to be part of the responder chain — that is handled implicitly by the framework for you.

What if the view possibly needs handle an incoming event itself? What happens then? Not much. You just add the event handlers onto the view directly like you normally did. If the event handler exists on the view and does not return false then the view itself handled the event, otherwise the view’s current state or states will be given the opportunity to handle the event. Example:

MyApp.MyView = SC.View.extend(
  Ki.StatechartManager,
{

  initialState: 'stateA',

  stateA: Ki.State.design({
    doubleClick: function(evt) {
      ...
      this.gotoState('stateB');
    }
  }),

  stateB: Ki.State.design({
    doubleClick: function(evt) {
      ...
      this.gotoState('stateA');
    }
  }),

  doubleClick: function(evt) {
    if (/** can handle the evt */) {
      // ... do some stuff. Current state will
      // not be given a chance to handle the double
      // click event
      return YES;
    } else {
      // Can't handle the event. Therefore the
      // view's current state will be given an
      // opportunity to handle the event
      return NO;
    }
  }

});

Again, not much to it. Ki takes care of all of the plumbing for you.

Delegate Work to a Statechart’s Current States

While it’s good that Ki now lets a statechart’s states to automatically be part of a responder chain, there still seems to be something missing. As an example, let’s say we have a custom view that overrides the render method, and based on the current state of the view, we render in a particular way:

MyApp.MyView = SC.View.extend(
  Ki.StatechartManager,
{

  initialState: 'stateA',

  stateA: Ki.State.design({ ... }),

  stateB: Ki.State.design({ ... }),

  render: function(context, firstTime) {
    var cs = this.get('firstCurrentState');

    if (cs === this.getState('stateA')) {
      // ... update view based on state A
    else if (cs === this.getState('stateB')) {
      // ... update view based on state B
    }

    sc_super();
  }

});

Looking at the render method above, it seems, well, clunky. We have states A and B, but in the render method, an if-else statement is used to decide on how the view should be rendered. This doesn’t seem very extensible or maintainable. Every time we add a new state to the view we have to add on to the if-else. And if we want to change how the rending logic works based on the current state, we have to update the logic within the method itself. Yuck. Given this lack-luster approach, it would be far better if we could simply delegate part or all of the rendering out to the view’s current state. With the updates made to Ki, you can do that using invokeStateMethod as the following shows:

MyApp.MyView = SC.View.extend(
  Ki.StatechartManager,
{

  initialState: 'stateA',

  stateA: Ki.State.design({
    render: function(context, firstTime) {
      ...
    } 
  }),

  stateB: Ki.State.design({
    render: function(context, firstTime) {
      ...
    } 
  }),

  render: function(context, firstTime) {
    this.invokeStateMethod('render', context, firstTime);
    sc_super();
  }

});

Ah, much better. The view’s render method has now been greatly simplified, and all of the rendering logic has been delegated out to the states.

A few things to note about using the invokeStateMethod method. First, it should not be used to invoke methods on a state that are intended to handle events. For that, use the standard sendEvent method. Second, when invokeStateMethod is used, it will apply to all current states. So, for example, if you have two current states and they both have the render method, then render will be invoked on each of them — and order of which one is executed is not guaranteed. Finally, if the given method can not be invoked on the current state, then that state’s parent states will be tried in order of immediate ancestry, but parent states will only ever be tried once per invokeStateMethod call.

You can do more with the invokeStateMethod, but I would encourage you to read the method’s comments in the statechart.js file for full details.

Accessing a State’s Owner

In the cases where states can handle an event or be delegated a task when invokeStateMethod is called, there may be the need for the states to access something on the object that manages them, such a property or method. All states can access its owner’s properties and methods via the state’s owner property. As an example:

stateA: Ki.State.design({

  doubleClick: function(evt) {
    ...
    this.get('owner').displayDidChange();
  },

  render: function(context, firstTime) {
    ...
    context.push(this.getPath('owner.color'));
  }

});

In addition to the state being able to access its owner, you can also change who the owner is by setting the statechart’s owner property, like so:

MyApp.MyView = SC.View.extend(Ki.StatechartManager, {
  ...
});

myView = MyApp.MyView.create();
myView.set('owner', someObject);

Changing the owner will immediately update the owner on all of the states belonging to the statechart.

In Conclusion

Hopefully this gives you a good overview of the changes recently made to Ki. And for those jumping up and down for the SproutCore 1.5 release, everything in Ki will be brought over to the 1.5 release with the only difference being the namespace used — SC instead of Ki.

In the mean time, happy statechart coding!

-FC

Advertisements