Accueil > Développement > Quitter proprement une application

Quitter proprement une application

Par Apple Computer, Inc,

Traduit par Pascal Gouhier le 28/02/2003

Quand un utilisateur quitte une application (en choisissant la commande Quitter ou en pressant les touches Commande–Q) ou quand un utilisateur quitte sa session, redémarre ou éteint le système, une application doit elle-même faire tout ce qui est nécessaire pour quitter proprement. Ceci devrait assurer une sauvegarde appropriée de toutes les données liées à l’application et de tous ses documents, le stockage de tous les “états” (comme les préférences), et le nettoyage de tous les résidus temporaires. Ce qu’un arrêt propre nécessite dépend du type d’application. Par exemple, une application dont plusieurs documents sont à sauvegarder doit faire plus de chose qu’une application sans document qui n’a qu’à libérer les ressources utilisées.

En Cocoa, tous les évènements demandant l’arrêt d’une application invoquent la méthode de délégation applicationShouldTerminate: de NSApplication. Si le délégué n’implémente pas cette méthode, l’application est arrêtée sans prêter attention aux éventuels documents non-sauvegardés. De plus, quitter, terminer sa session, redémarrer ou éteindre ne conduit pas nécessairement à invoquer la méthode de délégation windowShouldClose: de NSWindow pour toutes les fenêtres d’une application. Cette méthode est immédiatement invoquée quand un utilisateur clique sur le bouton de fermeture ou choisit la commande Fermer. C’est typiquement le moment où le délégué de la fenêtre (NSWindow) déroule une feuille demandant à l’utilisateur s’il veut ou non sauvegarder les données associées à la fenêtre. Pour quitter proprement votre application (en supposant que des données sont à sauvegarder), vous devez vous assurer que windowShouldClose: est invoqué pour chacune des fenêtres, ou que le comportement généralement mis en application dans cette méthode se produit ailleurs dans votre application.

Une application qui s’arrête proprement peut être de l’un de ces types :

  • applications multi-document fondées sur l’architecture document de Cocoa
  • applications multi-document non fondées sur l’architecture document
  • applications mono-document
  • applications qui doivent sauver un “état” et nettoyer ce que l’application n’a pas nettoyé elle-même.

La procédure diffèrent selon le type d’application. L’article qui suit met en avant le second type d’application — applications multi-document non fondées sur l’architecture document — car la procédure est plus complète. L’exemple de code utilisé pour illustrer le propos est issu de l’application exemple TextEdit située dans /Developer/Examples/AppKit/TextEdit/.

Applications fondées sur l’architecture document

Si votre application multi-document utilise l’architecture document de Cocoa —c’est à dire, l’ensemble d’objets NSDocument, NSWindowController, et NSDocumentController , ainsi que leurs délégués— la bonne nouvelle est que vous n’avez rien à faire pour que votre application quitte proprement. Cette fonctionnalité gratuite est implémentée dans NSDocumentController.

Si vous n’utilisez pas l’objet NSDocumentController prédéfini, ou si vous voulez créer une sous-classe de ce dernier, vous devriez en apprendre plus sur la façon dont la classe NSDocumentController arrête proprement les opérations; voici un résumé :

  1. Dans applicationShouldTerminate:, si plusieurs documents sont à sauvegarder, NSDocumentController appelle une méthode dont le nom est incroyablement long : reviewUnsavedDocumentsWithAlertTitle:cancellable:delegate:didReviewAllSelector:contextInfo:. Cette méthode affiche une boite d’alerte contenant des boutons pour passer en revue les documents non sauvegardés, quitter sans sauvegarder ou annuler l’opération.
  2. Si l’utilisateur choisit d’annuler, NSDocumentController renvoie simplement NSTerminateCancel.
  3. Si l’utilisateur choisit de quitter sans sauvegarder ou s’il n’y a aucun document à sauvegarder, la méthode identifiée par le sélecteur didReviewAllSelector est invoquée avec un paramêtre YES, permettant au délégué approprié de faire ce qui est nécessaire avant de quitter.
  4. Si l’utilisateur choisit de passer en revue les documents non saugardés, NSDocumentController appelle closeAllDocumentsWithDelegate:didCloseAllSelector:contextInfo:. Cette méthode affiche une feuille pour chaque fenêtre ayant des données à sauvegarder.

