Les NSArrayControllers à  la fête !

muqaddarmuqaddar Administrateur
février 2007 modifié dans API AppKit #1
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 :

@interface MyDocument : NSDocument<br />{<br />&nbsp; &nbsp; 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 />&nbsp; &nbsp; self = [super init];<br />&nbsp; &nbsp; if (self) {<br />&nbsp; &nbsp; &nbsp; &nbsp; tableau = [[NSMutableArray alloc] init];<br />&nbsp; &nbsp; }<br />&nbsp; &nbsp; 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 />&nbsp; &nbsp; [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 />&nbsp; &nbsp; 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 />&nbsp; &nbsp; [tableau autorelease];<br />&nbsp; &nbsp; tableau = [[NSMutableArray alloc] initWithContentsOfFile:(NSString*)aPath];<br />&nbsp; &nbsp; 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 .

Réponses

  • muqaddarmuqaddar Administrateur
    13:56 modifié #2
    Surpuissants bindings

    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]
  • muqaddarmuqaddar Administrateur
    13:56 modifié #3
    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.

    [Fichier joint supprimé par l'administrateur]
  • muqaddarmuqaddar Administrateur
    13:56 modifié #4
    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...


    [Fichier joint supprimé par l'administrateur]
  • muqaddarmuqaddar Administrateur
    13:56 modifié #5

    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]
  • muqaddarmuqaddar Administrateur
    13:56 modifié #6
    Quelques remarques :

    # 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 !
  • odjauodjau Membre
    13:56 modifié #7
    Salut tout le monde,

    Je viens de finir le tuto, tout fonctionne, mais à  chaque ouverture/création d'un document j'ai le message suivant dans la console :
    2008-07-14 23:15:33.604 LaboBindings[21738:10b] Cocoa Bindings: Error accessing value for key path selection.Titre of object &lt;NSArrayController: 0x1a240f0&gt;[object class: NSMutableDictionary, number of selected objects: 1] (from bound object &lt;NSTextField: 0x1a3c0f0&gt; with object ID 100114 in Nib named MyDocument.nib): [&lt;MyDocument 0x1ce740&gt; valueForUndefinedKey:]: this class is not key value coding-compliant for the key Titre.<br />2008-07-14 23:15:33.604 LaboBindings[21738:10b] Cocoa Bindings: Error accessing value for key path selection.@sum.Montant of object &lt;NSArrayController: 0x1a240f0&gt;[object class: NSMutableDictionary, number of selected objects: 1] (from bound object &lt;NSTextField: 0x1a4ee0&gt; with object ID 100082 in Nib named MyDocument.nib): [&lt;MyDocument 0x1ce740&gt; valueForUndefinedKey:]: this class is not key value coding-compliant for the key Montant.<br />2008-07-14 23:15:33.606 LaboBindings[21738:10b] Cocoa Bindings: Error accessing value for key path selection.@sum.Montant of object &lt;NSArrayController: 0x1a240f0&gt;[object class: NSMutableDictionary, number of selected objects: 1] (from bound object &lt;NSTextField: 0x1e2b30&gt; with object ID 100075 in Nib named MyDocument.nib): [&lt;MyDocument 0x1ce740&gt; valueForUndefinedKey:]: this class is not key value coding-compliant for the key Montant.<br />2008-07-14 23:15:33.607 LaboBindings[21738:10b] Cocoa Bindings: Error accessing value for key path selection.Auteur of object &lt;NSArrayController: 0x1a240f0&gt;[object class: NSMutableDictionary, number of selected objects: 1] (from bound object &lt;NSTextField: 0x1ee810&gt; with object ID 100116 in Nib named MyDocument.nib): [&lt;MyDocument 0x1ce740&gt; valueForUndefinedKey:]: this class is not key value coding-compliant for the key Auteur.<br />
    


    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

    @+

  • Philippe49Philippe49 Membre
    13:56 modifié #8
    dans 1216070376:

    Cocoa Bindings: Error accessing value for key path selection.Titre of object <NSArrayController: 0x1a240f0>[object class: NSMutableDictionary, number of selected objects: 1] (from bound object <NSTextField: 0x1a3c0f0> with object ID 100114 in Nib named MyDocument.nib): [<MyDocument 0x1ce740> valueForUndefinedKey:]: this class is not key value coding-compliant for the key Titre.



    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.
  • odjauodjau Membre
    13:56 modifié #9
    Merci pour les infos.
    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...

    dans 1216112207:

    il semble bizarre que la synchronisation (le binding) soit faite avec un champ de MyDocument !

    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...
  • Philippe49Philippe49 Membre
    juillet 2008 modifié #10
    dans 1216070376:

    Cocoa Bindings: Error accessing value for key path selection.Titre of object <NSArrayController: 0x1a240f0>[object class: NSMutableDictionary, number of selected objects: 1] (from bound object <NSTextField: 0x1a3c0f0> with object ID 100114 in Nib named MyDocument.nib): [<MyDocument 0x1ce740> valueForUndefinedKey:]: this class is not key value coding-compliant for the key Titre.



    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.
  • odjauodjau Membre
    13:56 modifié #11
    Ok, merci pour l'explication de texte ;) je vais essayer de comprendre d'où ça viens

    @+
Connectez-vous ou Inscrivez-vous pour répondre.