Writing a Sencha Touch MVC Application

In this article we will explore one of the approaches we can take to create a Sencha Touch application using the MVC pattern. I will base the tutorial on the Notes Application we created in the Writing a Sencha Touch Application series. Our goal is to build a new version of the Notes Application using Sencha Touch MVC.

The Notes Application allows its users to take notes and store them on the device running the app. The features we built into the app, which we will reproduce in the MVC version, are the following:

  • Ability to create notes
  • Ability to edit existing notes
  • Ability to delete notes
  • Ability to persist notes on the device running the application, across browser sessions

In terms of user interface, the application revolves around two views, the Notes List view and the Note Editor view:

Ready? Let’s get started.

The model-view-controller pattern in Sencha Touch

As of version 1.1.0 of Sencha Touch, the framework’s implementation of the model-view-controller pattern is, in my opinion, incomplete and poorly documented. However, we can still take advantage of it to build high-quality applications.

In general, a Sencha Touch MVC application will consist of one or more views, one or more data models and stores, and one or more controllers. Views have a dual role: they render representations of the data in the models, and capture and transmit user input to the controllers. Controllers will translate the user’s input into changes in the data and/or the behavior of the application. Models represent the application’s data and state.

In Sencha Touch MVC parlance, the mechanism by which views send messages to the controllers is called dispatch, and the controller functions used to handle these messages are called actions.

Let’s apply these concepts to our Notes Application and come up with a tentative design.

The Notes App use cases, MVC style

Based on our requirements list, we can translate our use cases into the following MVC workflows:

  1. When the application launches, the user will be presented with a list of the existing notes, or an empty list if there are no notes saved. This will happen by invoking a controller’s index action, which in turn will render the Notes List view:
  2. When our user needs to create a new note, she will tap the New button in the Notes List view. This will invoke the newnote action in a controller, where a new note will be created and loaded into the Note Editor view:
  3. Similarly, when our user needs to edit a note, she will tap the disclosure button for the given note. This will invoke the editnote action in a controller, causing the note to be loaded into the Note Editor view:
  4. Tapping the Save button in the Note Editor view will invoke a controller’s savenote action, where the note will be saved in notes cache and the Notes List view will be activated:
  5. If our user taps the Trash button in the Note Editor view, the view will invoke a controller’s deletenote action. This action will delete the note loaded in the view and activate the Notes List view:
  6. Tapping the Home button in the Note Editor view will invoke a controller’s canceledit action, which will discard any changes made to the loaded note, and activate the Notes List view:

Notice that I have referred to “a controller” in my descriptions of the use cases. This means that we still don’t know if we will use one or many controllers in the application. We will make this decision in a few minutes.

All right. Now that we have an idea about how we’re going to build the Notes Application, MVC-style, let’s talk about files.

Distributing a Sencha Touch application across multiple files

You should consider distributing your Sencha Touch application’s source code across multiple files. A single source file might be fine for a very small application, however, as the application and the development team grow, you can cut development and maintenance costs by using a multiple-files model.

What we will do for this example is, first, create the right folder structure for the application, and then create the different source files that will contain the application’s components. The folder structure reflects the MVC pattern we will use to build the application:

We have a root folder called app, and under it, folders for the models, views, controllers and data stores. Besides being self-explanatory, it follows Sencha’s recommendation to place models, views and controllers in separate folders. Sencha’s build tools are capable of leveraging this structure to create the application’s namespaces when it’s time to build a production-ready version of the source files.

At this point we have modeled our MVC workflows and created the right folder structure. What do you say about writing some code?

The Note data model

The Note model is the data model that represents a note. Its source goes in the NoteModel.js file, which we will place in the models folder. This is the source:

Ext.regModel('NoteModel', {
    idProperty: 'id',
    fields: [
        { name: 'id', type: 'int' },
        { name: 'date', type: 'date', dateFormat: 'c' },
        { name: 'title', type: 'string' },
        { name: 'narrative', type: 'string' }
    ],
    validations: [
        { type: 'presence', field: 'id' },
        { type: 'presence', field: 'title', message: 'Please enter a title for this note.' }
    ]
});

