bonjour,
j'aimerais avoir votre opinion quant à l'utilité des states dans un composant personnalisé. Attention, j'ai bien dis "states" et non "skinStates".
Je m'explique :
il n'est plus à démontrer l'utilité du pattern MVC (Model, Vue, Controleur) et Flex via sa nouvelle architecture Spark permet de découper plus facilement un composant en ces 3 tiers (voir un article ici ). Ainsi, ces 3 tiers peuvent être représentés par le schéma ci dessous (source : lien ci dessus) :
La Vue, représentée dorénavant dans l'architecture Spark par la classe skin associée au composant, doit donc encapsuler toute la logique visuelle du composant en question. Logique visuelle comprenant entre autre :
- les couleurs
- les fonts
- le placement relatif des composants entre eux (top, left, bottom, right...)
- la présence ou non de des composants (includeIn, excludeFrom)
- les transitions
- ...
nota : j'appelle composants dans la liste non exhaustive ci dessus, les sous composants du composant personnalisé en question (ie : les fameux skinParts).
Or, Flex via la classe UIComponent (classe parente de tous les composants personnalisés), définit une propriété "states" de type tableau initialisée à null mais pouvant facilement être modifiée via les getters/setters. Cette propriété sert à créer des états sur le composant lui même contrairement au skinStates définissant des états sur les skins.
Cependant, nous sommes en droit de nous poser la question suivante : A quoi peut donc servir cette propriété states sur le composant personnalisé ?
Par "facilité" ou par méconnaissance du pattern MVC, nous pourrions être tentés de basculer la logique vue dans la logique contrôleur et de faire dans le cas d'un composant personnalisé étendant la classe SkinnableContainer et nommé MyPersoComp la chose suivante :
Aucune erreur de compilation n'apparait et notre composant est opérationnel. Nous pouvons donc facilement instancier notre composant MyPersoComp et changer dynamiquement son label en jouant avec la propriété currentState du composant :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14 <?xml version="1.0" encoding="utf-8"?> <s:SkinnableContainer xmlns:fx="http://ns.adobe.com/mxml/2009" xmlns:s="library://ns.adobe.com/flex/spark" xmlns:mx="library://ns.adobe.com/flex/mx"> <s:states> <s:State name="state1" /> <s:State name="state2" /> </s:states> <s:Button label.state1="label de l'état 1" label.state2="label de l'état 2" /> </s:SkinnableContainer>
Un joli bouton labellisé "label de l'état 2" apparaît ce qui est le résultat attendu. Oui mais en faisant cela, nous avons déplacé la logique vue (ici représenté par le changement/affichage du label bouton) dans le tiers contrôleur représenté par le composant...
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9 <?xml version="1.0" encoding="utf-8"?> <s:Application xmlns:fx="http://ns.adobe.com/mxml/2009" xmlns:s="library://ns.adobe.com/flex/spark" xmlns:mx="library://ns.adobe.com/flex/mx" xmlns:components="com.components.*"> <components:MyPersoComp currentState="state2" /> </s:Application>
Cet exemple bien que simpliste montre la facilité avec laquelle un développeur peut se mélanger les pinceaux avec les notions de "component states" et de "skin states". Certes, l'exemple évoqué représente la façon la plus directe et la plus rapide pour modifier rapidement et automatiquement une propriété en fonction d'un état, mais comme souvent en informatique, la solution de facilité n'est pas la meilleure.
Voici maintenant 2 solutions qui selon moi représentent une bien meilleure alternative :
1ère solution : faire du "mirroring delegate"
description: nous créons toujours 2 états state1 et state2 sur notre contrôleur (notre composant dérivé de UIComponent) puis nous surchargeons la méthode getCurrentSkinState() de ce composant afin de faire correspondre (mirroring) l'état de notre composant avec l'état de notre skin. Nous avons donc un tableau de states correspondant au tableau de skinStates ou du moins une relation 1-1 entre les states et les skinStates.
MyPersoComp (version AS3):
Appel dans notre application :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103 package com.components { import flash.events.MouseEvent; import mx.events.FlexEvent; import mx.states.State; import spark.components.Button; import spark.components.supportClasses.SkinnableComponent; /** * notre skin devra contenir les skinStates "state1" et "state2" */ [SkinState("state1")] [SkinState("state2")] public class MyPersoComp extends SkinnableComponent { /** * notre skin devra obligatoirement implémenter un bouton dont l'id vaudra "buttonPerso" */ [SkinPart(name="buttonPerso", required="true")] public var buttonPerso:Button; public function MyPersoComp() { super(); initStates(); addEventListener(FlexEvent.STATE_CHANGE_COMPLETE, onStateChange); } /** * crée les états associés à notre composant */ private function initStates():void { var state1:State = new State(); state1.name = "state1"; var state2:State = new State(); state2.name = "state2"; states = [state1, state2]; } /** * un changement de state implique une invalidation de notre skin. * Cette invalidation appelle (automatiquement) la fonction getCurrentSkinState() * qui retourne le nouvel état à la skin */ private function onStateChange(e:FlexEvent):void { invalidateSkinState(); } /** * ajout d'un écouteur sur le bouton à l'ajout de celui dans notre composant personnalisé */ override protected function partAdded(partName:String, instance:Object):void { super.partAdded(partName, instance); if(instance == buttonPerso) { buttonPerso.addEventListener(MouseEvent.CLICK, onClick); } } /** * suppression de l'écouteur ajouté par la fonction partAdded */ override protected function partRemoved(partName:String, instance:Object):void { super.partRemoved(partName, instance); if(instance == buttonPerso) { buttonPerso.removeEventListener(MouseEvent.CLICK, onClick); } } /** * un click sur notre bouton change l'état de notre composant et finalement son label * via la diffusion de l'événement FlexEvent.STATE_CHANGE_COMPLETE */ protected function onClick(e:MouseEvent):void { currentState = (currentState == "state1" || null == currentState) ? "state2" : "state1"; } /** * un changement d'état sur notre composant sera immédiatement affecté sur notre skin. * Cette fonction sert donc de mirroir(composant -> skin) */ override protected function getCurrentSkinState():String { return currentState; } } }
MyPersoCompSkin1 :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2 <components:MyPersoComp skinClass="com.components.skins.MyPersoCompSkin1" />
Ainsi, notre composant personnalisé délégue toute sa logique vue à son skin via la diffusion d'un événement FlexEvent.STATE_CHANGE_COMPLETE.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 <?xml version="1.0" encoding="utf-8"?> <s:Skin xmlns:fx="http://ns.adobe.com/mxml/2009" xmlns:s="library://ns.adobe.com/flex/spark" xmlns:mx="library://ns.adobe.com/flex/mx"> <fx:Metadata> [HostComponent("com.components.MyPersoComp")] </fx:Metadata> <s:states> <s:State name="state1" /> <s:State name="state2" /> </s:states> <s:Button id="buttonPerso" label.state1="label de l'état 1" label.state2="label de l'état 2"/> </s:Skin>
2ième solution : solution "stateless states"
description: cette solution repose grandement sur le fondement de la solution précédente excepté que l'on s'abstient d'utiliser la propriété states de notre composant. On préférera utiliser une simple propriété de notre classe afin de stocker le nom de notre skinState (un peu moins consommateur de ressources donc).
MyPersoComp :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81 package com.components { import flash.events.MouseEvent; import mx.events.FlexEvent; import mx.states.State; import spark.components.Button; import spark.components.supportClasses.SkinnableComponent; /** * notre skin devra contenir les skinStates "state1" et "state2" */ [SkinState("state1")] [SkinState("state2")] public class MyPersoComp extends SkinnableComponent { /** * notre skin devra obligatoirement implémenter un bouton dont l'id vaudra "buttonPerso" */ [SkinPart(name="buttonPerso", required="true")] public var buttonPerso:Button; /** * notre propriété nous donnant le nom de notre skinState */ private var _state:String; public function MyPersoComp() { super(); } /** * ajout d'un écouteur sur le bouton à l'ajout de celui dans notre composant personnalisé */ override protected function partAdded(partName:String, instance:Object):void { super.partAdded(partName, instance); if(instance == buttonPerso) { buttonPerso.addEventListener(MouseEvent.CLICK, onClick); } } /** * suppression de l'écouteur ajouté par la fonction partAdded */ override protected function partRemoved(partName:String, instance:Object):void { super.partRemoved(partName, instance); if(instance == buttonPerso) { buttonPerso.removeEventListener(MouseEvent.CLICK, onClick); } } /** * un click sur notre bouton change l'état de notre composant via une invalidation de son skin */ protected function onClick(e:MouseEvent):void { _state = (_state == "state1" || null == _state) ? "state2" : "state1"; invalidateSkinState(); } /** * la valkeur de notre skinState est contenu dans notre propriété privée _state */ override protected function getCurrentSkinState():String { return _state; } } }
Appel dans notre application :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2 idem méthode précédente
MyPersoCompSkin1 :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2 idem méthode précédente
Les limites de ces 2 solutions : les solutions évoquées sont parfaitement fonctionnelles si le composant personnalisé ne comprend uniquement que des sous composants de type "skinPart" c'est à dire qu'il faut que tous ces sous composants soient listés via la metadata [SkinPart] dans notre fichier .as. A défaut, si nous ajoutons des composants non annotés par cette metadata dans notre composant (ce qui est loin d'être recommandé ni recommandable je pense), il nous faudra faire un "mix" entre la première solution proposée et une notation pointée dans notre composant (propriete.state1 = "..."). Nous retombons donc dans les travers évoqués en début de sujet.
Cette vision des choses n'engage que moi bien sûr. De mon côté, je n'utilise jamais les états de composants ou du moins je n'utilise jamais de notation pointée directement dans mes composants personnalisés. La seule façon dont j'utilise les états de composants est celle évoquées ici en méthode 1. Ceci dit, j'utilise la méthode 2 autant que faire se peut.
...et vous, comment voyez vous les choses concernant les "components states" ?
Merci de m'avoir lu et de votre participation au débat![]()
Partager