# Bonnes pratiques : Subclassing de NSArrayController ?



## mickadedel (2 Décembre 2009)

Je suis confronté à un soucis de bonnes pratiques, afin d'éviter les galères.
J'ai déjà conçu des petites applis cocoa appliquées notamment à la physique.

Souvent, la création d'un objet nécessite la création automatique d'autres objets. J'utilise en général des NSArrayController qui contiennent des NSMutableDictionary. Comment organiser les classes ? Est-il préférable d'overrider la méthode addid)sender ? Faut-il créer une subclasse de NSDictionary avec une init personnelle qui créée les autres objets dont j'ai besoin ?

Est-ce qu'un développeur averti pourrait m'indiquer les bonnes pratiques ?


----------



## Céroce (3 Décembre 2009)

Il est *extrêmement* rare de sous-classer NSArrayController. L'ayant moi-même tenté, je t'invite à ne pas le faire.
En général, Cocoa privilégie la composition à la dérivation. À chaque fois que tu veux dériver une classes standard, assure-toi dans la doc d'Apple que c'est bien la bonne manière. Habituellement, il existe d'autres méthodes telles que la délégation, qui conviennent mieux.

Dans l'exemple qui suit, imagine que tu veuilles gérer une liste de clients. Je suis parti sur une appli basée sur les documents.

1) Définis la classe Client


```
@interface Client:NSObject
{
	NSString* nom;
	NSString* prenom;	
}
```


2) La liste des clients est une variable d'instance du document


```
@interface MonDocument: NSDocument
{
	NSMutableArray* clients;	// Liste de Clients
```

3) Dans MyDocument.xib, binde le NSArrayController comme suit:
Content Array
Bind to [File's Owner]
Model Key Path: document.clients

Ce binding est possible car File's Owner correspond au NSWindowController de la fenêtre du document. NSWindowController possède une méthode -[document].

4) Toujours sur le NSArrayController, première rubrique de l'inspecteur (Attributes) > Object Controller
Mode: Class.
Class Name: Client

Cela dit au NSArrayController que les objets qu'il crée par l'action add: sont des Clients.

5) Pour finir, relie les actions add: et remove: du NSArrayController.


Je ne pense pas avoir oublié d'étape. Avec l'expérience, tout cela te semblera relativement logique.


----------



## mickadedel (4 Décembre 2009)

Bonjour,

Merci Céroce pour ta réponse. J'ai quand même encore un soucis : comment lancer la création automatique d'autres objets ? Je m'explique : si je crée une nouvelle "Variable", je dois alors créer autant de valeur qu'il y a de "Mesures". De même, si je crée une nouvelle "Mesure", je dois créer autant de valeurs qu'il y a de "Variables". Mêmes soucis à la suppression.

Aurais-tu une idée de la structure de classe à créer ? C'est encore mon soucis aujourd'hui : je ne visualise pas bien les structures à créer. J'ai tendance à trop bricoler les controleurs je crois.


----------



## Céroce (4 Décembre 2009)

Je t'avoue que je n'ai pas une vision assez claire de ton projet pour pouvoir te conseiller.
Il va falloir détailler davantage ton projet.
(Si tu ne veux pas en discuter en public, envoie-moi un message privé).

Commence par expliquer par ce que tu entends par Mesure et Variable et comment tu verrais l'interface utilisateur.


----------



## mickadedel (5 Décembre 2009)

Bonjour,

Pour ce projet, j'ai donc un tableau contenant 2 TableColumns, bindées à des NSMutableDictionary, qui sont pour moi des "Variables". 2 clefs : nom et unité. J'ai été obligé d'en créer une 3ème que j'ai appelée ID, un int que j'incrémente à chaque ajout d'une variable. J'ai été obligé pour une raison obscure : si je crée 2 variables avec le même nom et la même unité, alors le NSDictionary est le même ! Si j'ajoute une clef ID, chaque dictionnaire est unique. Pourquoi à la création 
	
	



```
[NSDictionary dictionaryWithObjectsAndKeys:@"NomVar",@"nom",@"UnitVar",@"Unit",nil]
```
 le résultat est le même dictionnaire après 2 créations successives ? Pourquoi n'est-ce plus le cas si je change le nom de la variable avant de créer la 2ème ?

Un 2ème tableau contient autant de tablecolumns qu'il y a de variables. Je peux ajouter alors des "Mesures" qui sont des NSMutableDictionary avec une entrée par variable (la clef de l'entrée). Du coup il faut créer une entrée nouvelle dans chaque Mesures à la création d'une nouvelle variable. (de même à la suppression). Cette table est gérée par une datasource, contrairement à la première qui est gérée par les bindings.

2 NSarrayControllers sont liés à 2 NSMutablesArray dans MyDocument.
J'ai créé une subclass de NSObject avec des outlets pour communiquer avec les arrayControllers dans IB, doté des méthodes "add:" et "remove:" dans lesquels je crée les nouveaux dictionnaires, que je mets dans les tableaux en envoyant le message addObject: au NSarrayController's appropriés. Pour l'instant cela semble fonctionner.