The Notes data store

The NotesStore is our notes cache. It uses the NoteModel class as its model and it’s configured to use an instance of the Ext.data.LocalStorageProxy class as its proxy. This allows us to save notes across browser sessions. We will place the NotesStore class in the NotesStore.js file, which lives in the stores folder. This is the store’s source:

Ext.regStore('NotesStore', {
    model: 'NoteModel',
    sorters: [{
        property: 'date',
        direction: 'DESC'
    }],
    proxy: {
        type: 'localstorage',
        id: 'notes-app-store'
    },
    getGroupString: function (record) {
        if (record && record.data.date) {
            return record.get('date').toDateString();
        } else {
            return '';
        }
    }
});

NotesApp.stores.notesStore = Ext.StoreMgr.get('NotesStore');

The Main view

As in the first version of the application, the application’s main view will serve as the viewport, hosting the Notes List view and the Note Editor view. This view’s class goes in the MainView.js file, which we will place in the views folder:

NotesApp.views.MainView = Ext.extend(Ext.Panel, {
    fullscreen: true,
    layout: 'card',
    cardSwitchAnimation: 'slide',
    initComponent: function () {

        Ext.apply(NotesApp.views, {
            notesListView: new NotesApp.views.NotesListView({ notesStore: NotesApp.stores.notesStore }),
            noteEditorView: new NotesApp.views.NoteEditorView()
        });

        this.items = [
            NotesApp.views.notesListView,
            NotesApp.views.noteEditorView
        ]

        NotesApp.views.MainView.superclass.initComponent.call(this);

        this.on('render', function () {
            NotesApp.stores.notesStore.load();
        });
    }
});

The Notes List view

We will use an instance of the NotesListView class to render the list of cached notes and allow the user to start the “new note” and “edit note” workflows. Its file, NotesListView.js, goes in the views folder. This is the source:

NotesApp.views.NotesListView = Ext.extend(Ext.Panel, {

    notesStore: Ext.emptyFn,
    notesList: Ext.emptyFn,

    layout: 'fit',

    initComponent: function () {

        this.newButton = new Ext.Button({
            text: 'New',
            ui: 'action',
            handler: this.onNewNote,
            scope: this
        });

        this.topToolbar = new Ext.Toolbar({
            title: 'My Notes',
            items: [
                { xtype: 'spacer' },
                this.newButton
            ]
        });

        this.dockedItems = [this.topToolbar];

        this.notesList = new Ext.List({
            store: this.notesStore,
            grouped: true,
emptyText: '</pre>
<div style="margin: <span class=;">5px;">No notes cached.</div>
<pre>
<pre>',
            onItemDisclosure: true,
itemTpl: '
<div class="list-item-title">{title}</div>
<pre>' +
                            '<div class="list-item-narrative">{narrative}</div>'

        });

        this.notesList.on('disclose', function (record, index, evt) {
            this.onEditNote(record, index);
        }, this),

        this.items = [this.notesList];

        NotesApp.views.NotesListView.superclass.initComponent.call(this);
    },

    onNewNote: function () {
        Ext.dispatch({
            controller: NotesApp.controllers.notesController,
            action: 'newnote'
        });
    },

    onEditNote: function (record, index) {
        Ext.dispatch({
            controller: NotesApp.controllers.notesController,
            action: 'editnote',
            note: record
        });
    },

    refreshList: function () {
        this.notesList.refresh();
    }
});

Let’s pause here and examine a few details.

While the initComponent() function should be very familiar to you, things become interesting when we look at the helper functions onNewNote() and onEditNote(). These functions will allow us to signal the view’s controller that the user executed either the “new note” or “edit note” commands. The newnote or editnote controller actions will be invoked in response to these commands.

How does the view send these signals to the controller? Well, it’s pretty simple. Remember what I said earlier about dispatch? Every application gets a default instance of the Dispatcher class, which you can use to send requests to specific actions in a given controller. Invoking Ext.Dispatcher.dispatch() or its shorthand, Ext.dispatch(), will find the desired controller and call the correct controller action.

