ExtJS - Alles asynchron

Beitrag vom 20.04.2016

Speziell mit dem in ExtJS 5 eingeführten ViewModels und ViewControllers merkt man eine Sache ganz schnell: Alles läuft asynchron. Und wenn man etwas entwickeln möchte was das noch recht junge Architektur nicht kann, müssen einige Sachen beachtet werden.

Ein Beispiel:

  • Eine Anwendung mit mehreren Tabs
  • Jedes Tab ist ein Modul, welches aus einem Menü gestartet werden kann
  • Es gibt einen App Controller, welcher mit Events die Tabs verwaltet
    • Andere Tabs müssen nur ein Event an den UI Controller schicken, um ein weiteres Tab zu laden
    • Das Event loadmodule erzeugt ein neues Tab
    • Das Event moduleloaded wird ausgelöst wenn ein Tab fertig erzeugt wurde

Das Szenario

  • Tab A, welches geöffnet ist, soll Tab B öffnen
  • In dem neuen Tab sollen automatisch Buttons erzeugt werden (wofür auch immer)

Der Ablauf

  • Klick in Tab A auf einen Button
  • Das click Event des Buttons sendet das loadmodule Event an den UI Controller
  • Es wird auf das moduleloaded Event gewartet, welches auch die entsprechenden Objekt Instanzen mitliefert um den Inhalt des neuen Tabs zu kontrollieren
  • An die neue Tab Instanz wird ein Event gesendet welches die Erstellung der Buttons veranlasst

Codebeispiel

Normal würde man die einzelnen Klassen bzw Contorols natürlich in separate Dateien auslagern. Das habe ich hier gespart, damit man das Beispiel in Senchas Fiddler ausführen kann.

/**
 * Demo Anwendung für asynchrones Laden von Controls
 * und das Auslösen von Aktionen
 * ExtJS 5.0.1
 */

//--> Tabpanel bzw Maincontrol der ExtJS App
Ext.define('Async.view.MainViewport', {
    extend:'Ext.container.Viewport',
    xtype:'app-main',
    layout:'fit',
    items: [
        // Tabpanel
        {
            xtype:'tabpanel',
            reference:'modules',

            // Toolbar mit Buttons
            tbar: [
                {
                    xtype:'button',
                    text:'Open Tab A',
                    reference:'clickme'
                },
                {
                    xtype:'button',
                    text:'Open Tab B',
                    reference:'clickmeb'
                }
            ]
        }
    ]
});

//--> Controller für Tab A
Ext.define('Async.view.TabAController', {
    extend:'Ext.app.ViewController',
    alias:'controller.taba', // alias wird im control benutzt

    config: {
        listen: {
            component: {
                // Auf das Click Event des "Tab B öffnen und Custom Event auslösen" buttons horchen
                'button[reference=testbutton]': {
                    click:'onTestClick'
                }
            }
        }
    },

    // Event methode
    onTestClick: function()
    {
        // ID erzeugen
        var uid = Ext.id();

        // Auf gestartetes Tab mit der entsprechenden ID warten
        var onlaunched = function(viewname, viewuid, viewobj)
        {
            console.log(['custom tabb opened', viewuid, uid]);
            if(viewuid==uid)
            {
                // Wenn richtige ID, customevent im Tab auslösen
                console.log('fire customaction');
                viewobj.fireEvent('customaction');
            }
        }

        // Auf neue Tabs einmalig horchen
        Async.App.on('modulelaunched', onlaunched, this, { single:true });

        // Aus Tab A das Tab B starten
        Async.App.fireEvent('launchmodule', 'Async.view.TabB', uid);
    }
});

//--> Tab A Control
Ext.define('Async.view.TabA', {
    extend: 'Ext.panel.Panel',
    xtype:'taba',
    controller: "taba", // alias des controllers ohne prefix
    layout:'anchor',
    title:'Tab A',
    bodyPadding:20,
    items: [
         // Button welcher Tab B mit Custom Event startet
        {
            xtype:'button',
            reference:'testbutton',
            text:'Tab B öffnen und Custom Event auslösen'
        }
    ]
});

