Sencha Inc. | HTML5 Apps

Blog

Using ViewControllers in Ext JS 5

May 22, 2014 | Don Griffin

Introduction

Ext JS 5 delivers some exciting improvements for you to use in your application architecture. We have added support for ViewModels and MVVM as well as ViewControllers to enhance MVC applications. Best of all, these choices are not mutually exclusive, so you can introduce these features incrementally or even blend them.

Recapping Controllers

In Ext JS 4, a Controller is a class derived from Ext.app.Controller. These Controllers use CSS-like selectors (called “Component Queries”) to match components and respond to their events. They also use “refs” to select and retrieve component instances.

These controllers are created at application launch and remain present for the life of the application. During its lifetime, views of interest to a controller will come and go. There may even be multiple instances of views that the controller manages.

Challenges

For large applications, these techniques can create certain challenges.

In such environments, views and controllers may be authored by multiple development teams and integrated into the final application. Ensuring that controllers only react to their intended views can be difficult. Further, it is common for developers to want to limit the number of controllers created at application launch. While lazily creating controllers is possible with some effort, they can’t be destroyed, so they remain even if they are no longer needed.

ViewControllers

While Ext JS 5 is backwards compatible with current controllers, it introduces a new type of controller designed to handle these challenges: Ext.app.ViewController. ViewController does this in the following ways:

  • Simplifies the connection to views using “listeners” and “reference” configs.
  • Leverages the life cycle of views to automatically manage their associated ViewController.
  • Reduces complexity in the ViewController based on a one-to-one relationship with the managed view.
  • Provides encapsulation to make nesting views reliable.
  • Retains the ability to select components and listen to their events at any level below the associated view.

Listeners

The listeners config is not new, but it has gained several new abilities in Ext JS 5. A more complete discussion of the new listeners features can be found in an upcoming article -- “Declarative Listeners in Ext JS 5”. For the purposes of ViewControllers, we can look at just two examples. The first is a basic use of a listeners config on a child item in a view:

 
Ext.define('MyApp.view.foo.Foo', {
    extend: 'Ext.panel.Panel',
    xtype: 'foo',
    controller: 'foo',
 
    items: [{
        xtype: 'textfield',
        fieldLabel: 'Bar',
        listeners: {
            change: 'onBarChange'  // no scope given here
        }
    }]
});
 
Ext.define('MyApp.view.foo.FooController', {
    extend: 'Ext.app.ViewController',
    alias: 'controller.foo',
 
    onBarChange: function (barTextField) {
        // called by 'change' event
    }
});
 

The above use of listeners shows a named event handler (“onBarChange”) with no specified “scope”. Internally, the event system resolves the default scope for the Bar textfield to its owning ViewController.

Historically, the listeners config was reserved for use by a component’s creator, so how would a view listen to its own events, or perhaps those fired by its base class? The answer is that we need to use an explicit scope:

 
Ext.define('MyApp.view.foo.Foo', {
    extend: 'Ext.panel.Panel',
    xtype: 'foo',
    controller: 'foo',
 
    listeners: {
        collapse: 'onCollapse',
        scope: 'controller'
    },
 
    items: [{
        ...
    }]
});
 

The above example leverages two new features in Ext JS 5: named scopes and declarative listeners. We’ll focus on the named scope here. There are two valid values for named scope: “this” and “controller”. When writing MVC applications, we almost always use “controller” which has the obvious result of looking on that view’s ViewController (not the ViewController of the view that created the instance).

Since a view is a type of Ext.Component, we have assigned this view an “xtype” which enables other views to create an instance of our view in the same way this view created its textfield. To see how this comes together, consider a view that uses this one. For example:

 
Ext.define('MyApp.view.bar.Bar', {
    extend: 'Ext.panel.Panel',
    xtype: 'bar',
    controller: 'bar',
 
    items: [{
        xtype: 'foo',
        listeners: {
            collapse: 'onCollapse'
        }
    }]
});
 

In this case, the Bar view is creating an instance of the Foo view as one of its items. Further, it is listening to the collapse event just like the Foo view. In previous versions of Ext JS and Sencha Touch, these declarations would conflict. With Ext JS 5, however, this will now resolve as one would hope. The listeners declared by the Foo view will fire on Foo’s ViewController while the listeners declared in the Bar view will fire on Bar’s ViewController.

Reference

One of the most common loose ends when writing controller logic is getting ahold of the necessary components to complete a particular action. Something as simple as:

 
Ext.define('MyApp.view.foo.Foo', {
    extend: 'Ext.panel.Panel',
    xtype: 'foo',
    controller: 'foo',
 
    tbar: [{
        xtype: 'button',
        text: 'Add',
        handler: 'onAdd'
    }],
 
    items: [{
        xtype: 'grid',
        ...
    }]
});
 
