[Résolu] Une fenêtre style "Inspecteur" avec des documents Core Data. Comment faire?

berfisberfis Membre
mai 2013 modifié dans API AppKit #1

Bonsoir à  tous,


 


J'ai un problème de design, et c'est lié aux ManagedObjectContexts. Dans une application multi-documents Core Data, j'ai actuellement une fenêtre "Inspecteur" liée (childWindow) à  la fenêtre principale de mon document. Elle liste les entités du documents. L'ennui, c'est que si on ouvre n documents, on se retrouve avec n*2 fenêtres sur l'écran. Je trouve ça moche.


 


Mon but est d'ouvrir, comme Pages par exemple, une seule fenêtre Inspecteur au niveau de l'application, de façon qu'on ait n+1 fenêtres ouvertes, ce qui me paraà®t plus élégant. De la même manière, je me retrouve avec une seule série de contrôleurs aussi, au lieu d'en avoir n séries.


 


Mais voilà , c'est encore un truc que Core Data n'a pas vraiment prévu. Comment faire?


 


Ma première idée était de stocker, au niveau de l'appDelegate, une @propriété NSManagedObjectContext *currentContext. C'est dans ce contexte que les contrôleurs viennent chercher leur contenu. Lorsqu'un document est activé et passe au premier plan, il transmet son contexte au currentContext de l'application.


 


Mais cela ne marche pas pour la raison suivante: que faire lorsque ce currentContext est indéfini? Par exemple:


- au lancement de l'application;


- quand le document actif est fermé et qu'aucun autre ne prend le relais.


 


Le contexte devient à  ce moment... un ticket pour la Twilight Zone. Le document qui se désactive peut bien envoyer NIL au currentContext, mais Core Data ne va pas aimer du tout.


 


Alors quoi? Définir un "contexte vide" auquel l'application revient par défaut? Et comment faire? J'aimerais naturellement que lorsque le dernier document est fermé, les contrôleurs vident leurs tableViews, désactivent les boutons, etc. Quelque chose dans ce style, dans le awakeFromNib de l'appDelegate:



- (void) awakeFromNib
{
NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@Document withExtension:@momd];
NSManagedObjectModel *mom = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
NSPersistentStoreCoordinator *psc = [[NSPersistentStoreCoordinator alloc]initWithManagedObjectModel:mom];
self.scratchContext = [[NSManagedObjectContext alloc]initWithConcurrencyType:0];
[self.scratchContext setPersistentStoreCoordinator:psc];
[self setScratch];
}

