Les NSArrayControllers à la fête !
muqaddar
Administrateur
Tutorial réalisé par ClicCool
Collections et dictionnaires.
Nous allons gérer une collection de données sous forme d'un tableau de dictionnaires. Pour éviter le "sempiternel répertoire" et être dans l'actualité, nous allons gérer nos emplettes sur le iTune's Music Store (c'est fou ce qu'on y dépense mine de rien) !
Commençons déjà par déclarer le NSArray qui contiendra tous nos achats sur le store, sous forme de "NSMutableDictionary". Evidemment, c'est un NSMutableArray, sinon nous ne pourrions rien y ajouter. Notre fichier interface MyDocument.h contient donc cette courte déclaration :
Nous allons initialiser le tableau, pas dans +(void)initialize puisque ce tableau est une propriété de chaque instance, mais dans le -(id)init préparé par Xcode. Initialisons un tableau vierge :
Ajoutons le "release final", qui doit intervenir quand le document est désalloué. C'est un bon réflexe de prévoir immédiatement le -dealloc dès qu'on initialise un objet dans -init.
Implémentons la sauvegarde sur fichier. Là , les NSDocument nous permettent plusieurs approches. Nous utiliserons la méthode :
-(BOOL)writeToFile:(NSString*)aPath ofType:(NSString*)type
Et là encore on reste très simple :
Implémentons la lecture sur fichier en utilisant la fonction miroir de la précédente :
-(BOOL)readFromFile:(NSString*)aPath ofType:(NSString*)type
Nous nous débarrassons du tableau précédent et le remplaçons par le contenu du fichier passé en argument :
Déclarons le type de documents. Cela fait plus propre, et cela limite les documents proposés à l'ouverture aux seuls documents ayant le bon label. Double-cliquons sur la cible (target) ou allez dans le menu "Project -> Edit Active Target", puis dans l'onglet "Properties", rensignons l'extension et l'OSType de myDocument, par exemple "lbbd" pour LaBoBinDings. :-)
Voilà pour le code. Et les NSMutableDictionnary et leurs clés qui représenteront les morceaux ? Et les méthodes d'ajout de suppression et de tri ? Les tris sur les Array ont pourtant fait couler beaucoup d'encre ! Cessons de dire des bétises et ouvrons myDocument.nib en doudle-cliquant dessus .
Collections et dictionnaires.
Nous allons gérer une collection de données sous forme d'un tableau de dictionnaires. Pour éviter le "sempiternel répertoire" et être dans l'actualité, nous allons gérer nos emplettes sur le iTune's Music Store (c'est fou ce qu'on y dépense mine de rien) !
Commençons déjà par déclarer le NSArray qui contiendra tous nos achats sur le store, sous forme de "NSMutableDictionary". Evidemment, c'est un NSMutableArray, sinon nous ne pourrions rien y ajouter. Notre fichier interface MyDocument.h contient donc cette courte déclaration :
@interface MyDocument : NSDocument<br />{<br /> NSMutableArray *tableau;<br />}<br />@end
Nous allons initialiser le tableau, pas dans +(void)initialize puisque ce tableau est une propriété de chaque instance, mais dans le -(id)init préparé par Xcode. Initialisons un tableau vierge :
- (id)init<br />{<br /> self = [super init];<br /> if (self) {<br /> tableau = [[NSMutableArray alloc] init];<br /> }<br /> return self;<br />}
Ajoutons le "release final", qui doit intervenir quand le document est désalloué. C'est un bon réflexe de prévoir immédiatement le -dealloc dès qu'on initialise un objet dans -init.
- (void)dealloc {<br /> [tableau release];<br />}
Implémentons la sauvegarde sur fichier. Là , les NSDocument nous permettent plusieurs approches. Nous utiliserons la méthode :
-(BOOL)writeToFile:(NSString*)aPath ofType:(NSString*)type
Et là encore on reste très simple :
-(BOOL)writeToFile:(NSString*)aPath ofType:(NSString*)type {<br /> return [tableau writeToFile:aPath atomically:YES];<br />}
Implémentons la lecture sur fichier en utilisant la fonction miroir de la précédente :
-(BOOL)readFromFile:(NSString*)aPath ofType:(NSString*)type
Nous nous débarrassons du tableau précédent et le remplaçons par le contenu du fichier passé en argument :
-(BOOL)readFromFile:(NSString*)aPath ofType:(NSString*)type {<br /> [tableau autorelease];<br /> tableau = [[NSMutableArray alloc] initWithContentsOfFile:(NSString*)aPath];<br /> return YES;<br />}
Déclarons le type de documents. Cela fait plus propre, et cela limite les documents proposés à l'ouverture aux seuls documents ayant le bon label. Double-cliquons sur la cible (target) ou allez dans le menu "Project -> Edit Active Target", puis dans l'onglet "Properties", rensignons l'extension et l'OSType de myDocument, par exemple "lbbd" pour LaBoBinDings. :-)
Voilà pour le code. Et les NSMutableDictionnary et leurs clés qui représenteront les morceaux ? Et les méthodes d'ajout de suppression et de tri ? Les tris sur les Array ont pourtant fait couler beaucoup d'encre ! Cessons de dire des bétises et ouvrons myDocument.nib en doudle-cliquant dessus .
Connectez-vous ou Inscrivez-vous pour répondre.
Réponses
Tout le reste de l'article se passe dans Interface Builder !
Créons notre fenêtre de document.
# Supprimons le NSTextField du centre : "your document's content here" à la trappe !
# Ajoutons une NSTableView qui prend toute la largeur et tout le haut de la fenêtre. Apportons lui la multi-sélection en cochant la case adéquat dans le panneau Attributes des infos.
# Ajoutons un NSArrayController en le faisant glisser de la palette des contrôleurs vers la fenêtre des instances. Connectons le contrôleur à son contenu, il faut qu'il sache où est le modèle. Au lieu d'utiliser son Outlet "content" (qui existe), nous allons le binder à un objet toujours présent quand il faut : le File's Owner. Ctrl-Clic de NSArrayController vers "File's Owner". Le propriétaire du nib, c'est justement notre instance de MyDocument. Donc, commençons à binder (on est là pour ça) le NSArrayController lui-même :
- "Controller content" -> "ContentArray"
- "Bind To": "File's Owner (myDocument)"
- "Controller key": indisponible
- "Model Key Path" : "tableau" (le nom de notre variable d'instance)
# Indiquons la nature des objets gérés par le contrôleur de tableaux pour lui indiquer qu'il contient des NSMutableDictionary. Dans le panneau "Attributes" de la fenêtre d'infos, dans le champ "Object Class Name", nous devrions donc taper le nom de la classe des objets du tableau. Ici ça tombe bien, par défaut y'a déjà "NSMutableDictionary".
# Précisons au contrôleur qu'il doit gérer la création d'un nouveau NSMutableDictionary lors d'un ajout. En cochant tout simplement en dessous la boà®te "Automatically prepare content". Au passage jetez un oeil aux autres boà®tes, leur titre est assez explicite. Laissons les toutes cochées.
# La NSTableView. Nous double-cliquons dessus (pas sur un en-tête de colonne) et regardons le panneau Bindings. Nous n'utiliserons ici aucun des Bindings proposés. Notons simplement que le binding "Table Content" -> "content" est le plus souvent configuré automatiquement dès que les colonnes sont bindées (même si ça se voit pas sous IB). Mais tant que la NSTableView est sélectionnée profitons en pour la configurer avec 3 colonnes (vous pouvez en mettre plus) dans le panneau Attributes, #column. Pour la configuration des bindings, nous allons le faire directement avec les colonnes.
# Bindings des NSTableColumns.
Double-cliquons sur l'en-tête de la première colonne et tapons directement dans l'en-tête (ou utilisons le panneau attributes) et fixons le titre d'en-tête à "Titre" justement. Puis Bindons la comme ceci :
- "Value" -> "value"
- "Bind To": "NSArrayController" (ou le nom que vous avez donné au Controller)
- "Controller Key": "arrangedObjects", tous les objets (NSDictionary) du tableau (éventuellement triés).
- "Model Key Path" : "Titre", nous créons par IB une des clés d'entrée des dictionnaires.
Cliquons maintenant sur l'en-tête de la deuxième colonne, titrons-là "Auteur" et bindons la comme la première, en changeant "Model Key Path" à "Auteur". Enfin, la troisième colonne s'appelera "Montant" et son "Model Key Path" à "Montant".
Pour cette colonne, nous allons également ajouter un NSFormatter. Depusi la palette des "text", glissons un NSNumberFormatter sur notre colonne. La palette "infos" bascule sur la gestion des Formatter. Choisissons "$9,999..99" comme formatter tout prêt, puis cochons la boà®te "Localize". Au passage, retournons voir le panneau Bindings. Remarquez que deux nouveaux bindings sont maintenant disponibles : Min et MaxValue. La présence d'un NSFormatter modifie en effet les bindings disponibles.
# Ajoutons des boutons.
Nous allons ajouter pas moins de 5 boutons.
- un boutont "Précédent" que nous lions à l'action "selectPrevious" du NSArrayController. Control-Dragons du bouton vers l'instance du NSArrayController (cube vert). Dans le panneau "Connexions" de la palette "info", connectons l'action "selectPrevious".
- un bouton "Suivant" que nous lions à l'action "selectNext" du NSArrayController
- un bouton "Ajouter" que nous lions à l'action "add" du NSArrayController
- un bouton "Insérer" que nous lions à l'action "insert" du NSArrayController
- un bouton "Supprimer" que nous lions à l'action "remove" du NSArrayController
# Bindons les deux premiers boutons
Le bouton "Précédent" :
- "Availability" -> "enabled"
- "Bind To": "NSArrayController"
- "Controller Key": "canSelectPrevious"
- "Model Key Path" : Inutilisé
Le bouton suivant "Suivant" :
- "Availability" -> "enabled"
- "Bind To" : "NSArrayController"
- "Controller key" : "canSelectNext"
- "Model Key Path" : Inutilisé
Malheureusement les "Controller key", "canAdd", "canInsert" et "canRemove", n'ont pas un comportement aussi transparent qu'on aurait pu le souhaiter et renverront toujours YES ici.
# Ajoutons des totaux
Un Label font (agrandissez-là ) pour lequel nous utiliserons le pattern binding non pas pour le coté multiple valeurs incluses, mais pour le coté Pattern seulement !
- "Value With Pattern" -> "displayPatternValue1"
- "Bind To": "NSArrayController"
- "Controller Key": "arrangedObjects"
- "Model Key Path" : "@count"
"@count" est un des opérateurs sur tableaux gérés par les bindings.
Ici nous demandons le nombre de tous les objets du tableau :
- "Display Pattern" : "Prix des %{value1}@ titres :"
Un NSTextField en face du Label Font et sur lequel nous glissons un NSNumberFormatter, paramétré pour la monnaie localisée comme pour la colonne des montants. Nous bindons le champ ainsi:
- "Value" -> "value"
- "Bind To": "NSArrayController"
- "Controller Key": "arrangedObjects"
- "Model Key Path" : "@sum.Montant"
Vous l'avez compris @sum, un autre opérateur sur tableau, va ici permettre un calcul en temps réel de la somme des clefs "Montant" de tous les dictionnaires contenus dans "tableau" !
Un autre Label Font (long) pour lequel nous utiliserons également le pattern binding.
- "Value With Pattern" -> "displayPatternValue1"
- "Bind To" : "NSArrayController"
- "Controller Key" : "selection"
- "Model Key Path" : "@count"
"@count" renvoie ici le nombre des objets sélectionnés du tableau.
- "Display Pattern" : "Prix des %{value1}@ titres sur les %{value2}@ :"
Et :
- "Value With Pattern" -> "displayPatternValue2"
- "Bind To": "NSArrayController"
- "Controller Key": "arrangedObjects"
- "Model Key Path" : "@count"
- "Display Pattern" : "Prix des %{value1}@ titres sur les %{value2}@ :"
Enfin un autre NSTextField sur lequel nous glissons un NSNumberFormatter paramétré pour la monnaie localisée. Nous bindons le champ ainsi:
- "Value" -> "value"
- "Bind To" : "NSArrayController"
- "Controller Key": "selection"
- "Model Key Path" : "@sum.Montant"
Vous l'avez compris "@sum", un autre opérateur sur tableau, va ici permettre un calcul en temps réel de la somme des clefs "Montant" des dictionnaires sélectionnés dans "tableau".
[Fichier joint supprimé par l'administrateur]
Nous allons ajouter un NSDrawer pour la vue détaillée. Glissons un NSDrawer dans notre fenêtre des instances puis une NSCustomView. Dans atributes, nous pouvons lui demander de sortir en bas de la fenêtre, choissisez "bottom". Connectons le Drawer : l'Outlet "ParentWindow" à la fenêtre du document, puis l'Outlet "ContentView" à la nouvelle NSCustomView, toujours par contrôle-clic.
[Fichier joint supprimé par l'administrateur]
- "Parameters" -> "hiden"
- "Bind To" : "NSArrayController"
- "Controller Key": "selection"
- "Model Key Path" : "Montant"
- "Value Transformer" : "NSIsNotNil" puisque ce binding attend un booléen.
Chaque fois que le montant est précisé, le Drawer s'ouvre et révèle son contenu.
Pas joli comme comportement ici mais rigolo : on peut imager d'autres critères pour révéler ou non la vue détaillée...
[Fichier joint supprimé par l'administrateur]
Vendredi 27 août 2004 / Réalisé par ClicCool, Mise en forme d'osxitan / [Vu : 1093 fois] / Réagir ! / Imprimer
Les ArrayControllers dans tous leurs états!
Qu'il est beau mon tiroir
Nous allons ajouter un NSDrawer pour la vue détaillée. Glissons un NSDrawer dans notre fenêtre des instances puis une NSCustomView. Dans atributes, nous pouvons lui demander de sortir en bas de la fenêtre, choissisez "bottom". Connectons le Drawer : l'Outlet "ParentWindow" à la fenêtre du document, puis l'Outlet "ContentView" à la nouvelle NSCustomView, toujours par contrôle-clic.
Il est l'heure de binder le Drawer :
- "Parameters" -> "hiden"
- "Bind To" : "NSArrayController"
- "Controller Key": "selection"
- "Model Key Path" : "Montant"
- "Value Transformer" : "NSIsNotNil" puisque ce binding attend un booléen.
Chaque fois que le montant est précisé, le Drawer s'ouvre et révèle son contenu.
Pas joli comme comportement ici mais rigolo : on peut imager d'autres critères pour révéler ou non la vue détaillée...
Remplisssons le Drawer. Il faut serrer un peu pour que ça rentre. Ajoutons 4 "Label Font" : Titre, Auteur, Prix et Note. Puis nous ajoutons 3 NSTextView à côté des 3 premiers Labels que nous bindons sur la sélection en cours du contôleur :
- "Value" -> "value"
- "Bind To" : "NSArrayController"
- "Controller Key" : "selection"
- "Model Key Path" : "Titre" pour le premier, "Auteur" pour le deuxième, "Montant" pour le troisième. N'oublions pas de glisser un NSNumberFormatter paramétré pour la monnaie localisée sur le troisième.
Ajoutons maintenant un NSTextView à côté du label "Note".
Pour lui nous devons binder sur "data" (le contenu est une NSAttributeString), il est nécessaire de double cliquer sur la TexteView :
- "Value" -> "data"
- "Bind To" : "NSArrayController"
- "Controller Key" : "selection"
- "Model Key Path" : et là nous créons une autre clé pour nos dictionnaires : "Note". Cochons aussi ici la case "Continuously Update Value".
Enfin, dans mainMenu.nib, ajoutons un menu "Fonte", afin de pouvoir "faire joujou" avec nos notes. Faisons glisser un menu "Font" de la palette sur le menu "MainMenu" entre Edit et Window.
Nous pouvons maintenant compiler et éxécuter le projet. Et bien sûr faire des tests.
[Fichier joint supprimé par l'administrateur]
# Le comportement de "add:".
Le bouton "Ajouter" ajoute une ligne vierge (un dictionnaire vierge) au tableau. Il faut ensuite double-cliquer sur un champ de la ligne pour l'éditer. Notons aussi que si nous n'avons pas pris la peine de cocher "Select Inserted Objects" dans le panneau attributes du NSArrayController, nous aurions carrément l'impression que rien ne se passe et risquerions d'ajouter n lignes au lieu d'une.
# Les tris sont automatiques.
Rien à dire, on peut également réorganiser les colonnes. Mais notons bien que le tri sur la TableView laisse intact le tableau du Document (c'est pratique).
Inversez les colonnes, triez puis sauvegardez, à la réouverture tout a repris l'ordre initial.
# Les sélections multiples.
Créons plusieurs lignes avec le même montant pour toutes (au hasard € 0,99).
Notons alors que lors de sélections multiples de ligne ayant le même montant, les champs du Drawer sont grisés et affichent "Multiple Values" (on peut changer le contenu en tapant un "Multiple Values PaceHolder" dans le panneau bindings).
MAIS pas tous les champs. Le binding nous permet d'éditer d'un coup tous les dictionnaires sélectionnés si la valeur retournée pas la clé du champ est identique (et c'est la même règle de comportement qu'applique la "Controller Key" "canEdit"). Mais si le montant diffère d'une ligne sélectionnée à l'autre, le drawer se ferme. Aucune valeur n'est alors renvoyée pour la clé, et donc NSIsNotNil renvoie faux.
Mais pour des data cela ne marche pas comme attendu. Le NSTextField est en effet toujours sélectionnable quelque soit la sélection ! Le champ est toujours vide et éditable en cas de multiple sélection ! Ce résultat par défaut est pour le moins peu conforme aux recommandations du Apple Human Interface Guidelines !
# Encore plus surprenant. Les Bindings permettent de synchroniser la vue et le modèle dans les 2 sens. Donc si l'un change, l'autre est mis à jour.
C'est très bien, mais savez-vous à quel point ? Par exemple, le Binding de "hidden" du Drawer sur "Montant" par le biais de NSIsNotNil, est-il à double sens ? On se demande comment ce pourrait-être le cas !
Et pourtant !
Retournez dans MyDocument.nib sous IB pour ajouter un bouton en bas de la fenêtre de document. Connectez le à l'action "toggle:" du NSDrawer.(control-drag du boutons vers le drawer ...). Sauvegardez, re-compilez et exécutez.
Créez plusieurs lignes avec des montants différents et sélectionnez les toutes, le Drawer se ferme, c'est normal. Et maintenant cliquez sur votre bouton, (mais perdez pas de vue les montants) pour forcer l'ouverture du Drawer. Surprenant non ? En fait, en cliquant sur notre bouton nous avons littéralement "saisi" une nouvelle valeur pour la propriété "hidden" qui, comme elle est bindée, répercute la saisie sur le modelObjet. Le binding va alors forcer selection.Montant à une valeur quelconque mais pas nil (transformer NSIsNotNil). Et comme nous l'avons vu plus haut, il faut pour ça que toutes les valeurs soient alors identiques. Aussitôt dit, aussitôt fait !!! Permettez moi de penser qu'ici le Binding est ici non seulement à double sens mais en plus à double tranchant ! :-)
Voilà quelques particularités des bindings, je vous les laisse explorer plus à fond. N'hésitez pas à les modifier.
Ajouter par exemple, au binding de "hidden" du Drawer, un Place Holder qui renvoie vrai en cas d'absence de sélection. Le drawer sera aussi ouvert s'il n'y a pas de sélection et ne sera en fait fermé que s'il y a une multiple sélection sur des dictionnaires n'ayant pas le même montant ou si un seul dictionnaire avec un montant non renseigné est seul sélectionné. Mais si le montant est nul (zéro) le drawer s'ouvre, bien sûr ! (nul n'est pas nil). A la prochaine fois !
Je viens de finir le tuto, tout fonctionne, mais à chaque ouverture/création d'un document j'ai le message suivant dans la console :
quelqu'un à une idée, j'ai l'impression qu'a l'init y a des "choses" pas "prêtes" et qu'ensuite ça tombe en marche... bizarre
@+
Je ne me souviens plus de ce tuto, mais il semble bizarre que la synchronisation (le binding) soit faite avec un champ de MyDocument !
Autrement pour répondre à ta question de la "bonne préparation" des bindings, la méthode béton (souvent inutile, notamment lorsqu'on utilise @property) est de mettre un "exposeBinding" dans la méthode de classe +(void) initialize.
J'ai testé le exposeBinding, mais ça change rien.
J'ai ensuite épuré au maximum --> j'ai retiré le drawer et les deux binding avec l'opérateur @sum. Là je n'ai plus de message dans la console.
Dès que je rajoute le drawer avec un simple binding sur Titre, Auteur et Montant je retrouve mon message, mais l'application est fonctionnelle ??? Vraiment bizarre...
J'veux pas dire de bêtise, mais les champs sont dans MyDocument.xib et le NSArrayControler est lié au tableau initialisé dans MyDocument.m. Cela peut expliquer lasynchro avec "MyDocument", non ?
Bon j'continue de creuser...
Le message d'erreur t'indiques qu'il cherche Titre dans MyDocument. Ce n'est pas possible. Titre ne peut-être qu'un champ d'un élément du tableau reférencé par MyDocument.
@+