Ext.define('MyApp.view.foo.FooController', {
    extend: 'Ext.app.ViewController',
    alias: 'controller.foo',
 
    onAdd: function () {
        // ... get the grid and add a record ...
    }
});
 

But how should one acquire the grid component? In Ext JS 4, you could use the “refs” config or some other way to look up the component. All techniques require you to place some recognizable property on the grid to allow it to be uniquely identified. Older techniques used the “id” config (and Ext.getCmp) or the “itemId” config (using “refs” or some component query method). The advantage of “id” is fast lookup, but since these identifiers must be unique across the entire application and the DOM as well, this is not often desirable. Using “itemId” and some form of query is more flexible, but it’s necessary to perform a search to find the desired component.

With the new reference config in Ext JS 5, we simply add the “reference” to the grid and use “lookupReference” to get at it:

 
Ext.define('MyApp.view.foo.Foo', {
    extend: 'Ext.panel.Panel',
    xtype: 'foo',
    controller: 'foo',
 
    tbar: [{
        xtype: 'button',
        text: 'Add',
        handler: 'onAdd'
    }],
 
    items: [{
        xtype: 'grid',
        reference: 'fooGrid'
        ...
    }]
});
 
Ext.define('MyApp.view.foo.FooController', {
    extend: 'Ext.app.ViewController',
    alias: 'controller.foo',
 
    onAdd: function () {
        var grid = this.lookupReference('fooGrid');
    }
});
 