Et bien sûr, histoire de rendre la chose piquante, que quasiment tout se gère par bindings (Core Data et les bindings sont, avec les contrôleurs, mes coqueluches, vous l'aviez déjà  compris).


 


Je sollicite ici le conseil avisé des Grands Anciens: est-ce une chimère que je poursuis? Après tout, mes documents bi-fenêtres fonctionnent comme des horloges (suisses en plus). Ai-je tort de courir après quelque chose qui va me rendre la vie trop compliquée pour un bénéfice que mes utilisateurs risquent de ne même pas remarquer (règle des 80/20)?


 


Merci de lever mes doutes.


Bernard


Réponses

  • laudemalaudema Membre
    mai 2013 modifié #2

    Bonjour,


    Pas de la part d'un grand ancien mais bon, ça ira peut être:


    Faire un singleton (c'en est un si je ne m'abuse ;)),  un "sharedInspecteurController" qui contrôlera ta fenêtre inspecteur. Un peu "à la" fenêtre des préférences de beaucoup d'applications, bien décrite dans le livre d'A Hillegass et certainement ailleurs.


    Un appel à  ce singleton quand un document est affiché au premier plan (notification NSWindowDidBecomeMainNotification ou son delegate éponyme) et mise à  jour des données de l'inspecteur si données il y a ou mise à  "nil" en cas de NSWindowWillCloseNotification ou peut être  NSWindowDidResignMainNotification. A toi de tester ce qui va le mieux.


     


    Par bindings tu peux aussi utiliser le chemin Application->MainWindow pour retrouver le document de ta mainWindow : Application.mainWindow.windowController.document.


    Google aurait pu être ton ami, la recherche de cocoa main window document bindings me donne en premier ceci qui me paraà®t très intéressant dans ton cas ( plus un brin de muguet du 1er mai  ;) ). 


    Et si tu utilises les bindings un NSViewController serait peut être plus adapté grace à  sa property representedObject


  • berfisberfis Membre
    mai 2013 modifié #3

    Bonjour laudema, et merci pour le muguet!


    Bah j'ai bien essayé de googler, mais visiblement pas avec les bons mots-clés... Le lien que tu proposes est effectivement intéressant, il me montre en tout cas que je ne suis pas le seul à  tenter de réaliser cette idée. Actuellement j'emploie les notifications windowDidBecomeMain et windowDidResignMain du document, qui sont appelées au bon moment et comme je suis dans le document j'ai accès directement à  ses variables.


     


    ça ne résout pas mon problème: comment changer le contexte de Core Data? Ce petit monstre refuse de sauver le contexte en mémoire, cela veut dire que lorsque je "switche" du doc A au doc B, le contenu modifié de doc A est perdu -- plus exactement la partie modifiée sur la fenêtre du document... car tout ne se passe pas dans mon "inspecteur".


     


    Je me bats donc actuellement avec les "bind" / "unbind" dans la fenêtre principale. Imaginons la chose suivante:


    l'inspecteur affiche la liste des enregistrements. La fenêtre principale a des champs "Nom" et "Photo". Je crée un nouvel enregistrement dans l'inspecteur. Ca marche. Je vais dans la fenêtre du document, je tape le nom et colle la photo. Je crée un nouvel enregistrement. Exit mes valeurs précédentes.


     


    Je ne suis pas sûr que cela provienne du changement de contexte. En fait, cela pourrait venir des bindings. Voici mes routines de switching:



    - (void) windowDidBecomeMain: (NSNotification *) notification
    {
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.0), dispatch_get_main_queue(), ^(void){
    _appDelegate.context =self.managedObjectContext;
    [_image bind:@value toObject:self withKeyPath:@appDelegate.controller.selection.image options:nil];
    [_text bind:@attributedString toObject:self withKeyPath:@appDelegate.controller.selection.text options:nil];
    [[_appDelegate controller]fetchWithRequest:nil merge:NO error:nil];
    [[_appDelegate controller]setSelectionIndex:_currentSelection];
    });

    }

    - (void)windowDidResignMain:(NSNotification *)notification
    {
    _currentSelection = [[self.appDelegate controller]selectionIndex];
    [_image unbind:@value];
    [_text unbind:@attributedString];
    [_appDelegate setScratch] ; // une méthode du delegate qui rétablit son "contexte bidon"


    Où peut se nicher l'erreur? Je continue à  tâtonner.


  • yoannyoann Membre

    Je n'ai jamais essayé de faire ce que tu cherche ici, par contre ce qui est certain c'est que CoreData n'est pas la source du problème. ça serait la même avec n'importe quel backend.


     


    Cherche comment gérer les shared inspector window avec NSDocument, comment est-ce que t'es censé changer de contexte. Perso j'utiliserais des notifications. Ensuite c'est juste une question d'accès à  ton contexte depuis son document et de changement de la source de tes arraycontroller.


  • CéroceCéroce Membre, Modérateur
    mai 2013 modifié #5

    Je ne comprends pas bien. As-tu bien un Manage Object Context par document ?


    Dans ce cas, tu as aussi un NSArrayController par document que est bindé sur le MOC.


     


    C'est ensuite que ça demande un peu d'astuce pour comprendre la hiérarchie: on peut binder sur application.delegate.mainWindow.windowController.document.arrayController


  • Bonjour,


     


    J'ai été confronté au même problème.


    Dans un premier temps j'ai opté pour autant d'inspecteur que de document en gérant leur apparition et leur position au même endroit ce qui simulait un inspecteur unique mais a fini par poser beaucoup de problèmes d'interface.


     


    Maintenant j'ai un seul inspecteur avec son NSManagedContext et ses NSArrayController (qui utilise de NSManagedContext de l'inspecteur). J'utilise les notifications de fenêtre pour assigner le contexte de l'inspecteur à  celui du document en cours.


     


    Dans le cas où je n'ai plus de document ouvert je gère une instance du délégué de l'application indiquant qu'il n'y a pas de document ouvert et par binding dessus je rend invisible tout mes éléments de l'inspecteur (en fait la vue principale).


     


    Comme le signale Berfis j'ai plusieurs warning au départ de l'application sans document à  l'ouverture du genre : 


    Cannot perform operation since managed object context has no persistent store coordinator,


    mais cela ne pose pas de problème de fonctionnement.


     


    À noter que j'aurais pu utiliser les bindings via Application.mainWindow.windowController.Document mais j'utilise des NSTableView avec leur NSController dans l'inspecteur et d'autres bricoles qui compliquent beaucoup les choses...

  • CéroceCéroce Membre, Modérateur

    Moi aussi, j'avais essayé les notifications au début. Mais j'ai le souvenir que ça posait de nombreux problèmes parce que les notifications n'étaient pas toujours émises, par exemple quand l'application passait à  l'arrière plan. Désolé de n'être pas plus précis, mais ça remonte à  quatre ans !


  • berfisberfis Membre

    Merci à  tous, visiblement nous utilisons les mêmes ficelles:


     


    @yoann : regarde mon code, j'utilise effectivement les notations, cela ne pose pas de problème;


    @ceroce : regarde mon code, j'utilise effectivement un "path" pour connecter ma tableView;


    @fleurentin :j'ai bien sûr pensé au "faux inspecteur unique" par superposition (ça c'est du design !  :lol: ) et c'est pour en finir avec l'erreur que tu signales que j'ai eu l'idée de mon "scratch context" pour l'application.


     


    OK, d'un point de vue design, je pense que filer le contexte du document à  l'application est correct et je ne vois pas en quoi CD rouspéterait. Je pense à  des bindings mal faits.


  • Re,


     


    Berfis, Ce n'est pas nécessaire de gérer un context bidon lors de la désactivation et je pense que c'est ce qui est à  l'origine de ton problème.


     


    Mets le à  nil.


     


    Pour ce qui est de la gestion des NSTableView de l'inspecteur utilise des NSArrayController DANS l'inspecteur fais les pointer sur un objet NSManagedContext commun DANS l'inspecteur.


     


    Ensuite utilise les notifications d'activation de fenêtre pour assigner le NSManagedContext de ton Document à  celui de l'inspecteur.


    Toutes les opérations faites sur l'inspecteur serons faites dans le contexte de ton document.


     


    Bien sûr dans ce cas là  il te faudra accéder au données VIA les NSArrayController de l'inspecteur dont tu auras pris soin de créer une propriété dans ton délégué de l'application.


     


    J'espère avoir été clair...

  • berfisberfis Membre
    mai 2013 modifié #10

    Euh... fleurentin, c'est exactement ce que j'ai fait. Ce qui me fait dire que j'ai uniquement un problème de bindings.


    Pourriez-vous voir ce qui cloche dans mes lignes "bind" et "unbind" ci-dessus, étant donné que j'ai les propriétés suivantes:



    @interface NSAppDelegate : NSObject
    @property IBOutlet NSArrayController *controller;
    @property NSManagedObjectContext *context, *scratchContext;
    @end


    @interface Document : NSPersistentDocument
    @property NSAppDelegate *appDelegate;
    @property IBOutlet NSImageView *image;
    @property IBOutlet NSWindow *mainWindow;
    @property IBOutlet NSTextView *text;
    @property NSUInteger currentSelection;
    @end

    Les bindings semblent ne pas se défaire lorsque le document est désactivé pour l'image, alors que le texte fonctionne. J'ai vraiment la tête dans le brouillard ce matin.


     


    EDIT 10 minutes plus tard: J'avais un binding dans mon Image cell". Je l'ai supprimé et ça marche pour l'image. Mais maintenant c'est le text qui ne fonctionne plus. Pourtant, là , il n'y a pas de "text cell" mais bien une attributedString.


     


    Argh! Je rate quelque chose de trivial, là , ça m'agace...  :s


  • Je ne saisis pas à  quoi te servent les propriétés controller et context de ton NSAppDelegate ?


     


    Tout ce qui utilise CoreData depuis l'inspecteur doit être DANS l'inspecteur et pointer sur une propriété NSManagedContext de l'inspecteur. Dès que tu change cette propriété lors de l'activation d'une fenêtre tous les bindings qui pointent dessus sont mis à  jour.


     


    PS : Fleurantin s'écrit avec un a.


  • berfisberfis Membre
    mai 2013 modifié #12

    Oups, désolé fleurAntin.


     


    la propriété controller est un IBOutlet de l'appDelegate sur l'arrayController de l'inspecteur.


    la propriété context est le contexte courant (qui pointe sur celui du document actif)


     


     


    Je n'ai pas de problème en ce qui concerne l'inspecteur lui-même. Il affiche correctement le contenu du document actif. J'ai même stocké dans le document la ligne sélectionnée de façon à  ne pas revenir sur le premier enregistrement quand je switche entre les fenêtres.


     


    C'est au niveau des bindings du document que j'ai de la peine: lorsqu'un document devient inactif, il faut "couper" la liaison qui existe sur le texte et l'image, sinon tous les documents ouverts présenteront le même texte et la même image que le document actif. Rigolo... mais pas bon.


     


    A propos, j'avais une boulette dans la seule partie que je ne vous avais pas montrée (bien sûr): le modèle Core Data. Le texte était défini par un attribut de type "String". Je l'ai mis à  "Transformable" (pour une attributedString, c'est préférable).


     


    Je suis sûr que dans quelques heures je me dirai "mais c'est tout simple et tellement évident"... mais je n'en suis pas là .




  •  


    ça ne résout pas mon problème: comment changer le contexte de Core Data? Ce petit monstre refuse de sauver le contexte en mémoire, cela veut dire que lorsque je "switche" du doc A au doc B, le contenu modifié de doc A est perdu -- 




    Sans utiliser Core Data moi même je ne peux guère t'aider. Par contre il y a une instruction, une méthode de NSWindow, que j'utilise pour que soit validée une saisie dans une NSTableView quand on clique sur un bouton en dehors de celle ci :



    [[tableView window] endEditingFor:nil];

    Aucune idée si ça t'aidera mais ça vaut le coup d'essayer ?


    Il y a aussi (de mémoire) la méthode save: qui peut s'appliquer à  un NSManagedObjectContext pourquoi ne pas l'utiliser quand ta fenêtre perd le statut de main ou key window ?

  • Berfis,


     


    J'ai du mal à  saisir ce que tu veux afficher dans tes documents et dans ton inspecteur.


    Si tu le désire je peux jeter un coup d'oe“il sur ton projet si tu me l'envoies.


  • berfisberfis Membre
    mai 2013 modifié #15


  • Moi aussi, j'avais essayé les notifications au début. Mais j'ai le souvenir que ça posait de nombreux problèmes parce que les notifications n'étaient pas toujours émises, par exemple quand l'application passait à  l'arrière plan. Désolé de n'être pas plus précis, mais ça remonte à  quatre ans !




    Voilà , c'est ça! tout se passe bien jusqu'au moment où je désactive l'application, c'est exactement ça.


     


    Dommage que tu ne te souviennes plus de ce que tu avais fait pour remédier au problème, parce que je suis en plein dedans.  :/

  • CéroceCéroce Membre, Modérateur


    Voilà , c'est ça! tout se passe bien jusqu'au moment où je désactive l'application, c'est exactement ça.


     


    Dommage que tu ne te souviennes plus de ce que tu avais fait pour remédier au problème, parce que je suis en plein dedans.  :/




     


    Si, ça je m'en souviens, j'ai arrêté d'utiliser les notifications, et j'ai tout fait avec les bindings comme je te l'ai indiqué.

  • Mmmh, non, parce que dans ta solution, je me retrouve avec un contrôleur par document. J'aimerais n'en avoir qu'un, au niveau de la fenêtre Inspecteur de l'application...


  • CéroceCéroce Membre, Modérateur

    Tu peux toujours binder le NSArrayController de l'inspecteur sur le MOC du document courant.


    Cela dit, comme c'est le NSArrayController qui gère la sélection, ça me semble sensé d'avoir un NSArrayController par document.


  • Je comprends ta logique, Ceroce, mais dans mon idée c'est l'inspecteur qui gère la sélection du document. Disons que le document représente un managedobject, comme une page de formulaire, et l'inspecteur permet de "feuilleter" l'array...
  • Bon, ce sera sans doute plus clair si je joins le projet...


  • berfisberfis Membre
    mai 2013 modifié #22

    Bonjour à  tous,


     


    Je pense avoir trouvé. Je livre en tout cas ma solution, sait-on jamais, elle pourra peut-être éviter des heures de manque de sommeil à  certains.


     


    En même temps, ceci démontrera dans quels états de quasi-paranoà¯a un bug vicieux (car pour moi il l'était) peut me plonger. Pour cela, je veux vous montrer dans quelles fausses directions je suis parti.


     


    Direction 1 -- Le "contexte bidon": l'idée est venue d'une solution trouvée sur le net mais qui faisait appel aux MagicalRecords. Il est facile d'en créer un avec une ligne. Mais comme je ne m'y suis pas encore mis, je l'ai donc implémenté de A à  Z en Core Data pur. Pourquoi? Parce que (désolé fleurantin) je trouve pour ma part insupportable de voir ma console se remplir dès le lancement du programme. Même s'il ne s'agit que d'un avertissement qui ne l'empêche pas de tourner, c'est le signe que quelque chose ne va pas, et un jour ou l'autre ça va me causer des ennuis. Et puis je suis fait ainsi, je trouve cela inesthétique et négligent.


     


    Direction 2 -- Les notifications: alerté par le message de Céroce, j'ai cherché de ce côté en bourrant le code de NSLog. Mais mes notifications étaient toujours émises correctement, même quand l'appli passait au second rang. Je me suis demandé un instant si elles l'étaient dans le mauvais ordre, l'activation d'abord et la désactivation ensuite, mais ce n'était pas le cas. Pourtant, c'est bien à  la réactivation que j'avais des problèmes: le lien était brisé. Mon problème était bien un problème de binding.


     


    J'ai donc relu (quasiment en entier) la doc d'Apple sur les bindings, et le KVO/KVC. La lecture de ces pages rédigées dans leur style "les ingénieurs parlent aux ingénieurs" m'emplit toujours d'une émotion sacrée, je me sens dans la peau du disciple tremblant qui écoute parler les grands prêtres en une langue qu'il ne comprend pas toujours...


     


    Instructif mais non, rien à  tirer de là . Pourtant, jetez un coup d'oeil sur le nombre de références Google liées au problème (même hors Core Data) de la mise à  jour des arrayControllers. Edifiant. On croit que c'est simple jusqu'au moment où... ça ne marche plus.


     


     


    Direction 3 -- Les paths des bindings: j'ai cru un instant avoir trouvé la solution. Prenez la ligne


     




    [_text bind:@attributedString toObject:self withKeyPath:@appDelegate.controller.selection.text options:nil];

    qui s'effectue lorsque le document est réactivé. C'est là  que, après avoir rétabli le contexte du document, je rétablis les bindings entre mes champs et le contrôleur. Que représente "self"? Eh bien, le document puisque c'est une méthode de document. Mais (aha!) c'est avec le contrôleur que je dois faire le lien, pas avec le document! Je devrais écrire:



    [_text bind:@attributedString toObject:_appDelegate.controller withKeyPath:@selection.text options:nil];

    Voyez à  quel degré de désespoir j'en étais rendu: cette ligne est strictement équivalente en termes de "binding path", puisque _appController est une propriété du document. Je ne fais que découper mon binding autrement. Le chemin reste le même.


     


    La solution.


     


    Je suis maintenant sur la trace des bindings, et je ne veux plus lâcher. Le problème est là , devant mes yeux (et les vôtres aussi) mais je ne le vois pas. L'application fonctionne parfaitement jusqu'à  la première désactivation. C'est ici, dans ces "re-bindings" que le bug se cache. Je vérifie en ajoutant dans la fenêtre du contrôleur un textView et je constate la chose suivante: le textView du contrôleur met à  jour caractère par caractère le textView du document... mais l'inverse ne se fait pas. Mais j'ai vérifié mon "binding path", il est correct. Je pense devenir cinglé (peut-être d'ailleurs suis-je en bon chemin) et je me demande pourquoi je n'ai pas choisi le macramé au lieu de la programmation.


     


    Je commence à  me parler à  moi-même, comme le Milou blanc (Mb) et le Milou rouge (Mr):


     


    Mb: -- Pourquoi le textView du contrôleur actualise-t-il le textView du document caractère par caractère?


    Mr: -- Ben, parce que je lui dit de le faire!


    Mb: -- Comment?


    Mr: -- Avec l'option "Validate Immediately". Sinon, je vais perdre le contenu édité. C'est un textView, pas un textField que je peux valider avec un "tab"...


    Mb: Et le binding, tu le rétablis comment au niveau des options?


    Mr: Ben je te l'ai dit: je fais:




    [_text bind:@attributedString toObject:_appDelegate.controller withKeyPath:@selection.text options:nil];

    Mb: -- Comment ça, nil?


    Mr: -- Ben... je veux laisser les options définies dans IB en place donc... ooooops... tu veux dire que... ça va me bousiller toutes les options?


    Mb: -- les bousiller, non, mais les remettre à  zéro sûrement.


     


    Je me tourne alors vers ces pages que j'ai dû survoler inconsidérément. Et j'ajoute ceci:



    NSDictionary *opt = @{NSContinuouslyUpdatesValueBindingOption:@YES};
    [self.text bind:@attributedString toObject:_appDelegate.controller withKeyPath:@selection.text options:opt];

    Et tout se met enfin à  fonctionner. C'est là , dans ce nil, que résidait le bug. Avouez qu'il était bien caché, le bougre. Un de ces bugs que je déteste: pas d'erreur à  la compilation, rien à  l'exécution, des fois ça marche même... et subitement l'application fait n'importe quoi alors que rien ne semble voir changé par ailleurs. Un de ces bugs qui me prend la tête jusqu'à  des heures déraisonnablement tardives, et me fait douter de la texture même du réel...


     


    Voilà . Je tenais à  remercier tous ceux qui (et ils sont nombreux) m'ont aidé, sinon à  résoudre mon problème, du moins à  mieux le cerner, m'ont obligé à  le reformuler pour mieux le comprendre, avec une bonne volonté que je qualifie d'admirable.


     


    Merci de tout coeur à  tous!


     


    Bernard


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