SproutCore: Making Use of Delegation

A few days ago there was a post on the SproutCore Google Group by an individual asking if his code could be reviewed by people in the community. Given the request, I checked out the code from github and began to go through it. In a nutshell, I was reviewing a small SproutCore application that allows you to simply log in and log out of the application. The application makes use of Ki, a statechart framework, in order to keep track of what state the application is currently in. In addition, there are two custom views. One view represents a toolbar displaying whether you are logged in or logged out, and another view that represents a login form. The login form view also contains logic that will fake the log in procedure.

When reviewing both the toolbar view and the login form view, I noticed that both of them have a mouseDown method that, based on the view’s current state, would make a direct call to methods on the application’s statechart, like so (various code removed for clarity):

Login.ToolbarView = SC.View.extend({

  ...

  mouseDown: function(evt) {
    var target = SC.CoreQuery(evt.target);
    var state = this.state;
    if (target.attr('id') === 'login') {
      if (state === 'loggedOut') {
        Login.statechart.beginLogin();
      } else if (state === 'loginIn') {
        // nothing to do here
      } else if (state === 'loggedIn') {
        Login.statechart.logout();
      }
    }
  }

});

Typically, any time I see a class that is directly coupled with an object instance (sometimes referred to as hard coupling), that gives me big, red warning signs. Why? Well, for a few primary reasons:

  1. It just makes unit testing a view that more difficult
  2. It makes your view less modular and harder to reuse
  3. The direct coupling increases the chance of hard to pin-down side effects

Whenever I construct a view, or any class for that matter, I strive heavily for loose coupling in order to prevent the above undesired outcomes — and, besides, it’s just a good object oriented practice whether you program in Java, Ruby, and, yes, even JavaScript. So given the toolbar view with its hard coupling, how do we go about decoupling the view from the application’s statechart? For that we turn our attention to delegation.

Delegation is a practice that allows an object to effectively delegate some task or tasks to another object but without having to directly know what that object is. During the execution of your application you can then construct your object where you provide your view (or what have you) an object that will be delegated to.

Alright. Now that we know about delegation, how do we actually apply it to the toolbar view within SproutCore? There are two primary ways we can go about applying delegation to the view. The first pattern is the target-action pair. The second pattern is to create a delegate mixin. Let’s explore how each works.

The Target-Action Pattern

The target-action pattern makes use of two concepts (surprise, surprise): 1) A target that is to receive an action; and 2) the action that will be invoked on the target. That seems simple enough, and it is as you’ll find out.

Because the toolbar view should not know nor care about how the application actually goes about logging and logging out, we want to delegate those actions to some target whenever the view is clicked based on the view’s current state. Let’s now go ahead and update the toolbar view to make use of this target and action (code simplified for clarity):

Login.ToolbarView = SC.View.extend(SC.DelegateSupport, {

  logoutTarget: null,
  logoutAction: null,

  loginTarget: null,
  loginAction: null,

  ...

  mouseDown: function(evt) {
    var target = SC.CoreQuery(evt.target), action;
    var state = this.state;

    if (target.attr('id') === 'login') {
      if (state === 'loggedOut') {
        target = this.get('loginTarget') || null;
        action = this.get('loginAction');
      } else if (state === 'loggedIn') {
        target = this.get('logoutTarget') || null;
        action = this.get('logoutAction');
      }

      this.invokeDelegateMethod(target, action);
    }
  }

});

Above you’ll notice a few changes. First, the toolbar view now has four new properties: logoutTargetlogoutActionloginTarget, andloginAction. These properties are what get assigned to specify the targets and actions to handle the actual logging in and logging out. The second change is to the mouseDown method. mouseDown will get the target and action pair based on the view’s current state and then use theSC.DelegateSupport* mixin’s invokeDelegateMethod method in order to invoke the action on the target. Note that if the value of target is null theninvokeDelegateMethod will default the target to the view itself.

Great. With the toolbar view updated, let’s look at how we would apply this in the sample application. In the sample app there is a main page that contains the toolbar view, so let’s update the main page to take advantage of the view’s target and action properties (some code removed for clarity):

Login.mainPage = SC.Page.design({

  ...

  mainPane: SC.MainPane.design({
   
    childViews: 'toolbar mainView'.w(),
    
    toolbar: Login.Toolbar.design({
      title: "Login States",
      logoutTarget: Login.statechart,
      logoutAction: 'logout',
      loginTarget: Login.statechart,
      loginAction: 'beginLogin'
    }),
    
    mainView: SC.View.design({ ... })

  }),
  
  loginPane: SC.Pane.design({ ... })

});

With the changes to the main page, we now have the toolbar view linked with the app’s statechart. The statechart is both the target for the login and logout actions, which, in this case, are the statechart’s beginLoginand logout methods, respectively. Pretty easy, right? Yep.

