Core Data et les observers, un casse tête

P^3P^3 Membre
22:38 modifié dans API AppKit #1
Bonjour,

Je suis relativement débutant en programmation cocoa et malgré la lecture du livre d'AAron Hillegass, de la doc  Apple et de différents posts sur différents forums dans différentes langues, je n'arrive pas à  résoudre mon problème.

J'essaie de programmer une application core data. Je veux à  partir des données faire des statistiques. Un des calculs consiste à  regarder la valeur max en table, la valeur min et à  faire la moyenne en fonction du nombre de lignes en tables. Mon application est un NSTableView lié à  un NSArrayController lié lui meme à  une structure core data. Il y a un bouton ajout et un supp liés au NSArrayController.

Mon application fonctionne correctement pour tout ce qui est gestion des données (édition, ajout, supp, stockage). J'ai même compris comment hériter d'un NSManagedObject pour initialiser des valeurs non standard (date par ex). J'ai également intégrer le système de fetch et de predicates. Ce que je n'arrive pas à  faire c'est à  déclencher mon calcul à  chaque fois que les données sont modifiées. J'ai appris qu'il fallait utiliser un observer. Je l'ai appliqué au code ci dessous :


/////////////EntityRelev.h
#import <Cocoa/Cocoa.h>

//j'utilise un objet personnalisé pour pouvoir initialiser les valeurs comme je l'entends
@interface EntityRelev : NSManagedObject {
}

@end

////////////EntityRelev.m
#import "EntityRelev.h"

@implementation EntityRelev

//Ajoute un observer sur l'object de telle façon que mon calcul sera déclenché quand je modifierai, ajouterai, supprimerai une ligne
- (void) awakeFromEverything
{
  [self addObserver:self forKeyPath:@kmReleve options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:NULL];
}

- (void) awakeFromInsert
{
  //codes pour les valeurs par défaut
...
  //ajoute un observer sur le nouvel objet
  [self awakeFromEverything];
}

- (void) awakeFromFetch
{
  //ajoute un observer pour les données déjà  stockées en "base"
  [self awakeFromEverything];
}

- (void) didTurnIntoFault
{
  //ici je mets le code pour supprimer l'observer d'une ligne qui va l'être par l'action du bouton Suppr
...
}

- (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionnary *)change context:(void *)context
{
  //Ici doivent se faire mes calculs mais à  la place je mets un petit commentaire
  NSLog(@Fais ce que tu as à  faire !);
}

@end


Quand j'édite des données déjà  enregistrées -> Le commentaire s'affiche dans la fenêtre de log -> ok
Quand j'ajoute de nouveaux objects puis les édite -> Le commentaire s'affiche dans la fenêtre de log -> ok
Quand je supprime un des objets que je viens de créer (avant sauvegarde) -> Le commentaire s'affiche dans la fenêtre de log -> ok
Quand je supprime un objet déjà  sauvegardé -> Le commentaire NE s'affiche PAS dans la fenêtre de log-> Probleme :-(

D'après ce que j'ai lu il y a un truc autour du "managedContext" et des "Faults" mais je n'arrive pas à  comprendre comment résoudre proprepement mon problème. Dois je ajouter quelque chose à  ma classe ? N'ai je pas compris quelque chose ?

Quelqu'un pourrait-il m'aider ?

D'avance merci
Xavier