You should pass the controller and action arguments to the dispatch() function. And you can also pass any other parameters that the controller might need to execute the action correctly. For example, in the onEditNote() function we also pass the record corresponding to the note the user needs to edit.

The third function, refreshList(), will allow this view’s controller to re-render the list of notes in the view after the list has been modified.

OK, we already dispatched a couple of messages to a controller that does not exist. How about we create it?

How many controllers does a Sencha Touch MVC application need?

I don’t think they make rules for this. It depends on the application. I know that the Notes App is small: three simple views and six actions. It looks like one controller is enough for this one.

Let’s create the NotesController.js file in the controllers folder, and add the following code to it:

Ext.regController('NotesController',{

    'index': function (options) {

    },

    'newnote': function (options) {

    },

    'editnote': function (options) {

    },

    'savenote': function (options) {

    },

    'deletenote': function (options) {

    },

    'canceledit': function (options) {

    }
});

NotesApp.controllers.notesController = Ext.ControllerManager.get('NotesController');

Starting to make more sense now? As you can see, the controller has the actions we have discussed. This is where we need to implement our use cases. But, before we do it, let’s work on the last view.

The Note Editor view

We will use an instance of the NoteEditorView class to give our users the ability to edit and delete notes. This class goes in the NoteEditorView.js file, in the views folder:

NotesApp.views.NoteEditorView = Ext.extend(Ext.form.FormPanel, {

    initComponent: function () {

        this.backButton = new Ext.Button({
            text: 'Home',
            ui: 'back',
            handler: this.backButtonTap,
            scope: this
        });

        this.saveButton = new Ext.Button({
            text: 'Save',
            ui: 'action',
            handler: this.saveButtonTap,
            scope: this
        });

        this.trashButton = new Ext.Button({
            iconCls: 'trash',
            iconMask: true,
            handler: this.trashButtonTap,
            scope: this
        });

        this.topToolbar = new Ext.Toolbar({
            title: 'Edit Note',
            items: [
                this.backButton,
                { xtype: 'spacer' },
                this.saveButton
            ]
        });

        this.bottomToolbar = new Ext.Toolbar({
            dock: 'bottom',
            items: [
                { xtype: 'spacer' },
                this.trashButton
            ]
        });

        this.dockedItems = [this.topToolbar, this.bottomToolbar];

        NotesApp.views.NoteEditorView.superclass.initComponent.call(this);
    },

    backButtonTap: function () {
        Ext.dispatch({
            controller: NotesApp.controllers.notesController,
            action: 'canceledit'
        });
    },

    saveButtonTap: function () {
        Ext.dispatch({
            controller: NotesApp.controllers.notesController,
            action: 'savenote'
        });
    },

    trashButtonTap: function () {
        Ext.dispatch({
            controller: NotesApp.controllers.notesController,
            action: 'deletenote'
        });
    },

    items: [{
        xtype: 'textfield',
        name: 'title',
        label: 'Title',
        required: true
    }, {
        xtype: 'textareafield',
        name: 'narrative',
        label: 'Narrative'
    }]
});

The helper functions saveButtonTap(), trashButtonTap() and backButtonTab() will allow us to signal the controller that the “save note”, “delete note” or “cancel edit” commands were executed by the user. The application will run the savenote, deletenote or canceledit controller actions in response to these commands.

Now that our views are finished, let’s implement the controller actions.

Implementing controller actions

Back in the NotesController class, as we’ve already discussed, the index action will create the Main view and activate the Notes List view:

'index': function (options) {

    if (!NotesApp.views.mainView) {
        NotesApp.views.mainView = new NotesApp.views.MainView();
    }

    NotesApp.views.mainView.setActiveItem(
        NotesApp.views.notesListView
    );
}

The index action will be invoked when the application launches. We will see how this is done when we implement the application’s launch() function.

The newnote controller action will create a new note, load it into the Note Editor view, and activate the view:

'newnote': function (options) {

    var now = new Date();
    var noteId = now.getTime();
    var note = Ext.ModelMgr.create({ id: noteId, date: now, title: '', narrative: '' },
        'NoteModel'
    );

    NotesApp.views.noteEditorView.load(note);
    NotesApp.views.mainView.setActiveItem(
        NotesApp.views.noteEditorView,
        { type: 'slide', direction: 'left' }
    );
}

