Répercuter un "Undo"dans une NSTableView après avoir déplacé une ligne.

berfisberfis Membre

Bonjour,

Je m'arrache le peu de cheveux qui me restent sur le problème suivant :

J'ai des entités Core Data munies d'un attribut "rowIndex" qui permet de garder l'ordre des entités dans une NSTableView (on peut donc déplacer les lignes par drag/drop). Tout fonctionne à merveille… jusqu'au moment où j'essaie d'annuler un déplacement. Il suffit en fait d'envoyer un rearrangeObjects su(x) contrôleur(s). Mais comme Core Data met dans le stack de son UndoManager la renumérotation des lignes de la tableView, il faut faire un double "Annuler" pour retrouver l'ordre initial… ce qui est assez laid. En fait, après l'annulation de la renumérotation (qui marche) il faut rafraîchir la tableView avec un rearrangeObjects... qui n'est pas automatique.

Ce que j'ai essayé en pure perte :

  • intercepter le Undo au niveau de la fenêtre du document (c'est moche, ça marche… si tableView est logée dans la fenêtre principale)
  • utiliser la méthode de délégué du document windowWillReturnUndoManager; (c'est n'importe quoi)
  • ajouter un observer sur l'array du arrayController (pire que tout)
  • poster des notifications (j'en poste une, j'en reçois quarante-huit)
  • utiliser des dispatch pour sortir de la boucle d'événements courante.

J'ai l'impression désagréable (que j'ai déjà éprouvée maintes fois) de travailler contre Core Data plutôt qu'avec lui. Il y a certainement l'un d'entre vous qui a dû, à un moment ou un autre, affronter ce problème et trouver une solution… non?

En plus, je suis sûr que la solution est plus simple que je l'imagine.

D'avance merci !

Réponses

  • Joanna CarterJoanna Carter Membre, Modérateur

    Peut-être c'est car un déplacement consiste de la suppression de l'objet d'un indexPath et l'insertion du même objet à un nouveau indexPath - du coup, deux opérations à annuler.

  • berfisberfis Membre

    Bonjour,

    Entre-temps j'ai continué à creuser et j'ai fini par trouver une solution qui (comme je le voulais) se situe dans ma classe BFReorderController (NSArrayController) :

    - (void) awakeFromNib
    {
        [super awakeFromNib];
     …
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(cancelling:) name:NSUndoManagerDidUndoChangeNotification object:nil];
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(cancelling:) name:NSUndoManagerDidRedoChangeNotification object:nil];
    }
    
    - (void) cancelling:(NSNotification *)notification
    {
         [self rearrangeObjects];
    }
    

    … ça semble fonctionner. Qu'en pensez-vous ?

  • Joanna CarterJoanna Carter Membre, Modérateur

    Pourquoi pas ? :)

  • PyrohPyroh Membre

    Normalement tu gère ton drag&drop quelque par dans ton code. Comme l'a si bien noté @Joanna Carter tu as deux opérations vu qu'une ligne qui change d'index et retirée puis rajoutée au bon endroit du tableau.

    Pour commencer il faut grouper ces modifications en entourant ton code relatif au switch de ligne de [undoManager beginUndoGrouping] et [undoManager endUndoGrouping] tu n'auras dès lors plus qu'une seule et unique entrée pour cette modification.

    Ensuite il y a la solution que tu as posté plus haut, c'est un workaround astucieux, certes, mais c'est terriblement inefficient. NSArrayController propose le flag automaticallyRearrangesObjects qui fera le boulot pour toi, à condition que tu utilise un sort descriptor, sur rowIndex par exemple.

  • berfisberfis Membre

    @Pyroh
    J'ai bien sûr essayé les begin/endUndoGrouping, mais ça ne fonctionnait pas. Il me semble que Core Data effectue certaines opérations avec un délai pour sortir de la boucle d'événements (et laisser la possibilité d'intercepter le changement avec un message d'erreur, p. ex.) alors que rearrangeObjects agit dans la boucle. Il y avait donc, si je me souviens bien, un souci de gestion des deux opérations.

    Quant à automaticallyRearrangesObjects, je l'ai débranché parce qu'il marchait trop bien, et les lignes de la tableView sortaient soudain du champ de vision, ce qui m'obligeait à faire des scrollToVisible qui pouvaient eux aussi déconcerter. Mais je l'utilise dans d'autres applications…

    Je ne poste jamais un problème sur CocoaCafé sans avoir galéré quelques jours :)

  • PyrohPyroh Membre

    @berfis
    J'ai aussi cette habitude d'essayer de me débrouiller avant de poster ici.
    Cependant y'a un truc qui me chiffonne avec ton soucis parce qu'on est sur quelques chose d'assez simple en terme fonctionnel mais qui m'a l'air étrangement compliqué à mettre en place.

    Je sais que NSOrderedSet a été créé spécifiquement pour CoreData ce qui fait que tu dois sûrement être en mesure de trier tes entrées sans cette propriété rowIndex.

    Concernant la boucle d'events il est fort probable que CoreData fasse 2/3 truc en douce sur autre chose que le main thread. Maintenant il nous reste des possibilités comme d'utiliser un save sur le contexte pour faire un bon commit qui va nous assurer une stack synchronisée. On peut aussi tenter le coup avec, sur ce même contexte, un appel à performAndWait qui va tout faire de manière synchrone.

    Mais devoir observer le undo manager pour le piloter ne m'a pas l'air très propre. Après si ça marche et que ça te va c'est ok pour moi, hein, c'est juste pour pousser la réflexion un peu plus loin. 😉

  • berfisberfis Membre

    @Pyroh
    Je travaille beaucoup avec les bindings (pire, je code en objective-c :o ) et j'aime bien lier le contenu d'un "sous-contrôleur" au set de la sélection du contrôleur principal. Même si NSOrderedSet semble être le graal des amateurs de Core Data, il n'y a aucun moyen le binder ce %&@! objet au contenu du sous-contrôleur. Ni par array ni par set (NSOrderedSet n'hérite pas de NSSet, mais de NSObject). Je reste donc campé sur mon attribut rowIndex…

  • CéroceCéroce Membre, Modérateur
    24 juin modifié #9

    @berfis a dit :
    Il y a certainement l'un d'entre vous qui a dû, à un moment ou un autre, affronter ce problème et trouver une solution… non?

    J'ai déjà affronté ce genre de problèmes, et la solution que j'avais trouvé était de ne pas laisser Core Data gérer l'undo. C'était un gros paquet d'emmerdes.
    http://mikeabdullah.net/core_data_undo_management.html

    Pour te dire, j'avais des tests unitaires, qui effectuaient une action puis faisaient un undo, et je n'ai jamais réussi à les faire fonctionner de manière fiable. Pourtant, c'est simple dans le principe: on peut tester que l'action d'undo est la bonne et tester si on est revenu dans l'état initial après l'undo.

    J'avais même écrit une classe pour logguer les appels au NSUndoManager et voir ce que Core Data mettait sur l'undo stack: que des conneries. Il y a une sorte de bidouille avec le runtime.

    Je suis désolé de ne pas pouvoir donner de détails, mais je ne me souviens plus.

    Tant que j'y suis: NSOrderedSet était buggué. J'avais trouvé un bout de code pour le faire marcher, avec quelques limitations. Je crois que ça a été corrigé il y a trois ans, mais j'ai un doute.

    En plus, je suis sûr que la solution est plus simple que je l'imagine.

    Pas moi.

    J'ai l'impression désagréable (que j'ai déjà éprouvée maintes fois) de travailler contre Core Data plutôt qu'avec lui.

    Sur ce projet, nous avions fini par virer Core Data, et utiliser une combinaison de fichiers .json et de répertoires. Ça a réglé tous nos problèmes du jour au lendemain. Et nous a permis de virer cette autre merde qu'est NSManagedDocument.

    Depuis, j'ai compris, je n'utilise plus Core Data. En dehors du fait que c'est une techno très opaque — et qu'elle soit en source fermé n'aide pas — elle n'a pas vraiment de qualités. Ça a l'air bien au début parce qu'on définit le schéma de la base de données graphiquement. C'est à l'usage qu'on constate que c'est chiant de mettre à jour les sous-classes de NSManagedObject, à quel point le système de migration est pourri, que la gestion du multithreading est inexistante, combien c'est lent, que ça marche mal avec iCloud, etc.

    Enfin, globalement, je dirais que c'est un concentré de tout ce que je déteste dans Objective-C: tous ces trucs non-typés qui font des trucs opaques à l'aide du KVC et du KVO et qui te pêtent à la gueule à l'exécution.

    On s'en sort bien mieux en utilisant SQLite à travers GRDB, par exemple.

  • J'ai un nouveau motif de détestation.

    Core Data mélange complètement les objectIDs si on fait une migration à l'aide d'un document Model Mapping. Génial si on s'est, comme moi, basé sur ces IDs pour faire des liens internes au document au lieu d'utiliser un UUID fait maison…

    Bon, ça m'aura au moins appris à être de plus en plus méfiant, mais c'est vrai que c'est agaçant de ne pas pouvoir construire fiablement sur ce truc.

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