MaCocoa 017

Capitolo 017 - Documenti

Adesso che so come leggere ed aggiungere file ad un controllo NSOutlineView, leggere e salvare i dati su di un file, mi accingo a compiere un passo molto importante.

L'argomento di questo capitolo è infatti l'architettura di una applicazione basata su documenti, e quindi in grado di visualizzare il contenuto di uno o più documenti all'interno di una serie di finestre, tutti assieme all'interno della stessa applicazione.

Tre sono le Classi con cui ho a che fare: NSDocument, NSDocumentController e NSWindowController. Ma comincio dall'inizio, ovvero dal misterioso e inutile file main.m.

Sorgenti: Sfrutto il capitolo 11 del libro Learning Cocoa.

Primo inserimento: Gennaio 2002 (?)

NSApplication

Il file contiene una unica istruzione che si limita a chiamare una funzione.

return NSApplicationMain(argc, argv);

NSApplicationMain è una funzione di utilità fornita dal framework, che costruisce un oggetto della classe NSApplication, carica il file nib indicato come principale (lo si indica nel pannello Application Setting quando si selezionano le opzioni dei Targets dentro XCode), tipicamente chiamato mainMenu.nib, e fa partire il loop, teoricamente infinito, degli eventi.

Ogni applicazione contiene uno ed un solo oggetto della classe NSApplication. L'oggetto incapsula l'intera applicazione (anche dal punto di vista del Finder e delle altre applicazioni) e funziona come unico punto di interfaccia verso il mondo esterno. Alcuni attributi (le variabili d'istanza) interessanti dell'oggetto NSApplication sono ad esempio l'icona, il già citato file nib principale, l'elenco delle finestre delle applicazioni, quale delle finestre è quella principale (davanti a tutte le altre), quale finestra riceve i caratteri inviati dalla tastiera (che può essere diversa dalla principale). Una variabile d'istanza importante è la classe delegata. Ricordo che una classe delegata aiuta la classe principale quando devono essere prese alcune decisioni sulle quali classe principale ha delle difficoltà. Un esempio è la decisione di uscire dall'applicazione. L'oggetto NSApplication, quando l'utente seleziona la voce di menu Esci o Quit non può decidere da sola se terminare; passa la palla alla classe delegate, che si preoccupa di verificare se tutti i documenti sono stati salvati, o se tale salvataggio interessa l'utente. La classe delegata può addirittura sospendere il procedimento di terminazione (ad esempio, perché l'utente ha deciso di rispondere Annulla ad uno dei dialoghi di salvataggio dei file).

Architettura

La struttura a documenti di una applicazione nasce a partire da un controllore di documenti. C'è un unico (credo...) oggetto della classe NSDocumentController che funziona da gestore dei documenti. È questo oggetto che risponde in prima istanza ai menu Nuovo, Apri, Salva. L'oggetto NSDocumentController gestisce quindi uno o più oggetti NSDocument, uno per ogni documento aperto o creato. Gli oggetti NSDocument sono responsabili della gestione del contenuto del documento, e di come questo contenuto è visualizzato dall'applicazione. Quindi, ogni oggetto NSDocument deve gestire una o più finestre in cui il documento è rappresentato a video. Comanda quindi su una serie di oggetti NSWindowController, uno per ogni finestra appartenente al documento. NSWindowController è la classe controllore della finestra, che contiene una serie di controlli, viste, eccetera.

NSDocumentController

Il compito principale dell'oggetto NSDocumentController è di creare o aprire documenti e di gestirli all'interno dell'applicazione. Possiede una lista di documenti aperti, e sorveglia particolarmente il documento attivo, quello relativo alla finestra principale.