Il me reste les remove à implémenter, et l'enregistrement dans un fichier. A ce propos, j'ai un peu de mal avec les méthodes à implémenter dans MyDocument. Si quelqu'un est au point là-dessus...

Voilà un peu la structure actuelle. Est-elle dans les règles de l'art ...


----------



## Céroce (6 Décembre 2009)

mickadedel a dit:


> Pourquoi à la création
> 
> 
> 
> ...


C'est une astuce de Cocoa pour économiser de la mémoire. Un NSDictionary n'étant pas modifiable, si deux dictionnaires contiennent exactement les mêmes clés et les mêmes valeurs, alors il est inutile de réserver de la mémoire pour le second: il suffit d'envoyer un -[retain] au premier dictionnaire et de renvoyer le même pointeur.

À mon avis, les dictionnaires ne répondent pas à trop à ton besoin. Voici comment je ferais.

Les données doivent faire partie du document pour te permettre d'enregistrer et de charger les données.

```
@interface MyDocument: NSDocument
{
	NSMutableArray*	variables;	// Liste de MIVariables
}
@end
```

N'oublie pas de créer le NSMutableArray dans la méthode d'init du document.



```
@interface	MIVariable: NSObject
{
	NSString*	nom;
	NSString*	unite;
	NSMutableArray*	mesures;	// Liste de MIMesures
}
@end
```

Une Variable a un nom et une unité, et une liste de Mesures.
N'oublie pas de créer le NSMutableArray dans la méthode d'init de MIVariable.



```
@interface MIMesure: NSObject
{
	double	temps;
	double	valeur;		
}
@end
```