//--> Controller für Tab B
Ext.define('Async.view.TabBController', {
    extend:'Ext.app.ViewController',
    alias:'controller.tabb',

    config: {
        listen: {
            component: {
                'tabb': {
                    customaction:'onCustomAction'
                }
            }
        }
    },

    onCustomAction: function()
    {
        var me = this;
        var buttons = [0,1,2,3,4,5,6,7,8,9];
        Ext.each(buttons, function(i)
        {
            me.getView().add([{ xtype:'button', text:'Button No '+i}]);
        });
    }
});

// Tab B Control
Ext.define('Async.view.TabB', {
    extend:'Ext.panel.Panel',
    xtype:'tabb',
    controller: "tabb",
    layout:'anchor',
    title:'Tab B',
    closable:true,
    bodyPadding:20,
    items: [
    ]
});

// Globaler UI Controller
Ext.define('Async.controller.Ui', {
    extend: 'Ext.app.Controller',

    config: {
        // Click Events aus der toolbar
        control: {
            'app-main > tabpanel > toolbar[dock=top] > button[reference=clickme]': {
                click:'onClickMe'
            },
            'app-main > tabpanel > toolbar[dock=top] > button[reference=clickmeb]': {
                click:'onClickMeB'
            }
        },
        // Erzeugt automatisch Setter für die Controls
        refs: {
            main:'app-main', // this.getMain()
            tabs:'app-main > tabpanel' // this.getTabs()
        },
        // Auf globale Events horchen
        listen: {
            controller: {
                '*': {
                    launchmodule:'onLaunchModule',
                    modulelaunched:'onModuleLaunched'
                }
            }
        }
    },

    init: function()
    {
        console.info('UI controller initialized');
    },

    // Click Event des Tab A Toolbar Buttons
    onClickMe: function()
    {
        this.fireEvent('launchmodule', 'Async.view.TabA');
    },

    // Click Event des Tab B Toolbar Buttons
    onClickMeB: function()
    {
        this.fireEvent('launchmodule', 'Async.view.TabB');
    },

    // Globales Event Tab starten
    onLaunchModule: function(viewname, uid)
    {
        console.log(['launchmodule', viewname, uid]);
        var me = this;

        // Wurde keine ID mitgegeben erstelle eine
        if(!uid)
        {
            uid = Ext.id();
        }

        // Neues Tab initialisieren
        var tab = Ext.create(viewname);

        // Globales Event feuern wenn Tab fertig asynchron initialisiert wurde
        tab.on('afterrender', function() 
               { 
                   console.log(['fire modulelaunched', viewname, uid]);
                   Async.App.fireEvent('modulelaunched', viewname, uid, tab); 
               }, me);

        // Tab zum Tabpanel hinzufügen
        this.getTabs().add(tab);

        // Tab anzeigen
        this.getTabs().setActiveTab(tab);
    },

    // Globales Event für "Tab fertig initialisiert"
    onModuleLaunched: function()
    {
        console.info('some module was launched');
    }
});

//--> Accessible Checks deaktivieren
Ext.enableAria = false;

//--> Start ExtJS Application
Ext.application({
    extend: 'Ext.app.Application',
    name : 'Async',
    appProperty:'App',
    autoCreateViewport:'Async.view.MainViewport',
    requires: [
        'Async.view.MainViewport'
    ],
    controllers: [
        'Ui'
    ]
});

Beim nachladen (AJAX) von Daten

Möchte man über ein Event Daten aus einem Grid manipulieren, besteht in diesem Szenario das Problem, dass auf die Fertigstellung des Ladevorgangs gewartet werden muss, bevor man die Grid Daten manipulieren kann.

Hier muss dann zusätzlich zu dem Laden des Tabs selbst noch auf den Data Store gewartet werden.

Netterweise bietet der Data Store eine Property mit dem Load Count. Dieser kann abgefragt werden. Ist der Zähler kleiner als 1, muss das load Event des Stores dazwischen geschaltet werden.

Ein Beispiel für die onCustomAction Methode in Tab B, sofern Tab B einen Data Store besitzen würde.

var me = this;
var store = me.getViewModel().getStore('contacts');
var doit = function() { store.add({ contacttype: 42 }); }

if(store.loadCount>0) {
    // Store ist bereits geladen
    doit();
} else {
    // Store ist noch nicht geladen
    store.on('load', doit, this, { single:true });
}

Hallo Internet

Mein Name ist Christian, vom Beruf bin ich Anwendungsentwickler.

In meiner Freizeit beschäftige ich mich mit verschiedensten Technologien. Hier sammele ich Dinge, die für mich interessant waren oder sind.