Pour plus d’information sur l’architecture document de Cocoa, référez vous au sujet  “Document-Based Applications” sur ADC.

Résumé de la procédure de sauvegarde d’un document

Une application multi-document non fondée sur l’architecture document de Cocoa doit accomplir par elle-même plus de tâches de la procédure. Ce travail est similaire à celui de NSDocumentController, et décrit dans “Applications fondées sur l’architecture document”. En résumé, les étapes sont les suivantes :

  1. Le délégué application doit implémenter applicationShouldTerminate: pour prendre en charge toute requête demandant de quitter l’application ou de terminer la session, de redémmarrer ou d’éteindre le système.
  2. Dans applicationShouldTerminate: le délégué peut obtenir une liste des fenêtres et déterminer si des données des document associés sont à sauvegarder.
  3. Si il y a des documents à sauvegarder, le délégué affiche un message d’alerte demandant à l’utilisateur si il (ou elle) veut sauvegarder avant de quitter, annuler tous les changements (et quitter) ou annuler l’opération.Bien sûr, s’il n’y a aucun document non sauvegardé, le délégué renvoie NSTerminateNow, ce qui demande à l’objet application (NSApp) de procéder à l’arrêt (fermeture des fenêtre, etc…).
  4. Si l’utilisateur veut passer en revue les changements et sauvegarder les documents, le délégué doit initier, dans applicationShouldTerminate:, la procédure de sauvegarde pour chaque fenêtre et renvoyer NSTerminateLater. Sinon, il renvoie selon le cas NSTerminateNow ou NSTerminateCancel.
  5. Dans une routine de sauvegarde, chaque fenêtre avec des données à sauvegarder affiche une feuille demandant à l’utilisateur s’il veut sauvegarder le document, fermer la fenêtre sans sauvegarder ou annuler l’opération. Chaque fenêtre s’affiche séquentiellement, et non toutes en même temps, et agit en fonction du choix de l’utilisateur.Comme le but est le même (sauvegarder les données du document), le code utilisé pour cette action peut être le même que celui qui est exécuté quand l’utilisateur ferme la fenêtre (invoqué par le délégué de la fenêtre dans windowShouldClose:).
  6. Une fois que tous les documents ont été sauvegardés (ou quand l’utilisateur choisit “Quitter sans sauvegarder”), After all document data has been saved (or when users choose “close without saving”), envoie de replyToApplicationShouldTerminate: à l’objet application (NSApp) avec un argument YES.Si l’utilisateur termine sa session, ou redémarre ou éteint le système, vous devez envoyer replyToApplicationShouldTerminate: dans les deux minutes après avoir renvoyé NSTerminateLater dans applicationShouldTerminate:, sinon la procédure sera hors jeu (time out).
  7. Quand l’application obtient le feu vert pour quitter, elle ferme, entre autres, toutes les fenêtres ouvertes. Ceci vient de l’invocation de la méthode délégué windowWillClose: de NSWindow grâce à laquelle le délégué peut accomplir toutes les tâches nécessaires relatives aux fenêtres (nettoyage, par exemple).
  8. Juste avant que l’application cesse son activité, la méthode du délégué application applicationWillTerminate: est invoqué; ainsi, le délégué peut accomplir toutes les tâches relatives à l’application elle-même, comme enregistrer les préférences.

La section suivante, “Un exemple : Text Edit”, illustre la procédure présentée ci-dessus et précise certains détails de l’implémentation.

Un exemple : Text Edit

Quand vous installez les outils pour développeurs de Mac OS X, l’application TextEdit, qui est fournie dans l’installation standard de Mac OS X, est aussi incluse comme un exemple de projet d’application Cocoa (/Developer/Examples/AppKit/TextEdit/). Même si Text Edit est une application multi-document, elle n’est pas construite (du moins pour l’instant) selon l’architecture document. De fait, étudier comment est géré l’arrêt de l’application est instructif.

