Skip to content

Ext JS MVC Architecture — Models, Views, Controllers, and Application Structure

DodaTech Updated 2026-06-28 7 min read

In this tutorial, you will learn about Ext JS MVC Architecture. We cover key concepts, practical examples, and best practices to help you master this topic.

Ext JS MVC architecture separates application logic into Models (data), Views (UI components), and Controllers (business logic), providing organized code structure for large-scale enterprise applications.

What You'll Learn

  • MVC folder structure and naming conventions
  • Defining Controllers with refs and control
  • View-Controller communication via events
  • Application class and app initialization
  • Best practices for large MVC projects

Why It Matters

Without a clear architecture, Ext JS applications become unmanageable as they grow. Controllers contain event handlers, Views remain pure component configurations, and Models define data contracts — each layer has a single responsibility and can be tested independently.

Real-World Use

An enterprise CRM with 100+ screens where each module (Contacts, Deals, Reports) has its own Controller, multiple View classes, and shared Model definitions — allowing teams to work on separate modules without merge conflicts.

MVC Architecture

flowchart LR
    A[Application] --> B[Controllers]
    B --> C[Views]
    B --> D[Models]
    B --> E[Stores]
    C --> F[Components]
    D --> G[Data Fields]
    E --> H[Data Sources]
    B -.-> I[Event Flow]
    I --> J[View Events]
    I --> K[Store Events]
    I --> L[Controller Actions]
    style A fill:#e6f3ff,stroke:#4a90d9,stroke-width:2px

Folder Structure

myapp/
  app/
    controller/
      Main.js
      Users.js
      Products.js
    model/
      User.js
      Product.js
    store/
      Users.js
      Products.js
    view/
      Main.js
      users/
        List.js
        Form.js
      products/
        Grid.js
        Form.js
    Application.js
  app.js
  index.html

Application Definition

// app/Application.js
Ext.define('MyApp.Application', {
  extend: 'Ext.app.Application',
  name: 'MyApp',
  appFolder: 'app',
  controllers: [
    'Main',
    'Users',
    'Products'
  ],
  stores: [
    'Users',
    'Products'
  ],
  launch: function() {
    Ext.create('MyApp.view.Main', {
      renderTo: Ext.getBody()
    });
  }
});

// app.js - Entry point
Ext.application({
  name: 'MyApp',
  appFolder: 'app',
  controllers: ['Main', 'Users', 'Products'],
  autoCreateViewport: false,
  launch: function() {
    Ext.create('MyApp.view.Main', {
      renderTo: Ext.getBody()
    });
  }
});

Expected output: The application loads all controllers and stores, then creates the main view. The Application class manages the lifecycle and Dependency Injection.

Controller Definition

// app/controller/Users.js
Ext.define('MyApp.controller.Users', {
  extend: 'Ext.app.Controller',
  refs: [{
    ref: 'usersGrid',
    selector: 'usersgrid'
  }, {
    ref: 'userForm',
    selector: 'userform'
  }, {
    ref: 'addButton',
    selector: 'usersgrid button[action=add]'
  }],
  control: {
    'usersgrid': {
      itemdblclick: 'onEditUser',
      itemcontextmenu: 'onUserContextMenu'
    },
    'usersgrid button[action=add]': {
      click: 'onAddUser'
    },
    'userform button[action=save]': {
      click: 'onSaveUser'
    },
    'userform button[action=cancel]': {
      click: 'onCancelEdit'
    }
  },
  stores: ['Users'],
  init: function() {
    this.getUsersStore().load();
    console.log('Users controller initialized');
  },
  onAddUser: function() {
    var form = Ext.create('MyApp.view.users.Form');
    form.show();
  },
  onEditUser: function(grid, record) {
    var form = Ext.create('MyApp.view.users.Form');
    form.getForm().loadRecord(record);
    form.show();
  },
  onUserContextMenu: function(grid, record, item, index, event) {
    event.stopEvent();
  },
  onSaveUser: function(btn) {
    var form = btn.up('userform').getForm();
    if (form.isValid()) {
      form.updateRecord(form.getRecord());
      this.getUsersStore().sync();
      btn.up('window').close();
    }
  },
  onCancelEdit: function(btn) {
    btn.up('window').close();
  }
});

Expected output: The controller listens for events from views using CSS-like selectors. Double-clicking a user grid row opens the edit form. Clicking Add opens a blank form. Save writes to the store and syncs to server.

View Definition with xtype

// app/view/users/Grid.js
Ext.define('MyApp.view.users.Grid', {
  extend: 'Ext.grid.Panel',
  alias: 'widget.usersgrid',
  title: 'Users',
  store: 'Users',
  columns: [
    { text: 'ID', dataIndex: 'id', width: 50 },
    { text: 'Name', dataIndex: 'name', flex: 1 },
    { text: 'Email', dataIndex: 'email', flex: 2 },
    { text: 'Status', dataIndex: 'status', width: 100,
      renderer: function(v) { return v === 'active' ? 'Active' : 'Inactive'; }
    }
  ],
  tbar: [{
    text: 'Add User',
    action: 'add',
    iconCls: 'x-fa fa-plus'
  }, {
    text: 'Delete',
    action: 'delete',
    disabled: true,
    iconCls: 'x-fa fa-trash'
  }],
  dockedItems: [{
    xtype: 'pagingtoolbar',
    dock: 'bottom',
    store: 'Users',
    displayInfo: true
  }]
});