È questo oggetto a rispondere ai comandi fondamentali da menu: apri, nuovo, salva, stampa, cose del genere. Ad esempio, quando l'utente seleziona Nuovo (o simile) da menu, è inviato un messaggio a NSDocumentController. Questo messaggio fa sì che venga costruito un nuovo documento (un oggetto NSDocument o sottoclasse) del tipo gestito dall'applicazione (l'elenco dei tipi si può dare sempre tramite il pannello Application Setting in XCode), e gli manda il messaggio init perché esegua le inizializzazioni base. Se invece la voce di menu scelta è Apri, si preoccupa di aprire il solito panello di apertura file, filtrando opportunamente i file secondo i tipi indicati (quelli di prima...). In base al tipo di file scelto, costruisce l'appropriato oggetto NSDocument; questa volta, invece di init gli manda il messaggio initWithContentOfFile, in modo da costringere la lettura del contenuto del file. Questo meccanismo ha una implicazione nella procedura di salvataggio e apertura file, molto più facile della realizzazione precedente.

NSDocument

NSDocument è una classe astratta, ovvero, così com'è non funziona. Devo sempre fare una sottoclasse di NSDocument perché funzioni come modello del mio tipo di documento. Questo significa che devo scrivere (anzi, sovrascrivere) una serie di metodi che facciamo il lavoro base di gestione dei documenti.

Il compito principale di un oggetto NSDocument è di rappresentare, manipolare, immagazzinare e caricare i dati persistenti associati ad un documento (insomma, gestisce un file su disco). Le operazioni che deve essere in grado di svolgere sono quindi essenzialmente di fornire i dati contenuti nel documento in una delle varie rappresentazioni richieste dagli altri oggetti dell'applicazione (in particolare, agli oggetti view contenuti nelle finestre), caricare i dati dal file in strutture interne e mostrarle a video, immagazzinare le informazioni in un file specificato. L'oggetto è il destinatario ultimo per salvare, stampare e chiudere documenti. È qui che si realizzano le operazioni di stampa, di Undo, è qui dove si traccia lo stato del documento (se è stato modificato o meno).

NSWindowController

Un oggetto NSWindowController gestisce una finestra associata ad un documento. Un documento può avere diverse finestre associate, ciascuna con il suo NSWindowController. Questi oggetti non sono molto interessanti, nel senso che il comportamento di default fornisce a NSDocument sufficienti servizi per compiere tutte le operazioni più comuni. In effetti, nell'esempio successivo, non mi sono nemmeno accorto della loro esistenza.

In definitiva, per fare una applicazione in grado di gestire più documenti, devo:

Sembra facile. Parto.

Comincio

figura 01

figura 01

figura 02

figura 02

figura 03

figura 03

Faccio un nuovo progetto, dal pregnante nome mc017, e dico di farlo document-based. XCode mi viene incontro e produce di suo già alcuni file; intanto, due file nib separati, uno con il menu dell'applicazione (è il file nib principale), ed uno con dentro la finestra dell'applicazione, per ora vuota. Poi, ha già definito per me una sottoclasse di NSDocument, dall'orribile nome di MyDocument. Ovviamente, il nome non mi va bene, e comincio a cambiare tutto. Il file nib lo passo da MyDocument.nib a CatalogWin.nib. I file MyDocument.h e m sono rinominati come CatalogDoc.h/m. Devo anche intervenire nel codice sorgente per cambiare il nome della sottoclasse (uso la comoda funzione Find estesa a tutti i file del progetto). Poi, dopo una serie di penosi tentativi, scopro altre cose da cambiare. Il file CatalogWin.nib, facendo riferimento ad una classe di NSDocument, utilizza ancora il nome MyDocument. Vado in IB, nel pannello Classes e rinomino la sottoclasse MyDocument di NSDocument in CatalogDoc. Non è finita. Devo specificare anche qual è il tipo di documento prediletto dall'applicazione (che è rimasto, nemmeno a dirlo, MyDocument). Si piglia ancora una volta la finestra Inspector del Target in questione; nei parametri della sezione Properties, sostituisco ancora una volta le occorrenze di MyDocument con CatalogDoc. E questa è finita.

Compilo, eseguo. Ho già una applicazione multi-documento funzionante (poco significativa, è vero, ma funzionante; dopotutto, cosa vi aspettate senza avere ancora scritto una riga di codice...).

Inserisco poi dall'esempio del capitolo precedente le classi che mi servono per sviluppare l'esempio corrente: LSFormatter, FileStruct, LSFileInfo, LSDataSource. Le userò tali e quali, senza alcuna modifica (che bello che bello che bello).

Interfaccia

figura 04

figura 04

Ricomincio da IB, e costruisco la finestra; butto via quella predefinita; dalle mie applicazioni precedenti riciclo la NSOutlineView (la copio dal precedente nib e la incollo qui, così evito di dover modificare le colonne e tutto il resto... si suppone che nel mondo object-oriented si cerchi di riciclare il più possibile). Già che ci sono, apro anche il nib principale con il menu, ed aggiungo un nuovo menu. Ci metto due voci, Add... e Delete, dove la prima voce intende rimpiazzare uno dei pulsanti che avevo nelle vecchie realizzazione dentro la finestra. I pulsanti Load e Save sono invece sostituiti dalle voci di menu Open e Save, appunto.

Ora, la mia domanda è la seguente. Chi è che svolge le funzioni della classe controllore (negli esempi fin qui, FileInfoCtrl)? In altre parole, adesso ho il problema di collegare la NSOutlineView con la sua sorgente di dati; negli esempi precedenti, il collegamento era automatico, in quanto attribuivo un dato valore ad un outlet in IB. Poi, il meccanismo di caricamento automatico del file nib faceva il resto, ricreando il collegamento all'apertura del file nib. Qui invece ho una NSOutlineView (almeno) per ogni documento aperto, ed il file nib (e quindi ogni outlet presente) è condiviso da tutti i documenti.

Proxy

La cosa è risolta sfruttando il (fino ad ora) misterioso oggetto File's Owner. In IB esiste per ogni file nib una icona File's Owner (proprietario del file). Il proprietario è un oggetto, esterno al file nib, che funziona da intermediario tra gli oggetti estratti dal nib al momento del caricamento dello stesso all'interno di una applicazione e gli altri oggetti presenti nell'applicazione. Quest'oggetto è spesso detto essere un oggetto proxy, con una terminologia utilizzata anche nelle connessioni di rete. L'oggetto proxy per un file nib funziona come referente di ogni messaggio da e verso il mondo esterno. Quando un oggetto definito in un file nib deve inviare dei messaggi al resto dell'applicazione, lo invia al proxy. È poi compito di questo oggetto proxy farlo arrivare. Un proxy è quindi un altro metodo per incapsulare i dati e le procedure, e facilitare la modularità delle applicazioni. Quando il file nib è caricato, il proxy è associato automaticamente a chi ha richiesto il caricamento. Quindi, nel file nib principale, quello che contiene il menu dell'applicazione, il proxy sta al posto dell'oggetto NSApplication. Nel file nib che contiene gli oggetti relativi alla rappresentazione di un dato documento, il proxy è associato a NSDocument.

Tutta questa storia serve a giustificare le seguenti due operazioni.

La prima cosa è aggiungere un outlet alla dichiarazione della classe CatalogDoc:

IBOutlet NSOutlineView        * outlineView ;

figura 05

figura 05

Già che ci sono, ho esplicitamente dichiarato l'outlet del tipo corretto, in modo da facilitare il lavoro al compilatore e all'applicazione. Ora, bisogna che questa dichiarazione di classe sia comprensibile al file nib. Dall'interno di IB bisogna selezionare il pannello Classes e la voce Read Files... dal menu Classes. Da qui, occorre leggere appunto il file CatalogDoc.h; in questo modo l'oggetto File's Owner, che ho tipizzato essere un CatalogDoc, presenta nella palette di Info, lo outlet che ho dichiarato. La seconda operazione è collegare a questo outlet la outlineView contenuta nella finestra, col solito meccanismo del control-drag. Noto che connetto direttamente il File's Owner, non devo generare alcuna istanza della classe CatalogDoc, come avevo fatto negli esempi precedenti.

L'interfaccia è terminata, torno in XCode.

Quattro metodi

Devo completare la dichiarazione della classe CatalogDoc. Mi rifaccio alla vecchia classe FileInfoCtrl, e vedo che mi manca in pratica solo l'oggetto di classe LSDataSource. Anticipo anche la dichiarazione di un oggetto della classe NSData, che servirà a contenere i dati contenuti nella finestra.

NSData         * dataFromFile ;
LSDataSource * dataSource;

Per completare la realizzazione della classe CatalogDoc, si devono (come stabilito da documentazione) definire quattro metodi, che vado nell'ordine ad elencare:

- (NSString *)windowNibName ;
- (void)windowControllerDidLoadNib:(NSWindowController *) aController ;
- (NSData *)dataRepresentationOfType:(NSString *)aType ;
- (BOOL)loadDataRepresentation:(NSData *)data ofType:(NSString *)aType ;

Il primo metodo deve restituire il nome del file nib che contiene gli oggetti che rappresentano il documento a video. Con questo metodo NSDocumentController è in grado di selezionare il file nib appropriato per ogni tipo di documento. Il secondo metodo è chiamato al termine del caricamento del file nib; penso che qui inserirò tutte le istruzioni che negli esempi precedenti inserivo nel metodo awakeFromNib. Il terzo ed il quarto metodo servono per salvare e leggere i dati del documento da un file su disco. Il terzo metodo deve restituire, come oggetto della classe NSData, una rappresentazione del documento adatta ad essere salvata su disco. Il quarto metodo, infine, esegue l'operazione inversa: dai dati salvati su disco e forniti sotto forma di NSData, deve ricostruire i dati del documento e mostrarli a video. Eccoli uno alla volta. Il primo è molto facile:

- (NSString *)windowNibName
{
    return @"CatalogWin";
}

Avendo chiamato il file nib CatalogWin.nib, devo restituire la stringa senza estensione. Il secondo metodo ricorda molto awakeFromNib degli esempi dei capitoli precedenti, ma con qualche complicazione in più. Infatti, non ho qui la semplificazione dell'associazione automatica degli outlet al caricamento del file nib. Mi devo quindi arrangiare con le chiamate apposite.

- (void)windowControllerDidLoadNib:(NSWindowController *) aController
{

    TOS9TCForm     *myDF1 = [[[ TOS9TCForm alloc ] init ] autorelease ];
    FileSizeForm     *myDF2 = [[[ FileSizeForm alloc ] init ] autorelease ];

    [super windowControllerDidLoadNib:aController];
    
    [[[ outlineView tableColumnWithIdentifier: @"typeCode" ] dataCell ] setFormatter: myDF1 ];
    [[[ outlineView tableColumnWithIdentifier: @"creatorCode"] dataCell ] setFormatter: myDF1 ];
    [[[ outlineView tableColumnWithIdentifier: @"fileSize"]    dataCell ] setFormatter: myDF2 ];

    // devo costruire l'oggetto sorgente dei dati
    [ self setDataSource: [[LSDataSource alloc] init ] ];
    // e' la sorgente di dati di outlineView
    [ outlineView setDataSource: dataSource];

    [ self loadCatalogWithData: dataFromFile] ;
}

Lascio per il momento fuori dalla considerazione l'ultima riga del metodo, e passo al terzo metodo.

È una semplificazione del metodo save2File dell'esempio precedente, ho tolto tutto il codice relativo al panello Save, che sarà svolto diligentemente da NSDocumentController, e modifico quanto rimane. In pratica, l'istruzione per archiviare gli oggetti. Questa volta, invece che su file, lo faccio su un oggetto NSData, utilizzando l'apposito metodo:

- (NSData *)dataRepresentationOfType:(NSString *)aType
{
    return ( [NSArchiver archivedDataWithRootObject: [dataSource startPoint] ] );
}

Ed adesso, il metodo contrario: NSDocumentController fornisce i servizi per la selezione di un file in lettura, e passa un oggetto NSData al metodo seguente, che deve preoccuparsi di mostrare questi dati. Alla fine, mi viene fuori una cosa semplicissima:

- (BOOL)loadDataRepresentation:(NSData *)data ofType:(NSString *)aType
{
    if ( outlineView )
    {
        [ self loadCatalogWithData: data ] ;
    }
    else
    {
        dataFromFile = [ data retain ];
    }
    return ( YES );
}

Il metodo serve ad eseguire due operazioni differenti. Il primo caso, quando lo outlet outlineView esiste già, si verifica ad esempio quando l'utente sceglie di ritornare ad una versione di documento precedentemente salvata. Nel caso invece di una nuova finestra per il documento, è attivo il secondo caso, dal momento che questo metodo è chiamato prima del caricamento del file nib relativo. Tutto quello che devo fare è mantenere vivo l'oggetto data, assegnandolo (dopo un retain) alla variabile d'istanza. Ma allora, quando è che i dati vengono inseriti all'interno della finestra, ovvero, in altre parole, quando è chiamata il metodo loadCatalogWithData che fa tutto il lavoro sporco? Ecco spiegata l'ultima riga del metodo windowControllerDidLoadNib:, chiamato appunto al termine del caricamento del nib.

Riassumendo: se si seleziona un file in seguito ad una chiamata Apri... da menu, NSDocumentController fornisce i servizi base, che produce un oggetto NSData e lo passa al metodo loadDataRepresentation:. Costui piglia i dati, outlineView non esiste ancora, e li inserisce su dataFromFile. Poi NSDocumentController carica il file nib e crea la finestra; al termine, esegue windowControllerDidLoadNib:, che esegue le sue inizializzazioni e finalmente chiama loadCatalogWithData come ultima istruzione.

Se invece l'utente invoca New o Nuovo da menu, NSDocumentController salta al caricamento del file nib e chiude con windowControllerDidLoadNib:. Ancora una volta, quindi, loadCatalogWithData deve trattare due casi: i dati ci sono (Apri) oppure non ci sono (Nuovo documento).

E quindi, ecco il codice:

- (void) loadCatalogWithData: (NSData *) data
{
    if ( data )
    {
        NSMutableArray * catData = [NSUnarchiver unarchiveObjectWithData: data ];
        // assegno il tutto alla sorgente di dati
        [ dataSource setStartPoint: catData ] ;
        // dico che sono cambiate le cose
        [ outlineView reloadData ];
    }
    else
    {
        [ dataSource setStartPoint: nil ] ;
        // dico che sono cambiate le cose
        [ outlineView reloadData ];
    }
}

Nel primo caso, estraggo i dati del documento dall'oggetto NSData, utilizzando il metodo inverso dell'archiviazione. Questi dati rappresentano i punti iniziali della outlineView, che quindi assegno e informo delle modifiche. Nel secondo caso, inizializzo i dati e nil.

Compilo e provo. Funziona. Che meraviglia.

Adesso però devo vedere che fare dei menu.

Licenza Creative Commons
Eccetto dove diversamente specificato, i contenuti di questo sito sono rilasciati sotto Licenza Creative Commons.
Pagina a cura di Livio Sandel (macocoa2012@gmail.com).