Although we’ve done the trick of decoupling the the view from the statechart, we could further improve how we make use of the target-action pairs since there are a few limitations with the changes we’ve made.

For one, the values that can be assigned to the target properties must be objects, not, say, a property path string due to the wayinvokeDelegateMethod works. Second, if the a target property is nullinvokeDelegateMethod will only use the view itself as a default target. It would better (and cooler!) if we could make our solution more generic so that we could supply a property path to the target properties, and if the target happens to be null, to try and make use of the default responder on the pane the view belongs to or the application’s root responder. Thankfully it turns out that’s pretty easy to do in SproutCore.

Instead of using the SC.DelegateSupport‘s invokeDelegateMethod, we will use the root responder (SC.RootResponder) and call its sendAction method, like so:

mouseDown: function(evt) {
  var target = SC.CoreQuery(evt.target), action;
  var state = this.state;
  var rootResponder = this.getPath('pane.rootResponder');

  if (target.attr('id') === 'login') {
    if (state === 'loggedOut') {
      target = this.get('loginTarget') || null;
      action = this.get('loginAction');
    } else if (state === 'loggedIn') {
      target = this.get('logoutTarget') || null;
      action = this.get('logoutAction');
    }

    if (rootResponder) {
      rootResponder.sendAction(action, target, this,
        this.get('pane'));
    } 
  }
}

Without getting too deep into how the root responder’s sendAction works, the method essentially goes through a series of checks try to find a target that can handle the given action. To simplify how the logic actually works, the sendAction will first try the given target passed to the method. If the given target can not handle the action then the given pane will be checked, if one was provided. If the pane itself can’t handle the action then its default responder will be tried. If the pane options are a no go,sendAction will go on to check the current main pane, key pane, in addition to their default responders, and, finally, then check the root responder’s default responder. Like I noted, a bunch of checks are performed. The other nice thing about sendAction is that the supplied target can be a property path string that will get converted into an actual object. Cool. Let’s see how we can apply this back into our main page:

Login.mainPage = SC.Page.design({

  ...

  mainPane: SC.MainPane.design({

    defaultResponder: 'Login.statechart',   

    childViews: 'toolbar mainView'.w(),
    
    toolbar: Login.Toolbar.design({
      title: "Login States",
      logoutAction: 'logout',
      loginAction: 'beginLogin'
    }),
    
    mainView: SC.View.design({ ... })

  }),
  
  loginPane: SC.Pane.design({ ... })

});

The target properties on the toolbar view can been removed and we added the property defaultResponder on to the main pane that has been assigned a property path to the app’s statechart. Again, because the given target supplied to sendAction will now be null, the method will ultimately use the main pane’s default responder as the default target. Neat’o!

We got some good coverage on the target-action pattern, now let’s see how the delegate mixin pattern works.

The Delegate Mixin Pattern

The delegate mixin pattern differs from the target-action pattern in that we make use of a mixin that gets applied to some object that we want our view to delegate to. The idea seems simple enough. The first step then is that we need to define our mixin, which will be the following:

Login.ToolbarViewDelegate = {

  isToolbarViewDelegate: YES,

  toolbarViewLogout: function() { },

  toolbarViewLogin: function() { }

};

The first thing to note is the name of the mixin: ToolbarViewDelegate. This is just a convention that helps clarify that the mixin is used by the toolbar view in order to delegate specific tasks to an object.

The next thing to observe is the isToolbarViewDelegate property that has a default value of YES (i.e. a value of true). When you add a mixin to a class, the mixin’s type, so to speak, is not retained. Rather, SproutCore simply takes all the properties from the mixin and directly applies it to the given class. This means that without some property indicating that the class does indeed have the mixin applied, there is then no way to determine so. Therefore we use isToolbarViewDelegate in order to support such a check. This essentially allows for duck typing.

The final part of the delegate mixin are the methods themselves. We see that the mixin has two methods: toolbarViewLogout and toolbarViewLogin, which will both be called by the toolbar view. Notice the convention used for the naming of the methods. They both have “toolbarView” prefixed to the action. Why? The main reason is to avoid method clobbering when a object, say, mixes in multiple mixins. If we just had methods logout andlogin, there is a chance another mixin might have a method with the same generic name. Hence, clobbering.

Alright. With our delegate mixin in place, let’s update the toolbar view to make use of it (various code removed for clarity):