// app/view/users/Form.js
Ext.define('MyApp.view.users.Form', {
  extend: 'Ext.window.Window',
  alias: 'widget.userform',
  title: 'User Details',
  width: 450,
  modal: true,
  layout: 'fit',
  items: [{
    xtype: 'form',
    bodyPadding: 15,
    defaults: { anchor: '100%' },
    items: [{
      xtype: 'textfield',
      fieldLabel: 'Name',
      name: 'name',
      allowBlank: false
    }, {
      xtype: 'textfield',
      fieldLabel: 'Email',
      name: 'email',
      vtype: 'email'
    }, {
      xtype: 'combobox',
      fieldLabel: 'Status',
      name: 'status',
      store: ['active', 'inactive']
    }]
  }],
  buttons: [{
    text: 'Save',
    action: 'save'
  }, {
    text: 'Cancel',
    action: 'cancel'
  }]
});

Expected output: The grid and form views are defined as Ext JS classes with aliases. The controller references them using widget selectors. Views remain pure component configurations with no business logic.

View-Controller Event Flow

// Option 1: Controller listens (recommended)
Ext.define('MyApp.controller.Orders', {
  extend: 'Ext.app.Controller',
  refs: [{ ref: 'ordersGrid', selector: 'ordersgrid' }],
  control: {
    'ordersgrid': {
      select: 'onOrderSelect',
      itemclick: 'onOrderClick'
    }
  },
  onOrderSelect: function(grid, record) {
    this.loadOrderDetails(record.get('id'));
  },
  loadOrderDetails: function(orderId) {
    console.log('Load order:', orderId);
  }
});

// Option 2: View fires custom events
Ext.define('MyApp.view.orders.Grid', {
  extend: 'Ext.grid.Panel',
  alias: 'widget.ordersgrid',
  listeners: {
    itemdblclick: function(grid, record) {
      grid.fireEvent('orderSelected', grid, record);
    }
  }
});

Expected output: The controller-centered approach keeps views stupid (no logic) and controllers smart (all logic). Custom events add semantic meaning like orderSelected instead of raw itemdblclick.

Store Management in MVC

// app/store/Users.js
Ext.define('MyApp.store.Users', {
  extend: 'Ext.data.Store',
  alias: 'store.users',
  model: 'MyApp.model.User',
  autoLoad: false,
  pageSize: 25,
  proxy: {
    type: 'ajax',
    url: '/api/users',
    reader: { type: 'json', rootProperty: 'data', totalProperty: 'total' },
    writer: { type: 'json', writeAllFields: true }
  }
});

// Controller accessing stores
Ext.define('MyApp.controller.Users', {
  extend: 'Ext.app.Controller',
  stores: ['Users'],
  init: function() {
    var store = this.getUsersStore();
    store.load({
      params: { status: 'active' },
      callback: function(records) {
        console.log('Loaded ' + records.length + ' users');
      }
    });
  }
});

Expected output: Stores are declared in the controller's stores array, which auto-generates a getter method. The controller initiates data loading and reacts to store events.

Application Lifecycle

Ext.define('MyApp.Application', {
  extend: 'Ext.app.Application',
  name: 'MyApp',
  controllers: ['Users', 'Products', 'Orders'],
  init: function() {
    console.log('App initializing');
  },
  launch: function() {
    Ext.create('MyApp.view.Viewport', {
      renderTo: Ext.getBody()
    });
    console.log('App launched');
  },
  onAppUpdate: function() {
    Ext.Msg.confirm('Update Available', 'Reload for the latest version?', function(btn) {
      if (btn === 'yes') {
        window.location.reload();
      }
    });
  }
});

Expected output: The application lifecycle runs init first, then creates controllers and stores, then calls launch. onAppUpdate provides a hook for service worker updates.

Common Mistakes

  1. Putting business logic in views - Views should be pure component configurations. All event handlers and data manipulation belong in controllers.

  2. Not using refs for component lookups - Using this.getUsersGrid() is cleaner than Ext.ComponentQuery.query('usersgrid'). Refs are defined once and reused.

  3. Creating circular controller dependencies - Controller A references Controller B's store, and B references A's. Break the cycle by sharing a common store or using the application-level stores.

  4. Loading data in view constructors - Views should not call store.load(). Controllers initiate data loading after views are created and configured.

  5. Defining the same store in multiple controllers - Register a store once (in Application.js or one controller) and reference it in others using the store alias.

Practice Questions

  1. What is the purpose of the refs array in a controller?
  2. How does the control config map events to handler methods?
  3. What is the difference between init and launch in the Application class?
  4. How do you access a store from a controller?
  5. Why should views not contain business logic?

Challenge: Build a complete MVC module for order management with: an Orders controller with CRUD handlers, a Grid view with action column buttons, a Form view in a window for create/edit, a Model with validators, a Store with server proxy, proper refs and control mappings, and integration with the main application.

FAQ

What is the difference between Ext.app.Controller and Ext.app.ViewController?

Controller manages multiple views globally. ViewController is lightweight and scoped to a single view instance. Use ViewController for simple component logic and Controller for cross-view orchestration.

How do I pass data between controllers?

Use shared stores, the application-level event bus (this.application.fireEvent()), or direct method calls with this.getController('Other').

Can I lazy-load controllers?

Yes. Use controller.getController('Name') to dynamically load a controller. The controller is created lazily and its init method is called on first access.

What happens if two controllers listen to the same event?

Both controllers receive the event in the order they were created. Each handler runs independently. Use application-level event control for shared concerns.

How does Ext JS handle view destruction in MVC?

When a view is destroyed, controller refs pointing to it return null. Check refs before accessing them, or listen to the view's destroy event to clean up.

Mini Project

Build a full MVC product management module: Product model with fields and validators, Product store with Ajax proxy and pagination, Product grid view with column renderers and action buttons, Product form view (window with form panel), Product controller with refs, control mappings, and CRUD handlers, integration with the main application class, and a main viewport with border layout hosting the grid.

What's Next

MVC structures your code. Learn how Ext JS Routing adds URL-based navigation with browser history support and deep-linking to specific views.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro