Récapitulatif sur les Controllers
Avec Ext JS 4, un contrôleur est une classe qui hérite de Ext.app.Controller
. Ces contrôleurs utilisent des sélecteurs (selector
) semblables aux sélecteurs CSS (appelés “Component Queries
”) pour localiser les composants (component
) et réagir à leurs événements. Ils font également usage de “refs
” pour sélectionner et récupérer des instances de component
.
Ces contrôleurs sont créés au lancement de l'application et restent présents pendant toute la durée de vie de celle-ci. Pendant cette durée de vie, les vues (view
) intéressant un contrôleur vont apparaître et disparaître. On peut même avoir plusieurs instances d'une même vue gérées par un même contrôleur.
Défis
Pour les applications d'envergure, ces techniques peuvent présenter certains défis.
Dans de tels environnements, les vues et les contrôleurs peuvent être écrits par plusieurs équipes de développement puis intégrés dans l'application finale. S'assurer que les contrôleurs ne réagissent que pour les vues qu'ils sont supposés gérer peut être difficile. De plus, il est courant que les développeurs veuillent limiter le nombre de contrôleurs créés au lancement de l'application. Bien que créer des contrôleurs au moment où on en a besoin soit possible au prix d'un peu d'effort, ils n'est pas possible de les détruire si bien qu'ils subsistent même quand ils ne servent plus à rien.
ViewControllers
Bien que Ext JS 5 assure la compatibilité ascendante avec les contrôleurs actuels (i.e ceux de Ext JS 4), il offre un nouveau type de contrôleur conçu pour répondre à ces défis : Ext.app.ViewController
. Un ViewController
:
- simplifie la connexion avec les vues en utilisant les entrées de
config
“listeners
” et “reference
”. - améliore le cycle de vie des vues pour gérer automatiquement leurs
ViewController
associés. - réduit la complexité en s'appuyant sur une relation un-un avec la vue prise en charge.
- apporte l'encapsulation nécessaire pour fiabiliser l'imbrication des vues.
- conserve la capacité de sélectionner des composants et de se mettre à l'écoute de leurs événements à tous les niveaux de profondeur de la vue associée.
Listeners
La config
listeners
n'est pas nouvelle mais elle a gagné plusieurs nouvelles capacités dans Ext JS 5. Une description plus complète des nouvelles fonctionnalités des listeners
est disponible dans l'article -- “Declarative Listeners in Ext JS 5” (traduction à venir). Pour ce qui concerne les ViewControllers
, nous pouvons nous limiter à examiner deux exemples. Le premier est une utilisation basique d'une config
listeners
concernant un sous-élément dans une vue (child item
) :
Ext.define('MyApp.view.foo.Foo', { extend: 'Ext.panel.Panel', xtype: 'foo', controller: 'foo', items: [{ xtype: 'textfield', fieldLabel: 'Bar', listeners: { change: 'onBarChange' // scope non indiqué ici } }] });
Ext.define('MyApp.view.foo.FooController', { extend: 'Ext.app.ViewController', alias: 'controller.foo', onBarChange: function (barTextField) { // appelé par l'événement 'change' } });
L'utilisation précédente de listeners
montre un gestionnaire d'événement nommé (« onBarChange
») qui n'indique pas la portée (scope
). En interne, le système d'événements résout le scope
par défaut pour le champ texte Bar
via son ViewController
d'appartenance.
Historiquement, la config
listeners
ne pouvait être utilisée que par le créateur d'un composant, donc comment une vue peut-elle se mettre à l'écoute de ses propres événements ou, éventuellement, à celle des événements émis par sa classe de base ? La solution passe par la déclaration explicite du scope :
Ext.define('MyApp.view.foo.Foo', { extend: 'Ext.panel.Panel', xtype: 'foo', controller: 'foo', listeners: { collapse: 'onCollapse', scope: 'controller' }, items: [{ ... }] });
L'exemple précédent expose deux nouvelles fonctionnalités dans Ext JS 5: les named scopes
et les listeners
déclaratifs. Nous allons nous intéresser ici aux named scopes
. Il existe deux valeurs possibles pour les named scopes
: “this
” et “controller
”. Quand on écrit des applications MVC, on a presque toujours besoin d'utiliser “controller
” qui a pour résultat évident d'observer le ViewController
de la vue en question (et pas le ViewController
de la vue qui a créé l'instance).
Puisqu'une vue est un type de Ext.Component
, nous avons affecté à cette vue un “xtype
” qui autorise d'autres vues à créer une instance de notre vue de la même façon qu'elle-même crée son champ texte. Pour voir comment tout ceci fonctionne ensemble, considérons une vue qui utilise celle-ci. Par exemple :
Ext.define('MyApp.view.bar.Bar', { extend: 'Ext.panel.Panel', xtype: 'bar', controller: 'bar', items: [{ xtype: 'foo', listeners: { collapse: 'onCollapse' } }] });
Dans ce cas, la vue Bar
crée une instance de la vue Foo
comme étant l'un de ses éléments. De plus, elle se met à l'écoute de l'événement collapse
tout comme la vue Foo
. Dans les versions précédentes d'Ext JS et de Sencha Touch, ces déclarations entraient en conflit. Avec Ext JS 5, cependant, ceci est résolu de la façon attendue. Les listeners
déclarés par la vue Foo
vont se déclencher dans le ViewController
de Foo
tandis que les listeners
déclarés dans la vue Bar
vont se déclencher dans le ViewController
de Bar
.
Références
L'une des difficultés les plus communes lorsqu'on écrit la logique d'un contrôleur est de se mettre en relation avec tous les composants nécessaires pour réaliser une action donnée. Prenons quelque chose de simple :
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 () { // ... trouver le grid et lui ajouter un enregistrement ... } });
Mais comment obtenir le composant grid
? Avec Ext JS 4, vous pouviez utiliser la config
“refs
” ou un autre moyen pour rechercher le composant. Toutes les techniques nécessitent que vous donniez à la grid
une propriété reconnaissable afin qu'elle puisse être identifiée de manière unique. Les précédentes techniques utilisaient la config
“id
” (et Ext.getCmp
) ou la config
“itemId
” (en utilisant “refs
” ou une méthode de requête de composant). L'avantage de “id
” est la rapidité avec laquelle on peut récupérer le composant correspondant, mais ces identifiants doivent être uniques à la fois dans toute l'application et le DOM, ce qui n'est pas toujours souhaitable. Utiliser “itemId
” offre davantage de flexibilité mais rend nécessaire de réaliser une recherche pour trouver le composant souhaité.
Avec la nouvelle config
reference
dans Ext JS 5, nous ajoutons simplement la « reference
» à la grid
et nous utilisons “lookupReference
” pour la récupérer :
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'); } });
Cela revient à affecter un itemId
“fooGrid
” et à exécuter : “this.down(‘#fooGrid’)
”. Cependant, la différence sous le capot est tout à fait significative. D'abord, la config
reference
indique au composant de s'enregistrer lui-même auprès de sa vue (identifiée par la présence d'un ViewController
dans ce cas précis). Ensuite, la méthode lookupReference
se contente de consulter le cache pour voir si la référence doit être rafraîchie (par exemple du fait de l'ajout ou de la suppression éventuelle d'un container
). Si tout va bien, la méthode renvoie simplement la référence stockée dans le cache. Ce qui donne, en pseudo-code ;
lookupReference: (reference) { var cache = this.references; if (!cache) { Ext.fixReferences(); // corrige toutes les références cache = this.references; // maintenant le cache est à jour } return cache[reference]; }
En d'autres mots, il n'y a pas de recherche et les liens endommagés par l'ajout ou la suppression d'éléments dans les containers
sont corrigés en une fois, quand c'est nécessaire. Comme nous allons le voir après, cette approche possède d'autres avantages que la seule performance.
Encapsulation
L'utilisation de sélecteurs dans l'implémentation MVC d'Ext JS 4 était très flexible mais présentait dans le même temps certains risques. Le fait que ces sélecteurs « voyaient » tout à tous les niveaux de la hiérarchie des composants était à la fois très puissant mais propice aux erreurs. Par exemple, un contrôleur pouvait être opérationnel à 100 % quand il s'exécutait de manière isolée mais pouvait planter dès que d'autres vues étaient ajoutées parce que ses sélecteurs avaient des correspondances non désirées avec la nouvelle vue.
Ces problèmes peuvent être gérés en suivant certaines techniques, mais en utilisant des listeners
et des references
avec un ViewController
, ces problèmes disparaissent purement et simplement. C'est parce que les config
listeners
et reference
ne se connectent qu'avec leur ViewController
d'appartenance. Les vues sont libres de choisir toute valeur de référence qui sera unique pour elle car ces noms ne seront pas exposés au créateur de la vue.
De la même façon, les listeners
sont résolus pour leur ViewController
d'appartenance et ne peuvent pas être accidentellement être distribués à des gestionnaires d'événements d'autres contrôleurs ayant des sélecteurs itinérants. Bien que les listeners
soient souvent préférables aux sélecteurs, les deux mécanismes fonctionnent bien ensembles pour les cas où une approche par sélecteurs serait préférable.
Pour compléter ce modèle, les vues ont besoin d'activer des événements qui seront traités par leurs propres ViewController
de vue. Il existe une méthode pour vous aider à ça dans ViewController
: fireViewEvent
. Par exemple :
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); } });
Ceci active la forme standard du listener
pour le créateur de la vue :
Ext.define('MyApp.view.bar.Bar', { extend: 'Ext.panel.Panel', xtype: 'bar', controller: 'bar', items: [{ xtype: 'foo', listeners: { collapse: 'onCollapse', addrecord: 'onAddRecord' } }] });
Listeners et domaines d'événements
Avec Ext JS 4.2, le répartiteur d'événements (event dispatcher
) avait été généralisé avec l'introduction des domaines d'événements (event domains
). Ces domaines d'événements interceptaient les événements qui étaient générés et les répartissaient aux contrôleurs en fonction de la correspondance des sélecteurs. Le « composant » event domain
possédait des sélecteurs de recherche de composants complets alors que les autres domaines avaient des sélecteurs plus limités.
Avec Ext JS 5, chaque ViewController
crée une instance d'un nouveau type de domaine d'événements appelé le “view
” event domain
. Ce domaine d'événements permet aux ViewControllers
d'utiliser les méthodes standard “listen
” et “control
” tout en limitant leur portée implicitement à leur vue. Il ajoute également un sélecteur spécial correspondant à la vue elle-même :
Ext.define('MyApp.view.foo.FooController', { extend: 'Ext.app.ViewController', alias: 'controller.foo', control: { '#': { // correspond à la vue elle-même collapse: 'onCollapse' }, button: { click: 'onAnyButtonClick' } } });
La différence majeure entre les listeners
et les sélecteurs est visible ci-dessus. Le sélecteur « button
» va correspondre avec tous les boutons de cette vue et de toutes les vues filles, quelle que soit la profondeur de parenté, donc même si le bouton appartient à une vue arrière-petite fille. En d'autres mots, les gestionnaires à base de sélecteurs ne respectent pas les frontières d'encapsulation. Ce comportement est conforme avec le comportement du Ext.app.Controller
précédent, ce qui peut s'avérer une technique pratique pour certaines situations particulières.
Enfin, ces domaines d'événements respectent l'imbrication et font efficacement remonter un événement dans la hiérarchie de la vue. C'est-à-dire que lorsqu'un événement est activé, il est d'abord transmis à tout listener
standard. Ensuite, il est transmis à son ViewController
d'appartenance, puis aux ViewController
parents (s'il y en a) en remontant la hiérarchie. Éventuellement, l'événement est transmis au composant standard domaine d'événements pour être pris en charge par les contrôleurs dérivés de Ext.app.Controller
.
Cycle de vie
Une technique courante avec les grandes applications consiste à créer dynamiquement des contrôleurs la première fois qu'on en a besoin. Ceci peut contribuer à réduire le temps de chargement de l'application et améliore également les performances d'exécution en n'activant pas la totalité des contrôleurs pouvant exister. Dans les versions précédentes, une des limitations de cette technique était que ces contrôleurs restaient actifs dans l'application. Il n'était pas possible de les détruire et de libérer leurs ressources. De plus, cela ne changeait pas le fait qu'un contrôleur pouvait avoir n'importe quel nombre de vues associées (y compris aucune).
Le ViewController
, cependant, est créé très tôt dans le cycle de vie du composant et est lié à cette vue pour sa durée de vie complète. Quand une vue est détruite, le ViewController
est détruit en même temps. Cela signifie que le ViewController
n'est désormais plus obligé de gérer les situations dans lesquelles il n'y a pas de vue ou il y en a plusieurs.
Cette relation un-un implique que le suivi des références est simplifié et n'est plus susceptible de provoquer de fuite pour les composants détruits. Un ViewController
peut implémenter n'importe lesquelles de ces méthodes pour exécuter des tâches aux moments clefs de son cycle de vie :
beforeInit
— Cette méthode peut être surchargée de façon à agir sur la vue avant que sa méthodeinitComponent
ne soit appelée. Cette méthode est appelée immédiatement après que le contrôleur a été créé ce qui se produit lorsqueinitConfig
est appelée par le constructeur deComponent
.init
— Appelée juste après queinitComponent
a été appelé pour la vue. C'est le moment typique pour réaliser l'initialisation du contrôleur dès lors que la vue est initialisée.initViewModel
— Appelée quand leViewModel
de la vue est créé (s'il y en a un de défini).destroy
— Nettoyage de toutes les ressources (assurez-vous d'appelercallParent
).
Conclusion
Nous pensons que les ViewControllers
vont significativement rationaliser vos applications MVC. Ils fonctionnent également très bien avec les ViewModels
si bien que vous pouvez combiner ces approches et leurs avantages respectifs. Nous sommes impatients que cette version soit livrée (ExtJS 5, ce qui est fait désormais) et de voir à l’œuvre dans vos applications les améliorations qu'elle apporte.
Écrit par Don Griffin
Don Griffin est un membre de l'équipe centrale de Ext JS. Il a été un utilisateur de Ext JS pendant 2 ans avant de rejoindre Sencha et possède plus de 20 ans d'expérience comme ingénieur de développement sur une large gamme de plates-formes. Son expérience s'étend de la conception des front-ends et backends d'applications web, aux applications client lourd, aux protocoles réseau et aux pilotes de périphériques. La passion de Don est de construire des produits de classe mondiale que les gens aiment utiliser.