Login.ToolbarView = SC.View.extend(
  Login.ToolbarViewDelegate,
  SC.DelegateSupport,
{

  delegate: null,

  toolbarViewDelegate: function() {
    var del = this.get('delegate');
    return this.delegateFor('isToolbarViewDelegate', del);
  }.property('delegate').cacheable(),

  mouseDown: function(evt) {
    var target = SC.CoreQuery(evt.target);
    var state = this.state;
    var del = this.get('toolbarViewDelegate');
    if (target.attr('id') === 'login') {
      if (state === 'loggedOut') {
        del.toolbarViewLogin();
      } else if (state === 'loginIn') {
        // nothing to do here
      } else if (state === 'loggedIn') {
        del.toolbarViewLogout();
      }
    }
  }

}); 

A bunch of changes have been made to the toolbar view. First, the view mixes in both the Login.ToolbarViewDelegate and the SC.DelegateSupport*. The view also has two new properties. A standard property calleddelegate and a computed property called toolbarViewDelegate. And finally, the mouseDown has been updated in order to get a delegate and call one of its methods based on the view’s current state.

As you may have guessed, the delegate property is what gets assigned the object representing what will be delegated to, which means that the object will have the Login.ToolbarViewDelegate applied to it. But wait a minute. The toolbar view also has the Login.ToolbarViewDelegate. Does that mean the view is a delegate to itself? Well, basically, yes. But why is this? Essentially, by applying the delegate mixin to the view itself, you can make the view the default delegate if no value has been assigned to the view’s delegate property.

The delegate check is performed by the view’s toolbarViewDelegatecomputed property. toolbarViewDelegate determines what will be the actual delegate with the use of the SC.DelegateSupport‘s delegateFormethod. If any of the values supplied to delegateFor have theisToolbarViewDelegate property, then that value will be returned, otherwise the default will be the view itself if it has theisToolbarViewDelegate property, which, in toolbar view’s case, it does.

With the toolbar view now updated, the next question becomes what do we actually apply the delegate mixin to besides the view itself? Anything really, but a standard practice is to apply the mixin to a controller that contains the logic to then make calls to the statechart. So let’s go ahead and do that:

Login.toolbarController = SC.Object.create(
  Login.ToolbarViewDelegate,
{

  toolbarViewLogout: function() {
    Login.statechart.logout();
  },

  toolbarViewLogin: function() {
    Login.statechart.beginLogin();
  }

}); 

With the controller in place, now let’s go back to the main page and once again update the code:

mainPage = SC.Page.design({

  mainPane: SC.MainPane.design({

    defaultResponder: 'Login.statechart',

    ...

    toolbar: Login.Toolbar.design({
      layout: { ... },
      title: "Login States",
      delegate: Login.toolbarController
    }),

    ...
  }),

  ...

}); 

With the controller now assigned to the toolbar view, the view will go ahead and delegate to it any time the mouse down action is fired on the view. Awesome. As one note, you’ll notice that the main pane still has itsdefaultResponder property assigned to the app’s statechart. For the delegate mixin, the default responder will not be called as a default target like it was when the target-action delegate pattern was used.

Which Pattern to Use?

We’ve gone through a fair amount of detail about how the two delegate patterns work. The question now is which one do you use. If you size up the two patterns against the toolbar view, you’re know doubt leaning towards the target-action pattern since it seems less involved and you get more bang for the buck with respect to making use of default responders. And to be fair, for this case, I would tend towards the target-action pattern. So if that’s the case, then when would you actually want to use the delegate mixin pattern? Let’s look at each and understand when they are useful and when they are not.

With respect to the target-action delegate pattern, it’s more useful when you want to fire simple actions against a target based on some response, say, a view reacts to. The most basic example of this can be seen with the SC.ButtonView. The button view just fires an action against a target and doesn’t care about any finer grained details. Where the target-action pattern is less useful is for cases when you want to delegate fine grained details of a particular task or behavior, which is what the delegate mixin pattern is more suited for.

With the delegate mixin, you’re more focused on allowing a delegate to optionally modify how something works. If a delegate is not supplied to an object that wants to delegate, then that object will continue using default behavior, hence why it’s customary for an object delegator to be its own default delegate. The common example of the delegate mixin pattern is with the SC.CollectionView and the SC.CollectionViewDelegate.

Looking a bit further at the two patterns, another difference is the information passed to a delegate. For the target-action, you’re not really focused on supplying the action with additional info. I mean, you can, but that’s not its sole purpose. The delegate mixin, however, it more suited to supplying the delegate with additional argument in order for the delegate to make a more informed decision about what to do. Again, you can witness this if you look at SC.CollectionViewDelegate. In addition, the delegate mixin provides a cohesive set of methods and properties that are intended to work together in some fashion; using the target-action pattern, not as much since each action and target can been seen as independent — again, not that it has to be.

In should be noted that the dos and don’ts for each of the patterns are just good practices to follow. Once you become familiar and know how to use them, then feel free to do what you see fit as far and bending the way the patterns are supposed to be used.

In any case, I hope this helps you get a good idea of what delegation is, why it’s important to know, and how you can ultimately apply it to your own SproutCore application.

Happy coding!

-FC

Update (Feb 5, 2011): A bit of an oversight on my part with regard to theSC.DelegateSupport mixin. SC.View already mixes in SC.DelegateSupport, so you do not have to explicitly mix it into your view. Mixing inSC.DelegateSupport explicitly won’t do any harm, but it’s not necessary. Despite the oversight, the fact that SC.View does mix inSC.DelegateSupport gives you a pretty good impression of just how important the idea of delegation is in SproutCore :-).



13 commentsPost your own or leave a trackback: Trackback URL

  1. Luc 

    Mike,

    Great blog post. Thank you!

    I’m the one who had posted the sample code and your responses on the mailing list was of tremendous help. Thanks for expanding on the mailing-list replies with this blog post.

    For those who go look at the code, if you want to see the code before the refactoring after Mike’s advice, you will have to look back at some older commits since the latest code has been refactored to use the target-action pattern that Mike suggested.

  2. Makea 

    Posts like this are tremendous for anyone trying to write an app with sproutcore. Thanks!

  3. [...] tbh, haven’t had time to read this yet, but its by a member of SC core team so its a must read in my book :http://frozencanuck.wordpress.com/2011/02/03/sproutcore-making-use-of-delegation/ [...]

  4. Enjoyed reading this, thanks.

    Couple of typos in the third-last paragraph: “For the target-action, you’re not really focused on supplying the action will additional info.”
    WILL should be WITH

    “I mean, you can, but that’s not its soul purpose.” SOUL should be SOLE.

    I’m still not quite getting the Sproutcore take on Statecharts – they seem to be linked closely to views in the examples I’ve seen, but that’s quite different from Ian Horrocks’ take in ‘Constructing the User Interface with Statecharts’, where the statecharts describe the state the entire UI is in. But that could be an artifact of my poor understanding! So articles like this help me get to grip with it, thanks.

  5. frozencanuck 

    @Oliver:

    First, thanks for catching those typos. This is what happens when I end up being my own editor and writing late at night :-P. Changes have been made.

    Regarding statecharts, yes, statecharts are more fundamentally grounded in managing an application’s states. However, you are also free to add statecharts on a per view or object basis. That being said, the focus of this post was really about decoupling a view from application logic via delegation.

    Apologies if the code examples I used caused any confusion with respect to statecharts in SproutCore appearing to be closely linked with views since that’s not the case.

    BTW, Ian Horrocks’ “Constructing the User Interface with Statecharts” is a great book :-).

  6. [...] SproutCore: Making Use of Delegation « Keeping Sanity One Smelting Accident at a Time. Good advice on avoiding hard coupling between views and controllers in client side applications. Worth reading if you’re using Sproutcore or building iOS/Cocoa applications. This entry was posted in Links. Bookmark the permalink. ← SproutCore: Statecharts vs Controllers « Keeping Sanity One Smelting Accident at a Time LikeBe the first to like this post. [...]

  7. Gok6tm 

    Hi,

    is there a variant of this pattern for delegate the control of some property of the view to a “delegate-controller” ? just like CollectionRowDelegate do for a ListView, but just for a simple view ?

    thanks for all this explaination.

    ../

  8. frozencanuck 

    @Gok6tm: Hmm, unfortunately I’m not entirely sure I understand what you mean. If you could elaborate some more then I might be able to help you out.

  9. Hi again,

    I’ve been trying to decipher how best to set up a sequence of responders to catch events fired by several statecharts working in concert. Suppose I have a grid view, composed of cell views, and an editor view I use to edit the value of a selected cell view. Each of these views have the SC.StatechartManager mixin applied. I also have an application statechart.

    The cell views, grid view and editor view are all sources and targets of events, but I understand (from your posts) that directly coupling the view statecharts to other view statecharts or the app statechart is a bad idea (e.g. no ‘MyApp.statechart.sendEvent(‘anEvent’) anywhere in the view statecharts). Instead, the views should just launch their events into the ether, and the defaultResponder of the mainPane will direct them to the app statechart via the responder mechanism after setting the defaultResponder: property of the pane to point at ‘MyApp.statechart’.

    This isn’t really something I’m getting to grips with. I’m using the view statecharts to convert user events into application events, but I want them to respond to events from other views too, and I can’t work out how to channel the events into the same responder chain. So, for example, I’ve an editor view that I want to behave differently depending on the state the application statechart is in, but I don’t know how to launch events from the application statechart that the view can catch & react to. (These events don’t affect the model layer, just how the user edits values before they’re saved to the model layer.) How can I get the application statechart and view statecharts to use the same responder context?

    Meanwhile, over on github –https://github.com/sproutcore/sproutcore/issues/382 – in discussion with Erich Ocean, you’ve mentioned another way of hooking into the event stream:
    SC.RootResponder.responder.set(‘defaultResponder’, ‘MyApp.statechart’);

    What does this line of code do? Does it make all objects with the responder mixin share in the same responder context? You both seem to agree that it’s the way to go about things, but I haven’t seen it explained anywhere why it’s the case.

    Explicitly targeting the views with the application statechart’s events does work, but I’m littering my code with hard-coded property paths which I know might change in the future, and I’m much more attracted to the idea of an amorphous stream of events that the application and view statecharts participate in as peers.

    Hopefully that wasn’t too much of a ramble! Let me know if I can clarify my issue any more if needed. I’m off to study SC.RootResponder’s source and to scratch my head some more :)

    Oliver.

  10. frozencanuck 

    Hey Oliver,

    Thanks for the post. I’d be glad to help address your statechart questions :-).

    First, let me start by addressing your question regarding setting the application’s statechart as the root responder’s default responder.

    When you ask about the following statement:

    1 SC.RootResponder.responder.set(‘defaultResponder’, MyApp.statechart);

    what you are doing is telling the root responder to let your application’s statechart be the catch-all (the default) for any action sent to it if no other target responder could be found to handle the incoming action. That, in a nutshell, is pretty much it. But why does this matter, you may ask. By having your app’s statechart by the one and only default responder, it helps centralize your application’s state logic into one cohesive set of states. It also makes it easier maintaining your code since you set the default responder in only one place instead of multiple places such as being the default responder for every pane in your application.

    As a basic example, a view can send an action to the root responder by doing the following:

    01 handleSomeEvent: function(event) {
    02   var pane = this.get('pane'),
    03         responder = pane.get('responder'),
    04         target = this.get('target'),
    05         action = this.get('action');
    06  
    07    if (action) {
    08      responder.sendAction(action, target, this, pane);
    09    }
    10

    Every view in an SC app has a reference to the pane it is contained within, and in turn, every pane has a reference to the application’s root responder. And that is how actions actually get to the root responder which then get routed to the application’s statechart.

    Let’s move on to the next topic: How to channel events and update your views according to the application’s current set of states.

    First, you are correct that views that make up your application’s interface should not directly reference other views, unless, of course, it’s a parent view referencing its child views that it has direct control over — but let’s just stick with the general theme here for the sake of simplicity. Second, you are also correct that your views should not be directly coupled to your application’s statechart nor have hard-coded property paths since both lead to hard to maintain code and view’s that become more difficult to unit test and reuse. As you have noted, this seems to be putting you into an interesting dilemma as to just how you actually get one view to “talk” to another view. The answer to that comes in two parts based on the use of the application’s statechart and controllers.

    With respect to your app making use of a grid view, cells in the grid, and an edit view, what you want is to simply edit the item selected in the grid view. In addition, the type of object you want to edit will affect the edit view in what fields are displayed to the user. This means that your application’s statechart will have some kind of edit state and a non-edit state, which for this example we will refer to as the “ready” state. When you are in the ready state only the grid is visible, and when you enter the edit state then the edit view is visible.

    01 MyApp.statechart = SC.Statechart.create({
    02  
    03   initialState: 'readyState',
    04  
    05   readyState: SC.State.design({
    06     enterState: function() { /* show the grid */ },
    07     exitState: function() { /* hide the grid */ }
    08   }),
    09  
    10   editState: SC.State.design({
    11     enterState: function() { /* show the edit view */ },
    12     exitState: function() { /* hide the edit view */ }
    13   })
    14  
    15 });

    Great, with these two basic states we now need to connect the views with those states. Let’s look at the grid view. Really what you want is to keep track of what object has been selected when the user clicks on the cell in the grid. In addition, when the user, say, double clicks a cell that causes the edit view to appear. What we don’t want is to couple the grid’s cell views with the edit view. Rather, we simply make use of a controller to loosely couple them together based on what has been selected in the grid view, which can be done as such:

    01 MyApp.selectedObjectController = SC.ObjectController.create({
    02  
    03 });
    04  
    05 MyApp.mainPage = SC.Page.create({
    06  
    07   mainPane: SC.MainPane.design({
    08     childViews: 'gridView'.w(),
    09     gridView: SC.GridView.design({
    10       contentBinding: 'MyApp.objectsController.arrangedObjects',
    11       selectionBinding: 'MyApp.selectedObjectController.content'
    12       action: 'edit'
    13     })
    14   })
    15  
    16 });
    17  
    18 MyApp.editPage = SC.Page.create({
    19  
    20   editPane: SC.PanelPane.design({
    21     ...
    22   })
    23  
    24 });

    Above, we use the selectedObjectController to connect the selected item in the grid with the view’s in the edit pane. In order to switch states, we simply set the grid view’s action property to be “edit”. What will happen is that the grid view will will pass the action “edit” along to the root responder and in turn the root responder will pass along the action to the app’s statechart. Notice how the grid view’s target property was not set. This means that the root responder will simply ignore the supplied target and continue on to eventually reach its default responder — the app’s statechart. Let’s update our ready state so that is will handle the edit action:

    1 readyState: SC.State.design({
    2   enterState: function() { /* show the grid */ },
    3   exitState: function() { /* hide the grid */ },
    4   edit: function() {
    5     this.gotoState('editState');
    6   }
    7 })

    Pretty easy, right? No need to couple views directly together nor any need to mix in the SC.StatechartManager into either the grid view or any of its cells. Simply make use of what the grid view already provides.

    Alright, so what about the edit view itself? How does it get configured based on the type of object that is being edited? There are a few ways to look at this, but the methods revolve around the use of controllers. Depending on the degree of change you want to apply to the edit view, one approach is to have different edit views for each type of object you are editing. This means that when you enter the edit state, the state will check the type and set a controller as to what view is to be displayed. However, if the variance is not that great and you just want to make use of the same edit view with some minor alterations, you can again make use of a controller that the edit view’s field bind to determine what is visible.

    To keep things simple, let’s say you have two common types of objects that can be edited: a rectangle object and a circle object. For the rectangle object you change the height and width, and for the circle object you can change the radius. For both you can assign and ID and a color. Therefore when a rectangle object is selected the id, color, height and width fields are visible, and when a circle is selected, the id, color and radius fields are visible. So we have something like the following:

    01 MyApp.editPage = SC.Page.create({
    02  
    03   editPane: SC.PanelPane.design({
    04     childViews: 'idField colorField rectFields circleFields'.w(),
    05     idField: SC.TextFieldView.design({
    06       valueBinding: 'MyApp.selectedObjectController.id'
    07     }),
    08     colorField: SC.TextFieldView.design({
    09       valueBinding: 'MyApp.selectedObjectController.color'
    10     }),
    11     rectFields: SC.View.design({
    12       isVisibleBinding: 'MyApp.selectedObjectController.isEditingRect',
    13       childViews: 'heightField widthField'.w(),
    14       heightField: SC.TextFieldView.design({ .. }),
    15       widthField: SC.TextFieldView.design({ .. })
    16     }),
    17  
    18     circleFields: SC.View.design({
    19       isVisibleBinding: 'MyApp.selectedObjectController.isEditingCircle',
    20       childViews: 'radiusField'.w(),
    21       radiusField: SC.TextFieldView.design({ ... })
    22     })
    23   })
    24  
    25 });

    Both the ID and color fields are always visible within the edit view and are both bound to the selected object controller’s respective properties — the same selected object controller whose content property was assigned by the grid view using a binding with its selection property. The rect fields view and circle fields views are more interesting. The rect fields view’sisVisible property is bound to the controller’s isEditingRect property, and the circle fields view’s isVisible property is bound to the controllerisEditingCircle property. Neither the isEditingRect nor theisEditingCircle property belong to the controller’s backing content object. Instead the properties are computed properties on the controller itself:

    01 MyApp.selectedObjectController = SC.ObjectController.create({
    02  
    03   isEditingCircle: function() {
    04     var content = this.get('content');
    05     return SC.kindOf(content, MyApp.Circle);
    06   }.property('content'),
    07  
    08   isEditingRect: function() {
    09     var content = this.get('content');
    10     return SC.kindOf(content, MyApp.Rect);
    11   }.property('content')
    12  
    13 });

    What we are doing with the controller is providing a level of transformation by way of computed properties in order to determine what fields are to be visible within the edit view. And notice how we did not have to do anything to our edit state since the controller is doing the mediating work for the state. This provides a nice, clean separation of concerns. The selected object controller acts as a mediator between views and the states within the app’s statechart act as a coordinator to react to an action raised by views.

    Granted the code examples I have provided are somewhat trivialized, but the essence here is really to demonstrate how minimal the work is to connect views together and with the app’s statechart. SproutCore does most of the plumbing for you instead of you having to do a lot of heavy lifting by creating custom views that have their own statechart and having to manually call the root responder’s sendAction yourself.

    Please let me know if you require any additional assistance as I would be happy to help. I want to make sure that for anyone who makes an application with SproutCore, they can do so with the least effort possible :-).

  11. Mike,

    Thanks a million for taking the time to write such an in-depth response! If I had to summarise, would it be reasonable to say that views communicate with other views and controllers via bindings, while the main app statechart communicates with the UI via custom events emitted by those views/controllers?

    Your example is very helpful too. It’s based on what seems to be a tree of views, where the ‘leaf’ view is either a rectangle editor or a circle editor. I’m getting stuck on something requires a graph of connections, rather than a tree; the edit operations available depend on the current edit ‘mode’ which is determined by the application statechart. The user can change modes mid-edit, which affects how the view should interpret UI events. And because the app statechart manages this mode, not the collection or item views, the app statechart is also sending events to the views so that they can give visual feedback to the user. This is pushing me to having the statechart refer directly to views to allow for this kind of interaction.

    But given that a statechart is likely to be app-specific, is giving it a reference (via an SC.outlet) to the view in the application hierarchy reasonable, as the statechart is a one-off piece of code?

    Thanks again for all the advice!

    Oliver.

  12. frozencanuck 

    @Oliver:

    So a couple of things….

    When creating a SproutCore application, what you want to strive for is to minimize the coupling among objects and maximize the cohesiveness of logic in each object. One way we reach these goals is by introducing the idea of the mediating controller so that all views that need to work together do so through a small set of objects.

    Here’s a basic example: Take the idea of an edit component that contains a text field, a slider view, and a button. Instead of using a mediating controller we instead must get the views to talk directly to one another. The text field takes a positive integer between 1 and 10. When a value is entered into the text field, the text field in turn must update the slider view’s value property so that the slider will reflect the same number and the knob on the slider moves to the correct position. The same goes for when the slider knob is moved and the slider then must directly update the text field to have the same corresponding value. Finally there is the button, and when clicked closes the edit component, but it is only enabled when the text field’s value and slider view’s value are valid, therefore the text field view and the slider view must inform the button view that they are indeed valid.

    Taking the above approach, the text field view is directly coupled with the slider and button view, and the slider view is directly coupled to the text field and button view. We have mostly come to a many-to-many dependency. If we were to add another field to the edit view, we’d have to go back to the other fields and update most of them again to have direct knowledge of the new fields so that they can communicate with each other, which just leads to harder to maintain code due to the high-level of coupling. With regard to cohesion of each object, it’s lowered since each view had to be subclassed in some way to have more knowledge of things outside of themselves in order to operate versus the core logic each view has that allows them to just visualize themselves and handle events that directly affect them. If there is one word to sum all this up is would be “icky”!

    Alright, now let’s take a step back from our icky first example and ask ourselves what do we really want. Well, first we’d like to avoid the nasty many-to-many coupling going on among all the views. Second, it’d be great if we could somehow extract all that code each view got extended with to update other views and instead centralize it all into some other object to do the work so that the views can go back to doing what they each do best and keep their code as cohesive as possible.

    Regarding the first thing to tackle, coupling, a more preferable model to work with is a one-to-many coupling. Each view only needs to be coupled to one thing to both inform of changes and to receive updates. And that’s what a controller does for us. However, in a tradition MVC framework, the standard stock controller turns into a kitchen sink of having to be responsible for both propagating updates among views connected to it and reacting to action sent by views, which in turns means having to also handle the actions based on what the current state (or states) of the application is. In SproutCore, those two responsibilities are split up so that mediating controllers are responsible for only propagating and transforming data among views and the statechart’s states being responsible for reacting to actions raised by views and in turn updating the corresponding mediating controller that the views are connected with.

    Now as for cohesion, we have used both the mediating controller and the statechart to move all the logic that the views used to have in order react to actions, keep track of the application’s state and update other views’ properties. Therefore our views are no longer subclassed and only contain the logic that is directly responsible for visualizing data given to it and reacting to events that each view cares about.

    Note that with regard to views and a statechart’s states, the states and views both go through the common mediating controller. Again, we do this so that we provide a low-level of coupling, and, in addition, loosen the direct knowledge between views and states. In effect, the mediating controller becomes a kind of facade or interface for the states to communicate with the views. This is important because the UI and application states are now more free to vary independently and make use of a more stable mediating controller that is independent of what makes up the UI and what state the app is currently in. The mediating controllers are basically naive to the world around them, as they should be, only to act like an interface between two related domains: the UI and the application state.

    01 MyApp.editObjectController = SC.ObjectController.create({
    02  
    03 });
    04  
    05 MyApp.editPage = SC.Page.create({
    06  
    07   editView: SC.View.design({
    08     childView: 'textFieldView sliderView buttonView'.w(),
    09  
    10     textFieldView: SC.TextFieldView.design({
    11       valueBinding: 'MyApp.editObjectController.value'
    12     }),
    13  
    14     sliderView: SC.SliderView.design({
    15       valueBinding: 'MyApp.editObjectController.value'
    16     }),
    17  
    18     buttonView: SC.ButtonView.design({
    19       isEnabledBinding: SC.Binding.transform(function(value) {
    20         return !SC.none(value);
    21       }).oneWay('MyApp.editObjectController.value'),
    22       action: 'finishingEditing'
    23     })
    24   })
    25  
    26 });
    27  
    28 MyApp.statechart = SC.Statechart.create({
    29  
    30   ...
    31  
    32   editState: SC.State.design({
    33  
    34     content: null,
    35  
    36     contentBinding: 'MyApp.editObjectController.content',
    37  
    38     enterState: function(context) {
    39       this.set('content', context.content);
    40     },
    41  
    42     exitState: function() {
    43       this.set('content', null);
    44     },
    45  
    46     finishingEditing: function() {
    47       this.gotoState('readyState');
    48     }
    49  
    50   })
    51  
    52 });

    So there you have it: With coupling minimized and cohesion maximized we now have code that is far more maintainable, easier to read, and best of all makes it way easier to add new functionality with minimum impact to the rest of the application.

    You also mentioned bindings. I think it’s fair to say that bindings are a tool to allow communication among objects in SproutCore. In its concrete form, bindings simply connect one object’s property with another object’s property to facilitate the exchange of data automatically. Without bindings, objects would then need to be explicitly setup via some kind of observer pattern implementation in order to communicate, and think of the all the messy plumbing and maintenance that will cause — ick, again! Bindings under the hood do make use of observer logic, but SproutCore takes care of all the details to connect them all up.

    Finally, the last thing you mention is the different modes one of your views can be in and how that mode affects the view’s behavior with respect to how it handles incoming user events. You also noted how you have states in your statechart that directly connects to the view to change the view’s mode. What you have there is one way in a state can modify a view behavior by either directly invoking a method on the view or directly changing a property on the view. However, as we have been discussing, we want to loosen the coupling between views and states as much as possible. Therefore, we again make use of a mediating controller to act as an interface between the view and states. This means that when you want to switch modes based on some fired actions, the states instead tell the mediating controller what edit mode it is in. In turn, the view has an mode property that is bound to the mediating controller mode property. When the mode is changed by the state, the view will pick up the change and update its internal state accordingly to react to incoming user events. A simple example:

    01 MyApp.FOO_MODE = 0;
    02 MyApp.BAR_MODE = 1;
    03  
    04 MyApp.MyCustomView = SC.View.extend({
    05  
    06   mode: MyApp.FOO_MODE,
    07  
    08   fooTarget: null,
    09   fooAction: null,
    10  
    11   barTarget: null,
    12   barAction: null
    13  
    14   mouseDown: function() {
    15     var mode = this.get('mode'),
    16           target, action;
    17     if (mode === MyApp.FOO_MODE) {
    18        target = this.get('fooTarget');
    19        action = this.get('fooAction');
    20     } else if (mode === MyApp.BAR_MODE) {
    21        target = this.get('barTarget');
    22        action = this.get('barAction');
    23     }
    24  
    25     // ... send target action to the root responder
    26   }
    27  
    28 });
    29  
    30 MyApp.someController = SC.Object.create({
    31   mode: null
    32 });
    33  
    34 MyApp.mainPage = SC.Page.create({
    35  
    36   mainPane: SC.MainPane.design({
    37     childViews: 'customView'.w(),
    38     customView: MyApp.MyCustomView.design({
    39       mode: 'MyApp.someController.mode',
    40       fooAction: 'doFoo',
    41       barAction: 'doBar'
    42     })
    43   })
    44  
    45 });
    46  
    47 MyApp.statechart = SC.Statechart.create({
    48  
    49   ...
    50  
    51   fooState: SC.State.design({
    52     mode: null,
    53     modeBinding: 'MyApp.someController.mode'
    54     enterState: function() {
    55       this.set('mode', MyApp.FOO_MODE);
    56     },
    57  
    58     doFoo: function() { ... }
    59   }),
    60  
    61   barState: SC.State.design({
    62     mode: null,
    63     modeBinding: 'MyApp.someController.mode'
    64     enterState: function() {
    65       this.set('mode', MyApp.BAR_MODE);
    66     },
    67  
    68     doBar: function() { ... }
    69   })
    70  
    71  
    72 });

    Remember that it’s fine for a view to have its own internal set of states independent of the application’s states. A view’s internal states are specific to how the view goes about visualizing its data and reacting to user events. The application’s states dictate how the application reacts to incoming application actions, how the application’s states changes, and in turn affect its various mediating controllers.

    Also note how by exposing properties on views to modify how they operate, we are able to then maximize the usage of bindings and thereby loosen the coupling among views and states.

    Again, hope all this information helps you out with your SproutCore development efforts.

    -Mike

  13. Hi Mike,

    Thanks a million again for such detailed responses. The code examples in particular are very useful; I hadn’t considered setting bindings on individual states before (mentally I’d assumed bindings were all set up in mediating controllers and only referred to in statecharts, let alone in individual states, which in hindsight is an odd restriction).

    This has been really helpful for me, thank you!

    Oliver.

Leave a Reply


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值