Introducing MVVM

MVVM can be seen as an augmentation of MVC. Introducing the view model concept, it recognizes that not every view concerned with a dataset will be using this data in the same way. It adds a layer of indirection between a view and a model, called a view model, to solve this issue. It also keeps separation of concerns to the fore; why should the model, which is dealing with our data be concerned about anything to do with our view, which is dealing with presentation?

Introducing MVVM

A typical representation of MVVM

How does Ext JS use MVVM?

With Ext JS 5, MVVM is wholeheartedly embraced. The sample application structure that Sencha Cmd generates will provide a ViewModel class alongside the View class. This has tight integration into the View class via new configuration options, which make it a first-class citizen when trying to solve the common problems that arise in a large MVC application, as we discussed earlier.

In addition, a ViewController class is created to encapsulate the logic you'd normally put in a view or in a standard controller. It removes the question of where to put event handlers that are concerned with things internal to the view, rather than event handlers that will be passing off events to other parts of your application.

Getting our MVVM started

We started by generating an Ext JS 5 application template using Sencha Cmd and used it as a basis to build an implementation of our example album list application. The default Ext JS 5 template uses an MVVM implementation as follows:

Getting our MVVM started

Our example app ported to Ext JS 5's MVVM architecture

The most immediate difference you'll notice is that we've lost our controllers directory and there's a lot more going on in the views directory. Let's break it down:

// model/Album.js
Ext.define('MvvmEx1v5.model.Album', {
    extend: 'Ext.data.Model',
    
    fields: [
        { name: 'name', type: 'string' },
        { name: 'artist', type: 'string' }
    ]
});

The album model is identical to the previous example, although note that we've changed the application name to MvvmEx1v5. The store is only very slightly different:

// store/Albums.js
Ext.define('MvvmEx1v5.store.Albums', {
    extend: 'Ext.data.JsonStore',

    model: 'MvvmEx1v5.model.Album',

    data: [
        { name: 'In Rainbows', artist: 'Radiohead' },
        { name: 'Swim', artist: 'Caribou' }
    ]
});

We've added the alias configuration option so that we can refer to the store later using the albums shorthand. Now, let's take a look at the views directory:

// view/album/Album.js
Ext.define('MvvmEx1v5.view.album.Album', {
    extend: 'Ext.container.Container',
    xtype: 'app-album',
    requires: ['Ext.grid.Panel'],
    controller: 'album',
    layout: 'hbox',
    defaults: {
        width: 250,
        margin: 20
    },
    items: [
        {
            xtype: 'grid',
            reference: 'list',
            viewModel: 'album',
            bind: '{albums}',
            forceFit: true,
            frame: true,
            margin: '20 10 20 20',
            columns: [
                { text: 'Album', dataIndex: 'name' },
                { text: 'Artist', dataIndex: 'artist' }
            ],
            bbar: [
                '->',
                { xtype: 'button', text: 'Show Artist Summary', handler: 'onShowSummary' },
                '->'
            ],
            listeners: {
                rowdblclick: 'onAlbumDblClick'
            }
        },
        { xtype: 'container', margin: '20 10', reference: 'detail', width: 150, html: 'Double-click an Album to select' }
    ]
});

We've combined the previous app-list and app-detail views into a single app-albums view, and whereas before we had the logic to build the album summary in the view, we now define only the event handler and the logic goes elsewhere. This view is now 100 percent presentation and defers all the complicated stuff to other classes.

Note that we have a controller configuration option that defines the view controller to use for this view class. Our grid component has several interesting configuration options too:

  • reference: We use this later to get hold of this component from the view controller.
  • viewModel: This is the alias of the view model this component will use.
  • bind: This defines how the view talks to the view model. We're using the simplest binding (the grid's default bindProperty is store), so here we're essentially just setting the store config to 'albums'.

Now, let's move on to our album view model:

// view/album/AlbumModel.js
Ext.define('MvvmEx1v5.view.album.AlbumModel', {
    extend: 'Ext.app.ViewModel',
    alias: 'viewmodel.album',

    requires: [
        'MvcEx1.store.Albums'
        'Ext.Msg'
    ],
    stores: {
        albums: {
            type: 'albums'
        }
    },

    buildSummary: function() {
        return this.getStore('albums').collect('name').join(', '),
    }
});