This is similar to assigning an itemId of “fooGrid” and doing: “this.down(‘#fooGrid’)”. The difference under the hood, however, is quite significant. First, a reference config instructs the component to register itself with its owning view (identified by the presence of a ViewController in this case). Second, the lookupReference method simply consults the cache to see if references need to be refreshed (say due to add or remove on a container). If all is well, it just returns the reference from the cache. Or, in pseudo-code:

 
lookupReference: (reference) {
    var cache = this.references;
    if (!cache) {
        Ext.fixReferences(); // fix all references
        cache = this.references; // now the cache is valid
    }
    return cache[reference];
}
 

In other words, there is no search and the linkages damaged by adding or removing items from containers are fixed in one go, when they are needed. As we will see below, this approach has benefits other than efficiency.

Encapsulation

The use of selectors in the Ext JS 4 MVC implementation was very flexible but at the same time had certain risks. The fact that these selectors “see” everything at all levels of the component hierarchy is both powerful and prone to mistakes. For example, a controller could be working 100% when it runs in isolation but then fails as soon as other views are introduced because its selectors have undesired matches with the new view.

These problems can be managed by following certain practices, but when using listeners and references with a ViewController these problems are simply not possible. This is because the listeners and reference configs connect only with their owning ViewController. Views are free to chose any reference value that is unique within that view knowing that these names will not be exposed to the view’s creator.

Likewise, listeners are resolved on their owning ViewController and cannot be accidentally dispatched to event handlers in other controllers with errant selectors. While listeners are often preferable to selectors, the two mechanisms play well together for those situations where a selector-based approach is desired.

To complete this model, views need to fire events that can be consumed by their owning view’s ViewController. There is a helper method in ViewController for this purpose: fireViewEvent. For example:

 
Ext.define('MyApp.view.foo.FooController', {
    extend: 'Ext.app.ViewController',
    alias: 'controller.foo',
 
    onAdd: function () {
        var record = new MyApp.model.Thing();
        var grid = this.lookupReference('fooGrid');
        grid.store.add(record);
 
        this.fireViewEvent('addrecord', this, record);
    }
});
 

This enables the standard form of listener for the creator of this view:

 
Ext.define('MyApp.view.bar.Bar', {
    extend: 'Ext.panel.Panel',
    xtype: 'bar',
    controller: 'bar',
 
    items: [{
        xtype: 'foo',
        listeners: {
            collapse: 'onCollapse',
            addrecord: 'onAddRecord'
        }
    }]
});
 

Listeners and Event Domains

In Ext JS 4.2, the MVC event dispatcher was generalized with the introduction of event domains. These event domains intercepted events as they were fired and dispatched them to controllers controlled by selector matching. The “component” event domain had full component query selectors while the other domains had more limited selectors.

With Ext JS 5, each ViewController creates an instance of a new type of event domain called the “view” event domain. This event domain allows ViewControllers to use the standard “listen” and “control” methods while limiting their scope implicitly to their view. It also adds a special selector to match the view itself:

 
Ext.define('MyApp.view.foo.FooController', {
    extend: 'Ext.app.ViewController',
    alias: 'controller.foo',
 
    control: {
        '#': {  // matches the view itself
            collapse: 'onCollapse'
        },
        button: {
            click: 'onAnyButtonClick'
        }
    }
});
 

The key difference between listeners and selectors can be seen above. The “button” selector will match any button in this view or any child view, irrespective of depth, even if that button belongs to a great-grandchild view. In other words, selector-based handlers do not respect encapsulation boundaries. This behavior is consistent with previous Ext.app.Controller behavior and can be a useful technique in limited situations.

Lastly, these event domains respect nesting and effectively “bubble” an event up the view hierarchy. That is to say, when an event fires, it’s first delivered to any standard listeners. Then, it’s delivered to its owning ViewController, followed by its parent ViewController (if any) on up the hierarchy. Eventually, the event is delivered to the standard “component” event domain to be handled by Ext.app.Controller-derived controllers.

Lifecycle

A common technique with large applications is to dynamically create controllers when they are first needed. This could help reduce the load time of the application and help runtime performance as well by not activating all potential controllers. The limitation to this in previous versions was that, once created, these controllers would remain active in the application. It was not possible to destroy them and release their resources. Likewise, this did not change the reality that a controller could have any number of associated views (including none).

The ViewController, however, is created very early in the component’s lifecycle and is bound to that view for its entire lifetime. When that view is destroyed, the ViewController is likewise destroyed. This means that the ViewController is no longer forced to manage states where there is no view or many views.

This one-to-one relationship means reference tracking is simplified and no longer prone to leaking destroyed components. A ViewController can implement any of these methods to perform tasks at key points in its lifecycle:

  • beforeInit — This method can be overridden in order to operate on the view prior to its initComponent method being called. This method is called immediately after the controller is created which occurs during initConfig called from the Component constructor.
  • init — Called shortly after initComponent has been called on the view. This is the typical time to perform initialization for the controller now that the view is initialized.
  • initViewModel — Called when the view’s ViewModel is created (if one is defined).
  • destroy — Cleanup any resources (be sure to callParent).

Conclusion

We think ViewControllers will greatly streamline your MVC applications. They also work very well with ViewModels, so you can combine these approaches and their respective strengths. We’re excited about the upcoming release and seeing these improvements at work in your applications.

Written by Don Griffin
Don Griffin is a member of the Ext JS core team. He was an Ext JS user for 2 years before joining Sencha and has over 20 years of software engineering experience on a broad range of platforms. His experience includes designing web application front-ends and back-ends, native GUI applications, network protocols and device drivers. Don’s passion is to build world class products that people love to use.

Share this post:
Leave a reply

There are 19 responses. Add yours.

Mitchell Simoens Sencha Employee

4 months ago

Do note, the scope config in the listener resolves to “this” or “controller”, both are strings even “this”! If you use this object, it will be the window but if you use “this” as a string, Ext will resolve to the class.

Russ Ferri

4 months ago

Is there a way to specify a “reference” identifier in a selector via the control config?

Mitchell Simoens Sencha Employee

4 months ago

The listeners config should be used as a primary means of listening to events. The control config in a ViewController should only be used when specifying multiple listeners isn’t the best idea. For example, if a form has 10 fields and you want to listen to the specialkey event to submit the form, using control may be a better fit. The control selectors specified use ComponentQuery.

Doug

4 months ago

Helpful, but the traditional foo bar example is horrible. 

Why not use something meaningful that makes the context of the example clear?  it would have been clearer to use consistent names like “ParentView” and “ChildView” or something equally clear in a general domain that anyone can relate to.

Russ Ferri

4 months ago

@Mitchell

Understood, but I’m not too wild about the additional coupling introduced between the view and the view-controller by making the view responsible for registering the view-controller’s listeners. I’m trying to get an idea of what can be done within the controller itself instead.

Allowing the view-controller to automatically attach and detach event handlers to reference items as they are added or removed from the view feels to me like a natural extension of the reference concept.

Mitchell Simoens Sencha Employee

4 months ago

@Russ Ferri, Understandable but that’s not how the reference config is meant to be used. For that I would use itemId and then use it in your ComponentQuery selector (e.g. ‘#foo’)

Les

4 months ago

I have a Tree panel, Grid panel and a regular Panel that contain the same tools.

How do I configure my 3 different ViewControllers to listen to tool click events in a way that avoids code duplication?

Is it possible to add the ‘control’ config dynamically?

Russ Ferri

4 months ago

@Mitchell

I guess? Still, I feel there’s some cognitive dissonance in saying that itemId’s can be used to identify components when attaching listeners but reference ids cannot despite them both serving the purpose of acting as an identifier for a component that will be contained by the view. Due to their specificity, using references would be much more powerful, however using an itemId selector will clearly work in most cases despite the additional cost of running a ComponentQuery as events are processed by the EventDomain.

Honestly, I’m not really in the habit of allowing controllers to have much knowledge of a view’s internals anyway. I usually prefer to have the view relay important events fired by child items for consumption by the controller so I will probably continue to do that.

Les

4 months ago

If I create a base ViewController class containing the ‘control’ config… can the subclass also contain the ‘control’ config?  Will these config’s accumulate?

Don Griffin Sencha Employee

4 months ago

@Russ

The additional linkages between the view and a ViewController make the relationship between the two much more obvious - but in truth there is an equal amount of coupling between a View and a Controller today ... it is just hidden in the selectors. Having had to review a fair bit of selector-based MVC code, there is no comparison really. It is much easier to understand what is going on using listeners and references when looking at unfamiliar code.

I think the fact this is just different is perhaps what will take the most getting used to. Using the new features starts to feel very natural and much simpler quite quickly. Of course, you can continue to use a selector-based approach and those are even scoped now in a ViewController,.. but I would recommend giving listeners a good try first. smile

Russ Ferri

4 months ago

@Don

It’s true that there still exists coupling via selectors, but all of the code that relies upon the coupling lives within the controller class today, and not mixed between both the ViewController and the View. In any case, as I mentioned above, we are already careful not to let coupling via selectors to slip into the controller in the most cases.

As far as reviewing code goes, personally I’d much prefer to see all of the events that the controller has bound in a single place than having to scan views for listener configs, but YMMV.

Don Griffin Sencha Employee

4 months ago

@Les

There is a “control” and “listen” method that back these configs so you can add things dynamically by calling them. The setControl and setListen methods exist as well because “control” and “listen” are configs based on the config system, but calling these is not really intended.

As config-system configs, both “control” and “listen” will use Ext.merge as you derive so these objects will be combined down the hierarchy… but this is a deep merge and therefore may not be spot on in all cases. With the “listeners” config we take a different approach that to combining base/derived listeners and avoid this merge for that reason.

Don Griffin Sencha Employee

4 months ago

@Russ

Thanks for all your input - I think these questions will be very common to those with a lot of experience using the current MVC package.

I think it helps to articulate what the current mental process is when trying to answer three key questions: “who is calling this method?”, “what does this component do?” and “who is using this component?”. These were the questions we kept coming back to when designing View Controllers because having good answers to these is critical for maintainable code.

With the selector-based approach, the process for answering #1 is: search in the controller for that method name in listen / control statements. For each selector, read the view to find all matching components (if any). Of course, the selectors are applied to all components not just the view one might have in mind, so a strategy must be in place to limit the matches accordingly.

Going the other way now for #2: look at interesting properties for the component such as its xtype, its inherited xtype’s, itemId or other identifying properties. Now go look at the controller’s listen / control block and see which selectors will match that component (if any). Similarly to #1, this component is presented to all controllers so a match may occur in places you don’t expect.

Conventions are essential to manage these challenges so, agreed: there will be mileage variance here smile We stress such conventions when training, but using reference / listeners all of these are answered with one level of indirection. While all the details remain in the View Controller, the relationships are always obvious on casual inspection.

Alex

4 months ago

In the latest (only) beta release, the ViewController function that looks up a reference is called “getReference”, not “lookupReference”.  What will that function be called in the GA release?

Mitchell Simoens Sencha Employee

4 months ago

@Alex, In the public beta it was getReference. In the next release and going forward it is lookupReference.

Les

4 months ago

lookupReference is a long method name.  Can you add a shorter alias?

I’d think this method will be used a lot.

Dennis

4 months ago

I can see the examples in the previous blog post heavily use the “old” way of doing things. Could it be updated, to be the prime example of how things should be done?

Tom

4 months ago

Hi, the Japanese translation of this blog article is here: http://www.xenophy.com/sencha-blog/11113
Link to the Japan Sencha User Group: http://www.meetup.com/Japan-Sencha-User-Group/

Mitchell Simoens Sencha Employee

4 months ago

@Dennis, The examples in the next release will be updated to use the new API names. There will be examples that will not use MVVM (or MVC) some will. The Ticket App was specifically created to show best practices for MVVM and data binding.

Comments are Gravatar enabled. Your email address will not be shown.

Commenting is not available in this channel entry.