Ensuite MyDocument.xib va comporter deux NSArrayController.
Le premier, "Variables" a son Content Array bindé sur [File's Owner].document.variables

Le second, "Mesures" a son Content Array bindé sur [Variables].[selection].mesures
=> Il contient donc la liste des mesures de la Variable actuellement sélectionnée dans le NSArrayController "Variables".

La première NSTableView présente les Variables.
Sa première colonne est bindée sur [Variables].nom
La seconde sur [Variables].unite.

La seconde NSTableView présente les Mesures.
Sa première colonne est bindée sur [Mesures].temps
La seconde sur [Mesures].valeur.


Je conçois que ceci ne répond pas totalement à ta vision, puisque les mesures sont affichées en lignes, et uniquement pour la variable actuelle, mais c'est beaucoup plus simple !
On pourrait envisager d'avoir une seule table avec des colonnes
temps | var1 | var2 | var3 | var4
Mais c'est plus compliqué, et implique que ce soit la Mesure qui contienne une liste de Variables. La NSTableView est conçue pour lister son contenu verticalement.

Pour l'enregistrement, le plus simple est d'utiliser les classes NSKeyArchiver/Unarchiver.
Je te conseille la lecture de l'incontournable Programmation Cocoa sous Mac OS X qui en cause largement.


----------



## mickadedel (7 Décembre 2009)

Bonjour à tous,

J'ai utilisé les dictionary car on peut utiliser un objet pour clef d'entrée. => Du coup les dictionnaires "mesures" on autant d'entrée qu'il y a de variables, les variables étant les clefs.
A la création d'une variable, je crée une colonne dont l'identifier est la variable (donc un nsmutableDictionary avec les cléf nom et unité).

Ma table de mesure est connectée à une datasource, et je retrouve rapidement mes petits puisque via l'identifier de colonne, j'ai acces à la variable en jeu. Cela fonctionne bien a priori. Il faut que je teste en enregistrant le fichier de données.

Avec ce que Céroce propose, je tombe dans le patern "Master-Details" qui ne me convient pas. Il me faut de la "double entrée" => 2 clefs me donnent accès à une valeur. Le Framework Cocoa ne propose pas d'outils simples pour gérer cette structure. Elle est hyper orientée masterDetail (base de données quoi). Mais bon, avec un peu de code ça se fait...

Je ne savais pas que les constructeurs tels que arrayWith, ou dictionaryWith ne créait pas toujours un nouvel objet. Il est vrai que cela m'a surpris : lorsque je renommais les variables avec le même nom, la chaîne de caractères est en fait le même objet, et le dictionnaire aussi !! C'est méga dangereux ça ! :mouais: Du coup je me galère à gérer un ID sur mes dictionnaires de variables afin de m'assurer qu'elles soient bien uniques. C'est un peu galère cette histoire.  Je vais essayer sans le constructeur de complaisance mais avec alloc init pour voir.


----------



## mickadedel (7 Décembre 2009)

Bonsoir,

Un truc que je ne m'explique pas : je crée un MutableDictionary. Je lui attribue 2 entrées (clef @"nom" valeur:une NSString, et @"unite" valeur:uneString). 

Je crée un MutableDictionnaire Mesure qui contient une entrée : clef => un dictionnaire décrit précédemment. La valeur : un NSNumber.
LE PROBLEME : SI JE CHANGE LA VALEUR D'UNE CLEF Nom ou Unite, ALORS MON DICO SEMBLE CHANGER D'ADRESSE, ET JE N'Y AI PLUS ACCES VIA LE 2EME DICO.. PEROIQ?


----------



## Céroce (8 Décembre 2009)

Donne un exemple, parce qu'il n'y a aucune raison que le dictionnaire change d'adresse, à moins que tu n'en n'aies créé un nouveau.


----------



## mickadedel (10 Décembre 2009)

Mes dicos sont bien des dicos différents. Le soucis, c'est que dans un dico, la clef n'est pas vraiment un pointeur vers un objet, mais une "valeur" => Donc si je mets en clef un dico1 pour une entrée, et un dico2 (donc adresse différente) identique au sens "isequal" de cocoa alors lors de l'envoi du message [leDico objectForKey:dico1] ou [leDico objectForKey:dico2] donne la même valeur, car dico1 et dico2 sont "equal", même s'ils ne sont pas les mêmes !!

En fait, il suffisait de lire la doc sur collection programming guide pour voir que les clefs sont des copies des objets, et que ce n'est pas l'objet lui-même qui est la clef, mais sont contenu qui est comparé au contenu de l'objet passé dans le message [dico objectForKey:.....].

En tenant compte de ce "petit" détail, j'ai donc utilisé comme clef ma numérotation "maison" des variables que je gère donc manuellement afin d'avoir des clefs uniques d'identification des variables. En effet, si à la création d'une mesure la variable s'appelait "x", et son unité était le "m", ce dictionnaire était copié comme clé, qui me servait à retrouver mes petits. Mais si je changeait ou le nom, ou l'unité la clé n'était plus "la même" au sens "isequal" et je perdais mes données.

Question : Puis-je utiliser comme clé l'ADRESSE en mémoire d'un objet ? (qui normalement ne change pas et est unique) Je ne me souvient plus de l'opérateur qui le permet. Quel sera alors le TYPE de variable de cette adresse ?


----------



## Céroce (11 Décembre 2009)

mickadedel a dit:


> Mes dicos sont bien des dicos différents. Le soucis, c'est que dans un dico, la clef n'est pas vraiment un pointeur vers un objet, mais une "valeur" => Donc si je mets en clef un dico1 pour une entrée, et un dico2 (donc adresse différente) identique au sens "isequal" de cocoa alors lors de l'envoi du message [leDico objectForKey:dico1] ou [leDico objectForKey:dico2] donne la même valeur, car dico1 et dico2 sont "equal", même s'ils ne sont pas les mêmes !!


Oui, les dictionnaires sont implémentés sous la forme de tables de hâchage. La méthode -[isEqual] est effectivement utilisée pour savoir si le dictionnaire contient déjà la clé; cela dit, c'est bien le comportement qu'on attend d'un dictionnaire. En général, la clé est une NSString, et ce qui compte pour changer la valeur est que le texte soit identique, pas que la clé soit le même objet.



mickadedel a dit:


> Question : Puis-je utiliser comme clé l'ADRESSE en mémoire d'un objet ? (qui normalement ne change pas et est unique) Je ne me souvient plus de l'opérateur qui le permet. Quel sera alors le TYPE de variable de cette adresse ?


Tu pourrais utiliser un NSNumber pour encapsuler l'adresse, mais tu serais sur la bonne voie pour rendre ton code complètement illisible. À mon avis, la meilleure méthode reste l'utilisation d'un NSMutableArray contenant des objets simples qui lient mesure et variable.


----------



## mickadedel (11 Décembre 2009)

Bonjour,

Pour gérer l'ajout et la suppression de données, j'ai 2 boutons (+ et -).
J'aimerais plutot que lorsque l'utilisateur a terminé l'EDITION de la dernière cellule (dernière row et dernière tableColumn), il y ait création automatique d'une nouvelle "mesure", donc une nouvelle row, et que le focus soit sur la nouvelle ligne fraichement créée.

J'ai tenté le truc en connectant dans IB la table avec une action add: que j'ai implémentée, en testant si l'index de la selection était le dernier. Le soucis, c'est que si je ne fait que cliquer, il y a création correctement, et la nouvelle ligne est sélectionnée. C'est un peu chiant. Par contre, si je termine l'édition de la dernière case et que j'appuye sur entrée, ou TAB, la nouvvelle ligne est bien créée, mais le focus est donnée à la toute première ligne malgré mon 
	
	



```
[table selectRow: byExtendingSelection:]
```

Je pense donc ne pas bien saisir les subtilités entre les SELECTION et les FOCUS (=FirstResponder ?). Si quelqu'un peut m'éclairer là-dessus, ce serait top...

De même, comment utiliser la touche "retour arrière" afin que sa pression entraîne une action*?


----------