Réponses

  • mars 2007 modifié #2
    Cette page devrait répondre à  ta question initiale (= la moyenne, le min et le max).
  • P^3P^3 Membre
    22:38 modifié #3
    Merci pour la réponse,

    en fait mon problème ne vient pas du calcul que je souhaite faire. J'ai appris à  utiliser le key coding et les "opérateurs min, max, sum, avg... De plus ce calcul n'est qu'un "prétexte" à  ma compréhension de core data. De manière générique je souhaite déclencher une opération chaque fois que mes données sont affectées (un peu comme si j'avais un événement onafterinsert, onafterupdate et onafterdelete). Mon problème se situe donc au niveau de l'utilisation de l'observer. Il existe un cas (quand on supprime un objet qui est chargé depuis le store) où l'observer ne se déclenche pas. J'aimerais comprendre pourquoi et savoir comment compenser cela. Je ne suis même pas certain de regarder dans la bonne direction donc si quelqu'un pouvait m'aiguiller...

    merci
  • 22:38 modifié #4
    OK, le plus simple dans ce cas serait de s'intéresser aux notifications envoyées par le NSManagedObjectContext, et en particulier celle-ci: NSManagedObjectContextObjectsDidChangeNotification.

    PS: j'oubliais une note positive: j'ai vraiment apprécié la qualité de ta recherche dans la question inititiale et la courtoisie dans la seconde. Bienvenue donc!
  • Eddy58Eddy58 Membre
    22:38 modifié #5
    dans 1173805972:

    PS: j'oubliais une note positive: j'ai vraiment apprécié la qualité de ta recherche dans la question inititiale et la courtoisie dans la seconde. Bienvenue donc!


    Tu as surtout oublié de demander la tournée générale des nouveaux ! ;) ;D

    Bienvenue à  toi P^3, qu'est-ce que tu nous offres ?? :p :p
  • P^3P^3 Membre
    22:38 modifié #6
    Merci pour cet accueil

    @Eddy58 : tu serais bien triste car je ne bois pas (question de goûts :-)). Je peux néanmoins t'offrir un bon verre de lait. Le lait de l'amitié ! Miam :-)

    @Renaud : j'avais exploré cette piste avant de m'en remettre aux observers tels que présentés dans mon premier post. J'utilisais l'exemple observé dans le code iClass (exemples livrés avec XCode) :

    NSNotificationCenter defaultCenter] addObserver: self selector: @selector(contextDidChange:) name: NSManagedObjectContextObjectsDidChangeNotification object: [self managedObjectContext;

    mais je n'ai pas été séduit par la formule (même si elle fonctionne). Il me manque peut-être un petit quelque chose :-) En fait ce qui m'a ennuyé c'est que l'événement se déclenche autant de fois qu'il y a de lignes dans la grille (je schématise).

    - (void)contextDidChange:(NSNotification *)aNotification
    {
    NSLog(@Travail);
    }

    En ajout : passe dans contextDidChange autant de fois qu'il y a de lignes (la nouvelle comprise)
    En modif : itoo
    En suppression : passe dans contextDidChange le nombre max qu'il y a eu de lignes dans la grille. Exemple au chargement s'il y en a 3, j'en supprime 2 il passera 3 fois. J'en ajoute 5 et j'en ressuprime 2 il passera 8 fois. Si par contre je sauvegarde il reprend le nombre de lignes "visibles". Rien de plus logique car on travaille avec le ManagedContext qui n'a pas été "fetché" entre temps.

    Ma question (je sais, je suis agaçant parfois) : existe t-il un moyen propre (c'est à  dire différent de compter le nombre de lignes, incrémenter un compteur et ne déclencher mon calcul qu'une fois le compteur ayant atteint le nombre de lignes) pour ne déclencher mon calcul qu'une fois ?

    P.S. Dans l'absolu rien de génant à  le déclencher n fois car il n'est pas lourd mais si un jour il le devenait...

  • 22:38 modifié #7
    Quand tu dis "j'en ajoute 3", tu le fais à  la main ou par code?

    Dans le cas où tu le fais à  la main, c'est normal qu'il exécute le tout "au cas par cas".
  • P^3P^3 Membre
    22:38 modifié #8
    Je fais mes ajouts/suppressions via le binding à  l'ArrayController. Donc à  la main.
    Pour le test dont je parle j'ai remplacé :

    [self addObserver:self forKeyPath:@kmReleve options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:NULL];
    }

    par

    NSNotificationCenter defaultCenter] addObserver: self selector: @selector(contextDidChange:) name: NSManagedObjectContextObjectsDidChangeNotification object: [self managedObjectContext;

    dans ma procédure awakeFromEverything

    J'embrouillerai moins mon monde avec un exemple vie réelle :

    Tu as déjà  utilisé l'application et fait des relevés. Disons que tu as 5 relevés (5 lignes) dans ta grille. Tu veut en ajouter un 6ème. Tu cliques sur le bouton ajout. Il passera alors 6 fois dans l'événement alors que tu ne viens d'ajouter qu'un seul objet.

    Tu te rends compte que ton 3ème relevé est faux. Tu le corriges. Il passera alors 6 fois dans l'événement alors que tu n'as modifié que cette ligne la.

    Tu souhaites supprimer un relevé "non significatif". Alors tu supprimes une ligne. Il ne t'en reste que 5. Il passe pourtant 6 fois dans l'événement (je pense que la ligne est flaggée comme à  supprimer sur sauvegarde donc le context a encore les 6 lignes même si une n'est plus visible).

    Ce qui me semble louche est donc le fait de passer n fois dans l'événement.
  • mars 2007 modifié #9
    Première erreur, il n'est pas nécessaire de s'enregistrer auprès du notification center pour chaque objet ajouté dans le managed object context. Tu ne dois le faire qu'une seule fois, et l'observer doit idéalement être un objet extérieur au managed object context.

    Le fait que tu remarques 5 appels lors d'une édition si tu as 5 lignes me semble du coup beaucoup plus normal: chaque objet contenu dans le contexte reçoit la notification indiquant qu'il y a eu changement.
  • P^3P^3 Membre
    22:38 modifié #10
      M'étant contenté de singer ce qui est fait dans Course.m de iClass je pensais bien faire. J'ai cependant suivi ton conseil et ai sorti mon observer du managedobject pour le mettre dans une classe plus "générique". Ca fonctionne ! Pas exactement comme je l'aurais espéré mais ça fonctionne :-)
    Pour ceux qui auraient le même problème que moi voici comment je m'y suis pris :

    - Dans interface builder héritage d'un NSObject, ajout d'outlets vers le NSArrayController qui m'intéressait et vers les champs dans lesquels je veux afficher le résultat de mes calculs. Instantiation de la classe, création des fichiers...
    - Dans XCode, dans le fichier d'implémentation de l'objet que je viens de créer :

    - (void) dealloc
    {
    //on retire l'observer
    [[NSNotificationCenter defaultCenter] removeObserver: self];
    [super dealloc];
    }

    - (void) awakeFromNib
    {
    //On va regarder les actions sur les données (dans le managedObjectContext) et en fonction de ça on fera les traitements
    NSNotificationCenter defaultCenter] addObserver: self selector: @selector(contextDidChange:) name: NSManagedObjectContextObjectsDidChangeNotification object: [mon_NSArrayController managedObjectContext;
    }

    - (void)contextDidChange:(NSNotification *)aNotification
    {
    //trouvé sur http://www.cocoadev.com/index.pl?RearrangeObjects
    NSArray *insertedEntities = [[[aNotification  userInfo] valueForKey:NSInsertedObjectsKey] valueForKeyPath:@entity.name];
    NSArray *updatedEntities  = [[[aNotification userInfo] valueForKey:NSUpdatedObjectsKey] valueForKeyPath:@entity.name];
    NSArray *deletedEntities  = [[[aNotification  userInfo] valueForKey:NSDeletedObjectsKey] valueForKeyPath:@entity.name];

    if ([insertedEntities containsObject:@Mon_Entite] ||
    [updatedEntities containsObject:@Mon_Entite] ||
    [deletedEntities containsObject:@Mon_Entite])
    {       
    NSLog(@calculs);
    }
    }


    Explications :

    - dans le awakeFromNib de l'objet on met l'observer sur le managedObjectContext. On est sur à  ce moment que celui ci est prêt...

    - dans le desalloc on retire proprement l'observer. Pour l'heure il ne sert "à  rien" car quand on desalloc l'objet c'est que l'on quitte l'application mais je compte faire une gestion plus fine des objets et ne charger que ceux qui sont nécessaires au moment où c'est nécessaire

    - dans le contextDidChange j'ai du aménager quelques tests. Ma classe est chargée comme toutes les classes de l'application, au démarrage de celle ci. Cela veut dire que mon observer sera actif même si je n'en ai pas besoin. De plus, par sa nature, l'observer regarde tout de managedObjectContext. Cela implique que si je travaille sur une autre entité, l'observer déclenchera également un événement. Je met donc des conditions aux traitements que je souhaite faire : je distingue dans un premier temps les cas ajout, suppression, modification. J'en profite pour récupérer l'entité pour laquelle l'événement s'est déclenché. Si mon entité correspond à  celle qui m'intéresse => je fais les calculs.

    Je remercie Renaud pour son aide précieuse ainsi que ceux qui auraient voulu m'aider mais ne le pouvaient pas.

    A++
  • AliGatorAliGator Membre, Modérateur
    22:38 modifié #11
    dans 1173873978:
    Je remercie Renaud pour son aide précieuse ainsi que ceux qui auraient voulu m'aider mais ne le pouvaient pas.
    Et moi j'applaudis des 2 mains pour ton attitude exemplaire sur les forums :
    - poli et tout (même avec ceux qui ne t'ont pas répondu, tu fais très fort :D)
    - question claire et infos utiles mentionnées dans le post
    - tu montres que tu as cherché avant de poser la question et les pistes que tu as explorées
    - tu indiques la solution une fois que tu l'as trouvée pour que ça serve aux autres
    - ...

    Et comme on ne vois pas ça assez souvent sur les forums, ça mérite d'être signalé  ;)

    Allez, pour le coup c'est moi qui l'offre la tournée générale  :p :p (y'a du jus de fruits, t'inquiète) :)


    PS : allez, le petit plus pour parfaire tes futurs posts : pense à  utiliser les balises [code]...[/code] pour entourer le code source que tu mets dans tes posts (c'est le bouton "#" quand tu rédiges un post), ça le fait ressortir plus clairement et dans une police adaptée :)
  • mars 2007 modifié #12
    dans 1173873978:

    Je remercie Renaud pour son aide précieuse ainsi que ceux qui auraient voulu m'aider mais ne le pouvaient pas.


    C'est moi qui te remercie pour ton "comportement": normalement si je vois "débutant" et "CoreData" dans une même question j'ignore, mais là  ta question comportait tous les éléments que j'attends lorsque je vois une question:
    - référence (même succinte) à  des docs extérieures
    - explication pratique du problème
    - explication "code" de ta manière de le résoudre
    - sélection pertinente des trucs qui ne vont pas (certains se contentent de taper les runlogs, ou à  l'inverse des "ça ne marche pas")
    - les formules de politesse
    - l'orthographe.
    Donc je ne pouvais pas laisser une question comme ça sans réponse.

    Tes réponses qui ont suivi ont toujours été conformes à  ces points, et en plus pour couronner le tout tu fais un résumé à  la fin. Les messages que tu mentionnais étaient en plus suffisament précis que pour pouvoir répondre de mémoire (ou après une recherche très brève), ce que j'ai apprécié aussi.

    Comportement exemplaire donc, et c'est suffisament rare que pour être mentionné.

    Juste une petite remarque sur le code de cocoadev. Il est correct, mais je ne l'aime pas: au nom de la lisibilité du code, on sacrifie les performances: si l'entité est déjà  présente dans les insertedEntities, calculer les updatedEntities et deletedEntities a été inutile car l'opération qui suit est tout ce qu'il y a de plus générique. En mettant les messages qui permettent d'obtenir la liste des entités directement dans les conditions, ce genre de calcul peut être évité. Mais c'est moins joli (à  moins que tu ne fasses une macro).

    [tt]if ([[[[aNotification  userInfo] valueForKey:NSUpdatedObjectsKey] valueForKeyPath:@entity.name] containsObject:@Mon_Entite] ||
            [[[[aNotification userInfo] valueForKey: NSInsertedObjectsKey] valueForKeyPath:@entity.name] containsObject:@Mon_Entite] ||
            [[[[aNotification  userInfo] valueForKey:NSDeletedObjectsKey] valueForKeyPath:@entity.name] containsObject:@Mon_Entite])
      {        
         NSLog(@calculs);
      }[/tt]

    Tu remarqueras aussi que j'ai mis NSUpdatedObjectsKey en début de liste. Ce n'est pas pour rien: il y a de fortes chances que ce soit cette opération qui soit le plus souvent effectuée, donc on évite de cette manière des calculs qui pourraient être inutiles parce que inserted a été mis avant.
  • cargocargo Membre
    mars 2007 modifié #13
    Soit je n'ai pas exactement compris ce que tu veux faire, soit tu te compliques la vie pour rien.
    Dans le doute voilà  des éléments de réponses qui peuvent servir à  d'autres aussi.

    1- Pour générer une somme, valeur max/min ou moyenne, etc :
    Crée un champ texte dans Interface Builder et Bind le comme ceci :
    Value
    Bind to : tonArrayController
    Controller Key : arrangedObjects
    Model Key Path : @sum.tonAttributDeTonEntitéCoreData (par exemple : montantDeLaFacture)
    De même avec @avg, @max, etc. Tu peux même parcourir les relationships pour aller chercher la moyenne des objets liés à  ton objet en question (par exemple : la moyenne des montants de factures du vendeur untel).
    L'update des valeurs se fait en temps réel.

    2- Pour générer tout autre calcul qui ne découle pas directement du KVC :
    Ajoute un attribut à  ton entité CoreData, définis le comme "transient" en cochant la case correspondante.
    Crée les fichiers TonEntité.h TonEntité.m tel que c'est expliqué dans le tutorial Apple.
    Code le calcul de ton attribut, par exemple:
    <br />-(NSNumber*)tonNombre3<br />{<br />	return [NSNumber numberWithInt:[[self valueForKey:@&quot;tonNombre1&quot;] intValue]-[[self valueForKey:@&quot;tonNombre2&quot;] intValue]]; <br />}<br />
    


    Ajoute la fonction suivante à  TonEntité.m pour un update du calcul en temps réel :
    <br />+ (void)initialize<br />{<br />&nbsp; &nbsp; if (self == [TonEntité class])<br />&nbsp; &nbsp; {<br />		NSArray *tesValeursQuiSontALaBaseDuCalcul = [NSArray arrayWithObjects:@&quot;tonNombre1&quot;, @&quot;tonNombre2&quot;, nil];<br />&nbsp; &nbsp; &nbsp; &nbsp; [self setKeys: tesValeursQuiSontALaBaseDuCalcul triggerChangeNotificationsForDependentKey:@&quot;tonNombre3&quot;];<br />&nbsp; &nbsp; }<br />}<br />
    



    Quand tu vas ajouter ou supprimer ou modifier quoi que ce soit dans ton managedObjectContext, tes calculs seront mis à  jour automatiquement. C'est justement parce qu'il y a un managedObjectContext que l'on peut se passer d'observers dans ce cas là .
    Le code de cocoadev est destiné aux sortDescriptors, il est logique dans ce cas d'ajouter un observer car le managedObjectContext fetch des NSSet par défaut, il ne gère pas l'ordre.
    Mais ce n'est que mon avis de plus-trop-débutant-mais-pas-encore-expert.... ;)
Connectez-vous ou Inscrivez-vous pour répondre.