Also, here we have one of these places that now contain this logic. A view model takes data from a model (or stores) and presents it in a way suitable for its matching view. In this case, we take the data from the 'albums' store (referenced in the type configuration option by the albums alias we mentioned earlier). It provides a buildSummary method that transforms the stored data into a string ready to be used in the UI as follows:

// view/album/AlbumController.js
Ext.define('MvvmEx1v5.view.album.AlbumController', {
    extend: 'Ext.app.ViewController',
    alias: 'controller.album',

    onShowSummary: function() {
        var summary = this.lookupReference('list').getViewModel().buildSummary();

        Ext.Msg.alert('Artists', summary);
    },

    onAlbumDblClick: function(list, record) {
        var html = Ext.String.format('{0} by {1}', record.get('name'), record.get('artist'));
        this.lookupReference('detail').getEl().setHtml(html);
    }
});

Finally, we have our view controller, which is where any logic that manages our view should go. Event handlers defined in the view controller will automatically be available to the matching view.

Are we better off?

Yes, we're better off because we're more organized. We know where all of our application bits go, and although we've got a lot more files than a straightforward one-class Ext JS application, we'll always know where to look to change the configuration of a view, where to find our logic for the albums, or where to shape the information we pull from a store.

One important point about this example is that we forego the overarching controller from the first example in favor of a view controller. Here, this makes sense; we want this view controller to be concerned only with albums, not with other parts of the application. However, a higher-level controller is still a valid piece of the Ext JS MVVM architecture and can be reintroduced in situations that require a way to coordinate an application at a higher level than a view controller.

A brief interlude regarding stores

Throughout this entire chapter, we've talked a lot about models, but never specifically about stores despite using them in our example applications. Why isn't it "SVC" or "SVVM"?

In Ext JS, a store is a specific class, which brings specific functionality and is tightly bound into your application. However, in a simple MVC or MVVM implementation, the "store" could just be an array of models, rather than being a separate architectural feature. So, a store is really just a way of collecting models together and Ext JS happens to be the place where we can do lots of extra things (such as sorting, filtering, and batching).

Inter-communication

We've shown how to create a simple application that uses several moving parts to create a logical unit. Thanks to the MVVM pattern, we have a methodology that allows the parts of this unit to communicate without having to be explicitly tied to the implementation details of other parts.

When we extend our application, we'll have several of these logical units, perhaps, an artist section in addition to an album section. Now, these have to communicate with each other in turn. This represents one of the main problems in software architecture: how to allow albums and artists to talk to each other without contaminating either component with details of the other. It's a problem that scales in direct proportion to the size and complexity of an application.

The main event

One approach to this problem is to allow your application parts to fire off custom events, each containing a payload that can be consumed by any other part of your application that might be interested in them.

In fact, we see this in action all the time in web development. Event handlers are an integral part of JavaScript programming as we bind functions to the events thrown by user interface elements (such as buttons) or to browser events (such as window.onload). We've touched on this in our example code already; our view fired off a rowdblclick event that was handled by our view controller.

In complex applications, developers will often implement a feature called an event bus, a way of taking the events that application components fire off and transporting them to various subscribers. Since Ext JS 4.2, event domains have allowed developers to incorporate a similar feature into their code base.

Event domains

Event domains allow controllers to react to events from various different sources in your application. The default sources are:

  • Components: These are events fired from components. This is essentially the functionality that Ext.app.Controller.control() provides by handling events from classes that extend Ext.Component and bind them to event listeners.
  • Global: These are events fired from a single global source and used to bind arbitrary application-wide events.
  • Controller: These are events fired from other controllers.
  • Store: These are events fired from a store.
  • Direct: These are events fired from classes that extend Ext.direct.Provider. This is only used if you require Ext.direct.Manager in your application.

Some event domains allow you to filter the received events by a selector (usually the ID associated with the source), but in the Component case, you can use a full-blown Ext.Component query. This allows you to have finer-grained control on how to subscribe to events.

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

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