We need the editnote action to load the selected note into the Note Editor view:

'editnote': function (options) {

    NotesApp.views.noteEditorView.load(options.note);
    NotesApp.views.mainView.setActiveItem(
        NotesApp.views.noteEditorView,
        { type: 'slide', direction: 'left' }
    );
}

The savenote action will save the note in the cache and activate the Notes List view:

'savenote': function (options) {

    var currentNote = NotesApp.views.noteEditorView.getRecord();

    NotesApp.views.noteEditorView.updateRecord(currentNote);

    var errors = currentNote.validate();
    if (!errors.isValid()) {
        currentNote.reject();
        Ext.Msg.alert('Wait!', errors.getByField('title')[0].message, Ext.emptyFn);
        return;
    }

    if (null == NotesApp.stores.notesStore.findRecord('id', currentNote.data.id)) {
        NotesApp.stores.notesStore.add(currentNote);
    } else {
        currentNote.setDirty();
    }

    NotesApp.stores.notesStore.sync();

    NotesApp.stores.notesStore.sort([{ property: 'date', direction: 'DESC'}]);

    NotesApp.views.notesListView.refreshList();

    NotesApp.views.mainView.setActiveItem(
        NotesApp.views.notesListView,
        { type: 'slide', direction: 'right' }
    );
}

Next comes the deletenote action. This action will delete the note loaded in the Note Editor and activate the Notes List view:

'deletenote': function (options) {

    var currentNote = NotesApp.views.noteEditorView.getRecord();

    if (NotesApp.stores.notesStore.findRecord('id', currentNote.data.id)) {
        NotesApp.stores.notesStore.remove(currentNote);
    }

    NotesApp.stores.notesStore.sync();
    NotesApp.views.notesListView.refreshList();

    NotesApp.views.mainView.setActiveItem(
        NotesApp.views.notesListView,
        { type: 'slide', direction: 'right' }
    );
}

Finally, the controller’s canceledit action will discard any changes made to the loaded note, and activate the Notes List view:

'canceledit': function (options) {

    NotesApp.views.mainView.setActiveItem(
        NotesApp.views.notesListView,
        { type: 'slide', direction: 'right' }
    );
}

You with me so far? We’re almost done.

Compared to the first version of the app, we’ve moved most of the application’s behavior out of the views, which now deal strictly with UI matters, and into a central location. I can see the MVC version being easier to understand and support.

Now that we have our data store, model, views and controller in place, we’re just missing the piece that will get this whole mechanism going.

Launching the application

We will place our app’s entry point in the app.js file, app folder, together with the index.html and app.css files. We will use app.js to create our application’s instance like so:

var App = new Ext.Application({
    name: 'NotesApp',
    useLoadMask: true,

    launch: function () {
        Ext.dispatch({
            controller: NotesApp.controllers.notesController,
            action: 'index'
        });
    }
});

And that’s it. Makes sense?

As you can imagine, this is not the only approach you can take for incorporating the MVC pattern in a Sencha Touch app. You can go from rolling out your own implementation, to taking full advantage of the framework’s built-in features, which I think are still a work in progress. Either way, you should be able to reach a satisficing solution that makes your application easier to develop and more maintainable.

What do you think?

Download this tutorial’s source: NotesApp-v1.zip

Want To Learn More?

My Sencha Touch books will teach you how to create an application from scratch.

Comments

  1. Mangesh says

    Hello Jorge,

    Thanks for this wonderful example. I liked your style of explaining.

    What is the link for your latest book on Sencha Touch.

  2. vinoth says

    hi , i am newbie in ST V2.2.0.
    i want new updates for this article . can u help me .

  3. Sur007 says

    Hi Jorge,
    i have a condition where i want to limit data.For ex. in a view i only want to post 7 data and next data are to be listed on next view respectively. and in the last page i want show some information. How can i do that?

  4. Sruthi says

    hi,

    i am new to senchatouch. i downloaded the sample app(contactList) from github. How to run the senchatouch app…