Coding with statecharts

While you can, and should, design statecharts for applications written in any language, only a few frameworks allow you to directly implement the statechart in your code. Fortunately for us, SproutCore is one of those frameworks. What's really amazing is that all the hard work we did designing the statechart diagram takes care of all the logic for our application state layer. All we have to do now is implement the states one-for-one into the code. Let's look at how we do that now.

While you can have multiple statecharts in your application, we only need one to manage the application state. To create a statechart, SproutCore provides the SC.StatechartManager mixing that will turn any object into a statechart. While some people like to mix SC.StatechartManager into the application object, for example, in core.js:

MyApp = SC.Application.create(SC.StatechartManager, { 
  // …
});

I think it's nice to use a separate statechart SC.Object, for example, in a file called statechart.js:

MyApp.statechart = SC.Object.create(SC.StatechartManager, {
  // ...
});

Simply by doing one of the preceding methods, we now have a statechart that we can use to manage our application. The next step is to transcribe what we've diagramed across to the statechart object. Since every statechart has a root state, we would add that state first.

All states in SproutCore are created by extending SC.State and so we simply define the rootState property as follows:

MyApp.statechart = SC.Object.create(SC.StatechartManager, {

  rootState: SC.State.extend({
    // ...
  })

});

To add substates, simply add them to the appropriate parent state. Remember that any state that has substates should define the initial substate, which is done with the initialSubstate property. Using the first application statechart diagram as an example we would get the following code:

MyApp.statechart = SC.Object.create(SC.StatechartManager, {

  rootState: SC.State.extend({
    initialSubstate: 'loadingState',

    loadingState: SC.State,

    readyState: SC.State,

    editState: SC.State,

    savingState: SC.State
  })

});

After this we can add the relative state actions and events, which will simply transition to the appropriate state. Building upon the preceding example again, we would have the following code:

MyApp.statechart = SC.Object.create(SC.StatechartManager, {

  rootState: SC.State.extend({
    initialSubstate: 'loadingState',
    loadingState: SC.State.extend({
      didLoad: function () {
        this.gotoState('readyState'),
      }
    }),

    readyState: SC.State.extend({
      doEdit: function () {
        this.gotoState('editState'),
      }
    }),

    editState: SC.State.extend({
      doCancel: function () {
        this.gotoState('readyState'),
      },

      doSubmit: function () {
        this.gotoState('savingState'),
      }
    }),

    savingState: SC.State.extend({
      saved: function (successOrFailure) {
        if (successOrFailure) {
          this.gotoState('readyState'),
        } else {
          this.gotoState('editState'),
      }
    })
  })

});

Wow! Isn't it neat how simply the diagram came across into code and how cleanly we can follow the statechart logic in our code? Do you see how while in a certain state, our application is locked down to the appropriate events and actions for that state? Let's go a little bit further using the other examples.

Another example that we looked at previously used a history state to return to the last substate entered before exiting. Just as we used gotoState to move between substates without history, we use gotoHistoryState to move between states with history. The following is a Root state we could use for such a chess game:

// …
rootState: SC.State.extend({
  initialSubstate: 'menuState',

  menuState: SC.State.extend({
    // Start a new match.
    startMatch: function () {
      this.gotoState('matchState'),
    },

    // Resume the previous match.
    resumeMatch: function () {
      this.gotoHistoryState('matchState'),
    }
  }),

  matchState: SC.State.extend({
    initialSubstate: 'whitePlaysState',

    // Pause the match.
    pauseMatch: function () {
      this.gotoState('menuState'),
    },

    whitePlaysState: SC.State.extend({
      move: function () {
        this.gotoState('blackPlaysState'),
      }
    }),

    blackPlaysState: SC.State.extend({
      move: function () {
        this.gotoState('whitePlaysState'),
      }
    })
  })
}) 
// …

Notice how we simply use gotoHistoryState in the resumeMatch action when we want to resume our game? It's remarkably easy.

The next item we captured in the example diagrams was our entry and exit actions. The SC.State function provides the methods enterState and exitState to do just this. The next example shows how we would implement the LogIn state from an earlier diagram:

logInState: SC.State.extend({

  enterState: function () {
    // Append the log in pane.
    MyApp.logInPage.get('logInPane').append();

    // Reset the form.
    this.resetForm();
  },

  exitState: function () {
    // Remove the log in pane.
    MyApp.logInPage.get('logInPane').remove();
  }
  
})

Finally, we won't want to define all of our states inside of a single file because it will grow too large. Instead, we usually create a states directory in the app and give each state its own file. To make this easy, SC.State includes a plugin method that we use to reference the external state classes. Using our first statechart again as an example, the statechart.js file could end up looking something like the following code:

MyApp.statechart = SC.Object.create(SC.StatechartManager, {

  rootState: SC.State.extend({
    initialSubstate: 'loadingState',

    loadingState: SC.State.plugin('MyApp.LoadingState'),
    readyState: SC.State.plugin('MyApp.ReadyState'),
    editState: SC.State.plugin('MyApp.EditState'),
    savingState: SC.State.plugin('MyApp.SavingState')
  })

});

This makes the statechart file much easier to read now that the code that was previously inside each substate has been moved to its own file. For example, the MyApp.LoadingState would be defined in states/loading_state.js and would start out with just the subclass definition like the following code:

MyApp.LoadingState = SC.State.extend({
  didLoad: function () {
    this.gotoState('readyState'),
  }
});

There are a few more tricks you can do with SproutCore's statechart library, but that covers everything we want to do so far with statecharts. By the way, to follow along with your new statechart in action, you can set the trace property to true on the statechart, which will log lots of useful information to the browser's console.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.16.51.157