Le listing 1 montre comment le délégué application de Text Edit — qui est son objet controller (Controller.m) — implémente applicationShouldTerminate:.

Listing 1 : Implémenter applicationShouldTerminate:

- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)app {
    NSArray *windows = [app windows];
    unsigned count = [windows count];
    unsigned needsSaving = 0;

    // Determine if there are any unsaved documents...
    while (count--) {
        NSWindow *window = [windows objectAtIndex:count];
        Document *document = [Document documentForWindow:window];
        if (document && [document isDocumentEdited]) needsSaving++;
    }
    if (needsSaving > 0) {
        int choice = NSAlertDefaultReturn;  // Meaning, review changes
    if (needsSaving > 1) { // If we only have 1 unsaved document,
                          // we skip the "review changes?" panel
            NSString *title = [NSString stringWithFormat:
                NSLocalizedString(@"You have %d documents with unsaved
                changes. Do you want to review these changes before
                quitting?", @"Title of alert panel which comes up when user
                chooses Quit and there are multiple unsaved documents."),
                needsSaving];
        choice = NSRunAlertPanel(title,
            NSLocalizedString(@"If you don't review your documents, all
                changes will be lost.", @"Warning in the alert panel which
                comes up when user chooses Quit and there are unsaved
                documents."),
            NSLocalizedString(@"Review Changes...", @"Choice (on a button)
                given to user which allows him/her to review all unsaved
                documents if he/she quits the application without saving
                them all first."),
            NSLocalizedString(@"Discard Changes", @"Choice (on a button)
                given to user which allows him/her to quit the application
                even though there are unsaved documents."),
            NSLocalizedString(@"Cancel", @"Button choice allowing user to
                cancel."));
            if (choice == NSAlertOtherReturn) return NSTerminateCancel; /* Cancel */
        }
        if (choice == NSAlertDefaultReturn) { /* Review unsaved; Quit Anyway falls through */
            [Document reviewChangesAndQuitEnumeration:YES];
            return NSTerminateLater;
        }
    }
    return NSTerminateNow;
}

Dans cette méthode, le délégué obtient la liste des fenêtres et demande son statut à chaque document associé (modifié ou non). Si aucun document n’est modifié, le délégué renvoie NSTerminateNow. Si plusieurs documents sont modifiés, il affiche une boite de dialogue demandant à l’utilisateur s’il ou elle veut revoir les changements, ne pas tenir compte des changements ou annuler l’opération. En fonction de la réponse, le délégué renvoie une constante appropriée  : NSTerminateLater, NSTerminateNow, ou NSTerminateCancel. Si l’utilisateur veut passer en revue les fenêtres et leur document, ou si il n’y a qu’une seule fenêtre avec un document modifié, le délégué envoie le message reviewChangesAndQuitEnumeration: à la classe Document class avant de renvoyer la constante NSTerminateLater.

La méthode reviewChangesAndQuitEnumeration: tourne parmi les fenêtres de l’application et, pour chaque fenêtre contenant des données non enregistrées, elle appelle askToSave: afin d’afficher la feuille “voulez-vous sauvegarder ?”. Ce qui est important c’est que ceci s’exécute dans une séquence controlée (ce qui évite que toutes les fenêtres s’affichent en même temps avec leur feuille de sauvegarde). Le listing 2 illustre comment Text Edit (dans sa classe Document) implemente cette méthode de classe.

Listing 2 : Passer récursivement en revue les document modifiés

+ (void)reviewChangesAndQuitEnumeration:(BOOL)cont {
    if (cont) {
        NSArray *windows = [NSApp windows];
        unsigned count = [windows count];
        while (count--) {
            NSWindow *window = [windows objectAtIndex:count];
            Document *document = [Document documentForWindow:window];
            if (document) {
                if ([document isDocumentEdited]) {
                    [document askToSave:@selector(reviewChangesAndQuitEnumeration:)];
                    return;
                }
            }
        }
    }
    [NSApp replyToApplicationShouldTerminate:cont];
}

Text Edit affiche successivement les feuilles d’alerte des fenêtres en invoquant récursivement reviewChangesAndQuitEnumeration: (comme cela sera montré). Le drapeau (flag) passé à la méthode (cont), s’il est à NO, signale que l’utilisateur a annulé l’arrêt ; s’il est à YES, la méthode continue le processus avec le document non-sauvegardé suivant. Quand il n’y a plus de document à revoir, ou si cont est à NO, replyToApplicationShouldTerminate: est envoyé à l’objet application avec le drapeau approprié.

La valeur passée à askToSave: est un sélecteur, qui identifie dans ce cas la méthode reviewChangesAndQuitEnumeration:. Comme montré dans le listing 3, Text Edit implémente simplement askToSave: pour rendre visible la fenêtre et l’identifiant (key) du document courant et appelle la fonction NSBeginAlertSheet, qui affiche la feuille d’alerte demandant si l’utilisateur veut sauver le document avant de fermer la fenêtre. Remarquez que le selecteur est passé dans cette fonction comme le paramêtre d’information sur le context. (NdT : notez aussi l’utilisation de NSLocalisedString qui permet de gérer plus facilement la localisation des des applications Cocoa).

Listing 3 Afficher et gérer une feuille de sauvegarde de document

- (void)askToSave:(SEL)callback {
    [[self window] makeKeyAndOrderFront:nil];
    NSBeginAlertSheet(NSLocalizedString(@"Do you want to save changes
        to this document before closing?", @"Title in the alert panel when
        the user tries to close a window containing an unsaved document."),
        NSLocalizedString(@"Save",
        @"Button choice which allows the user to save the document."),
        NSLocalizedString(@"Don't Save",
        @"Button choice which allows the user to abort the save of a
        document which is being closed."),
        NSLocalizedString(@"Cancel",
        @"Button choice allowing user to cancel."),
         [self window], self,
        @selector(willEndCloseSheet:returnCode:contextInfo:),
        @selector(didEndCloseSheet:returnCode:contextInfo:),
        (void *)callback,
        NSLocalizedString(@"If you don't save, your changes will be lost.",
        @"Subtitle in the alert panel when the user tries to close a
        window containing an unsaved document."));
}

L’API Cocoa concernant les feuilles spécifie deux méthodes de rappel (callback methods) qui sont éventuellement invoquées dans le délégué modal comme un résultat de l’appel de NSBeginAlertSheet. La première, appelée méthode “did-end”, est invoquée après que l’utilisateur ait cliqué sur un bouton de la feuille d’alerte mais avant que la feuille disparaisse ; la seconde, appelée méthode “did-dismiss”, est invoquée après que la feuille ait disparue. Les paramètres de fonction qui les identifient sont des sélecteurs. Les méthodes doivent être conformes à une certaine signature. (Allez voir le sujet “Sheets” sur l’ADC pour plus d’informations.)

L’implémentation de askToSave: fait usage de ces deux méthodes. Pour le paramètre contextInfo de NSBeginAlertSheet elle passe le sélecteur qui lui est passé, qui identifie dans ce cas la méthode de classe reviewChangesAndQuitEnumeration:. Ensuite, pour le délégué modal (self), il implémente (ainsi que montré dans le listing 4) la méthode “did-end” willEndCloseSheet:returnCode:contextInfo: et la méthode “did-dismiss” didEndCloseSheet:returnCode:contextInfo:.

Listing 4 : Les méthodes de rappel de NSBeginAlertSheet

- (void)willEndCloseSheet:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo {
    if (returnCode == NSAlertAlternateReturn) {     /* "Don't Save" */
        [[self window] close];
        // Send callback (YES means continue save
        if (contextInfo) ((void (*)(id, SEL, BOOL))objc_msgSend)([self class], (SEL)contextInfo, YES);
    }
}

- (void)didEndCloseSheet:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo {
    if (returnCode == NSAlertDefaultReturn) {       /* "Save" */
        [self saveDocument:NO rememberName:YES shouldClose:YES whenDone:(SEL)contextInfo];
    } else if (returnCode == NSAlertOtherReturn) {  /* "Cancel" */
        // Send callback indicating save cancel
        if (contextInfo) ((void (*)(id, SEL, BOOL))objc_msgSend)([self class], (SEL)contextInfo, NO);
    }
}

Text Edit implemente la méthode willEndCloseSheet:returnCode:contextInfo: pour le cas où l’utilisateur veut fermer une fenêtre sans sauvegarder. Dans ce cas, il veut l’expérience utilisateur préférée pour la fermeture des fenêtres avant que la feuille d’alerte glisse “sous” la barre de titre. Notez ce que fait cette méthode après avoir fermé la fenêtre. En utilisant la fonction objc_msgSend du runtime Objective-C, willEndCloseSheet:returnCode:contextInfo: envoie le message identifié par le sélecteur transmis, reviewChangesAndQuitEnumeration:, à la classe Document avec un paramètre YES, provoquant ainsi l’affichage de la feuille d’alerte de la fenêtre suivante.

didEndCloseSheet:returnCode:contextInfo: gère les constantes d’identification de boutons restantes potentiellement envoyées par la fonction NSBeginAlertSheet. Si l’utilisateur clique sur le bouton permettant de sauvegarder le document, cela invoque une méthode qui non seulement enregistre les données (et affiche le gestionnaire de fichier si nécessaire) mais aussi ferme la fenêtre et appelle reviewChangesAndQuitEnumeration: avec un paramètre YES. (ce détail n’est pas montré ici.) Si l’utilisateur veut annuler l’opération d’arrêt, didEndCloseSheet:returnCode:contextInfo: utilise la fonction objc_msgSend pour envoyer reviewChangesAndQuitEnumeration: à la classe  Document, cette fois avec un paramètre NO.

Text Edit partage les portions de code appropriées entre la procédure d’arrêt de l’application et la procédure de fermeture des fenêtres. Le délégué d’une fenêtre de document implémente windowShouldClose: d’une façon qui invoque askToSave: si le document contient des données non sauvegardées. Cette fois, NULL est passé au lieu d’un sélecteur, et donc reviewChangesAndQuitEnumeration: n’est invoquée qu’une seule fois. Le listing 5 illustre comment Text Edit fait cela.

Listing 5 : Gestion de la fermeture explicite d’une fenêtre

- (BOOL)windowShouldClose:(id)sender {
    return [self canCloseDocument];
}

- (BOOL)canCloseDocument {
    if (isDocumentEdited) {
        [self askToSave:NULL];
        return NO;
    }
    return YES;
}

Nettoyage

En arrêtant proprement votre application, il n’y en fait pas grand chose que vous deviez faire après avoir sauvegardé vos données. Les objets qui composent une application prennent soin, en général, de libérer les objets utilisés et les ressources allouées. Il y a des exceptions à cela. L’une d’elles concerne les fenêtres ayant des références externes, dont on doit vérifier la déconnection. Il en est ainsi des délégués. La méthode windowWillClose: de Text Edit (qui est invoquée juste après windowShouldClose:), le délégué de la fenêtre s’efface lui-même en tant que référence à la fenêtre (Listing 6).

Listing 6  : Effacement d’une référence dans windowWillClose:

- (void)windowWillClose:(NSNotification *)notification {
    NSWindow *window = [self window];
    [window setDelegate:nil];
    [self release];
}

De même, quand vous devez enregistrer des données permanentes, un nettoyage final peut être nécessaire. Les préférences de l’utilisateur sont un bon exemple, et applicationWillTerminate: est un moment idéal pour les sauvegarder. Le listing 7 illustre comment Text Edit utilise applicationWillTerminate:.

Listing 7 : Enregistrer les préférences de l’utilisateur avec applicationWillTerminate:

- (void)applicationWillTerminate:(NSNotification *)notification {
    [Preferences saveDefaults];
}

Texte original en anglais sur developer.apple.com : Overview of Programming Topic: Application Architecture : Graceful Application Termination

Pascal Développement

  1. Pas encore de commentaire
  1. Pas encore de trackbacks
Vous devez être identifié pour poster un commentaire