Nell'articolo precedente abbiamo visto come Windows metta a disposizione numerose opzioni per persistere i dati e le impostazioni applicative. In questo articolo, vedremo invece come sfruttare le API di WinRT per salvare o leggere file e cartelle su filesystem, gestire lo stream dei file, impostare le estensioni e le associazioni dei file, accedere alle librerie dell'utente, selezionare file e cartelle tramite picker, e così via.
Accedere a file e cartelle tramite picker
WinRT fornisce diverse tipologie di picker per svolgere varie operazioni sul filesystem: un picker per selezionare file (FileOpenPicker
), un altro per selezionare cartelle (FolderPicker
), nonché un picker per salvare file su disco (FileSavePicker
). Vediamo come funzionano i vari picker in dettaglio.
In particolare, un file picker consente all'utente di selezionare cartelle e file tramite un'interfaccia utente standard. Una volta che l'utente ha selezionato il file o la cartella, WinRT restituisce il controllo al chiamante, in modo da poter eseguire una qualche operazione sull'elemento selezionato. La prossima immagine mostra un esempio di file picker in azione:
La logica del controllo è incapsulata dalla classe FileOpenPicker
. Per attivare il picker, è sufficiente istanziare un nuovo oggetto di tipo FileOpenPicker
, indicando se vogliamo selezionare uno o più file e per quali tipi di file filtrare la ricerca. Nel codice che segue, l'apertura del picker è collegata al click su un pulsante:
function chooseFile_click(args) {
var picker = new Windows.Storage.Pickers.FileOpenPicker();
picker.fileTypeFilter.replaceAll([".jpg"]);
picker.pickSingleFileAsync().then(function(file){
if (file == null){
// il file non esiste
}
else {
// operazione su file
}
});
}
La proprietà FileTypeFilter
specifica il tipo di file da mostrare nell'interfaccia utente. La funzione replaceAll
permette di sostituire qualunque filtro eventualmente già aggiunto alla collection con quelli indicati nell'array passato come parametro. È importante notare che la collection non può essere vuota: se non viene specificato almeno un tipo di file, il sistema solleverà un'eccezione di tipo ComException
nel momento in cui il picker viene attivato. Per dare all'utente la possibilità di selezionare qualunque tipo di file, è sufficiente passare un array con un unico elemento, "*", come mostrato qui di seguito:
picker.fileTypeFilter.replaceAll(["*"]);
La funzione clear
permette invece di eliminare dalla collection i filtri aggiunti in precedenza:
picker.FileTypeFilter.clear();
Il metodo asincrono PickSingleFileAsync
, infine, attiva l'interfaccia utente del picker per la selezione di un singolo file. Una volta che l'utente ha selezionato il file del tipo richiesto (in questo caso un jpeg) o annullato l'operazione premendo sul pulsante Cancel
, WinRT restituisce il controllo al chiamante. Nel primo caso, il risultato sarà un oggetto di tipo Windows.Storage.StorageFile
, mentre nel caso di cancellazione dell'operazione il metodo restituirà null
.
La prossima immagine mostra il flusso completo:
(fonte: MSDN)
Come si può notare, l'applicazione richiede al sistema di attivare il file picker per consentire all'utente di selezionare un file. WinRT chiama il componente e mostra l'interfaccia utente con l'elenco dei file e delle cartelle. A questo punto, se l'utente seleziona il file, il picker restituirà al chiamante l'elemento selezionato.
Se invece l'utente seleziona un provider diverso (è infatti possibile registrare un'applicazione come file picker provider, personalizzando così l'interfaccia utente. Per farlo, è sufficiente implementare il relativo contratto; su questo punto si rinvia alla documentazione ufficiale MSDN), il sistema attiverà l'applicazione che funge da provider e la relativa interfaccia verrà mostrata all'utente. La prossima immagine mostra l'applicazione Sound Recorder elencata come file picker provider.
La classe FileOpenPicker
contiene una serie di proprietà che permettono di specificare meglio una serie di opzioni. Ad esempio, la proprietà ViewMode
permette di specificare, tramite l'enum PickerViewMode
, come rappresentare i file a video (se come elenco testuale ovvero come serie di thumbnail). La proprietà SuggestedStartLocation
, invece, indica al picker da dove iniziare a cercare i file da mostrare all'utente (ad esempio, la libreria immagini dell'utente). Il prossimo codice mostra un esempio di queste proprietà:
function chooseFile_click(args) {
var picker = new Windows.Storage.Pickers.FileOpenPicker();
picker.fileTypeFilter.replaceAll([".jpg", ".png", ".bmp"]);
picker.viewMode = Windows.Storage.Pickers.PickerViewMode.thumbnail;
picker.suggestedStartLocation = Windows.Storage.Pickers.PickerLocationId.picturesLibrary;
picker.pickSingleFileAsync().done(function(file){
if (file == null){
// il file non esiste
}
else {
// operazione su file
}
});
}
Infine, per consentire all'utente di selezionare più file, la classe FileOpenPicker
espone anche il metodo asincrono PickMultipleFileAsync, il quale restituisce un array di oggetti di tipo StorageFile
. Il prossimo snippet illustra questo punto:
function chooseMultipleFiles_click(args) {
var picker = new Windows.Storage.Pickers.FileOpenPicker();
picker.fileTypeFilter.replaceAll([".jpg", ".png", ".bmp"]);
picker.viewMode = Windows.Storage.Pickers.PickerViewMode.thumbnail;
picker.suggestedStartLocation = Windows.Storage.Pickers.PickerLocationId.picturesLibrary;
picker.pickMultipleFilesAsync().done(function (files) {
if (files.size > 0) {
for (var i = 0; i < files.size; i++) {
// operazione su file
}
}
});
}
Oltre che selezionare file, un picker consente anche di selezionare cartelle. La classe responsabile per questa operazione è la classe FolderPicker
, la quale espone metodi e proprietà simili a quelli già visti con la classe FileOpenPicker
(ma che, ovviamente, in questo caso hanno a che fare con cartelle, anziché con file).
Il prossimo snippet ne mostra un esempio:
function chooseFolder_click(args) {
var picker = new Windows.Storage.Pickers.FolderPicker();
picker.fileTypeFilter.replaceAll(["*"]);
picker.suggestedStartLocation = Windows.Storage.Pickers.PickerLocationId.computerFolder;
picker.pickSingleFolderAsync().then(function (folder) {
if (folder == null) {
// nessuna directory selezionata
}
else {
// codice omesso
}
});
}
Il codice, dopo aver istanziato un nuovo FolderPicker
e impostati filtri e punto di partenza, invoca il metodo PickSingleFolderAsync
, il quale attiva l'interfaccia utente del picker per consentire all'utente di selezionare una cartella. Una volta che questa è stata selezionata dall'utente, il controllo viene restituito al chiamante, assieme a un oggetto di tipo StorageFolder
che rappresenta la selezione dell'utente (o null
, se l'utente annulla l'operazione).
Così come ci sono picker per selezionare file o cartelle, WinRT espone anche picker per il salvataggio di file. La classe responsabile per queste operazioni è la classe FileSavePicker
.
Eccone un esempio:
function saveFile_click(args) {
var picker = new Windows.Storage.Pickers.FileSavePicker();
picker.suggestedFileName = "sample.txt";
picker.defaultFileExtension = ".txt";
picker.fileTypeChoices.insert("Plain Text", [".txt"]);
picker.suggestedStartLocation = Windows.Storage.Pickers.PickerLocationId.documentsLibrary;
picker.pickSaveFileAsync().then(function (file) {
if (file != null) {
Windows.Storage.FileIO.writeTextAsync(file, "Ciao da Html.it").done(function () {
// notifica all'utente
});
}
});
}
Il codice, dopo aver istanziato un nuovo oggetto FileSavePicker
, imposta (tramite la proprietà SuggestedFileName
) il nome del file da suggerire all'utente non appena si aprirà l'interfaccia utente del picker, indica la cartella di partenza del picker (SuggestedStartLocation
), definisce l'elenco delle estensioni ammesse per quel file (da aggiungere alla collection FileTypeChoices
) e l'estensione di default del file, quindi invoca il metodo PickSaveFileAsync
. Questo metodo restituisce un oggetto StorageFile
che rappresenta il file salvato dall'utente (con relativo nome, estensione e posizione su filesystem). Se invece l'utente annulla l'operazione, il valore restituito sarà null
.
A questo punto, possiamo usare questo oggetto per le nostre operazioni di scrittura.
Tuttavia, prima di eseguire queste operazioni è importante evitare che il file sia modificato prima che le operazioni di salvataggio siano completate. A questo fine, possiamo sfruttare la classe CachedFileManager
per operare un lock del file:
function saveFile_click(args) {
var picker = new Windows.Storage.Pickers.FileSavePicker();
picker.suggestedFileName = "sample.txt";
picker.defaultFileExtension = ".txt";
picker.fileTypeChoices.insert("Plain Text", [".txt"]);
picker.suggestedStartLocation = Windows.Storage.Pickers.PickerLocationId.documentsLibrary;
picker.pickSaveFileAsync().then(function (file) {
if (file != null) {
Windows.Storage.CachedFileManager.deferUpdates(file);
Windows.Storage.FileIO.writeTextAsync(file, "Ciao da Html.it")
.then(function () {
return Windows.Storage.CachedFileManager.completeUpdatesAsync(file);
})
.done(function (status) {
if (status == Windows.Storage.Provider.FileUpdateStatus.complete) {
// operazione completata con successo
}
else {
// qualcosa è andato storto
}
});
}
});
}
Per testare il codice relativo ai diversi picker illustrati finora, possiamo usare la seguente definizione XAML come riferimento per la pagina principale dell'applicazione:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Demo.Html.it.StorageSample.JS</title>
<!-- WinJS references -->
<link href="//Microsoft.WinJS.2.0/css/ui-dark.css" rel="stylesheet" />
<script src="//Microsoft.WinJS.2.0/js/base.js"></script>
<script src="//Microsoft.WinJS.2.0/js/ui.js"></script>
<!-- Demo.Html.it.StorageSample.JS references -->
<link href="/css/default.css" rel="stylesheet" />
<script src="/js/default.js"></script>
</head>
<body>
<button id="btnChooseFile">Scegli un file</button>
<button id="btnChooseMultipleFiles">Scegli più file</button>
<button id="btnChooseFolder">Seleziona una cartella</button>
<button id="btnSaveFile">Salva un file</button>
</body>
</html>
Ricordiamo anche di sottoscrivere gli eventi di click dei vari pulsanti, come illustrato qui di seguito:
app.onloaded = function () {
btnChooseFile.addEventListener("click", chooseFile_click);
btnChooseMultipleFiles.addEventListener("click", chooseMultipleFiles_click);
btnChooseFolder.addEventListener("click", chooseFolder_click);
btnSaveFile.addEventListener("click", saveFile_click);
};
Accedere a file e cartelle programmaticamente
Oltre che tramite picker, un'applicazione può accedere a file e cartelle presenti su filesystem via codice. Nel caso in cui si tratti di accedere alle librerie dell'utente (Music Library, Videos Library e Pictures Library), l'applicazione deve espressamente dichiarare le relative "capability" nell'application manifest, come mostrato nella prossima immagine.
Dopo aver dichiarato le capability nell'application manifest, è possibile accedere programmaticamente alle librerie utente. Ad esempio, il seguente codice sfrutta la classe StorageFolder
per elencare i file contenuti nella cartella Pictures dell'utente:
function getAllUserPictures_click(args){
var picturesFolder = Windows.Storage.KnownFolders.picturesLibrary;
picturesFolder.getFilesAsync()
.done(function (files) {
if (files.size > 0) {
files.forEach(function (item) {
// codice omesso
});
}
});
}
Il codice utilizza il metodo StorageFolder.GetFileAsync
per recuperare l'elenco dei file presenti nella libreria utente; nel caso volessimo recuperare anche l'elenco delle cartelle, oltre a quello dei file, avremmo potuto usare il metodo GetItemsAsync
. In questo caso, il codice sarebbe stato simile al seguente:
var picturesFolder = Windows.Storage.KnownFolders.picturesLibrary;
picturesLibrary.getItemsAsync().done(function (items) {
items.forEach(function (item) {
if (item.isOfType(Windows.Storage.StorageItemTypes.folder)) {
// item è di tipo StorageFolder
}
else {
// item è di tipo StorageFile
}
});
});
È anche possibile cercare file e cartelle specifiche, ed eventualmente raggrupparli sulla base di una delle loro proprietà (come ad esempio la data di creazione), grazie al metodo CreateFolderQuery
, come mostrato nel prossimo snippet:
function groupPhotoByYear_click(args) {
var picturesFolder = Windows.Storage.KnownFolders.picturesLibrary;
var queryResult = picturesFolder.createFolderQuery(Windows.Storage.Search.CommonFolderQuery.groupByYear);
queryResult.getFoldersAsync().done(function (folders) {
if (folders.size > 0) {
folders.forEach(function (year) {
// gruppo di foto per anno
});
}
});
}
Dopo aver recuperato una reference alla libreria delle immagini, il codice invoca il metodo CreateFolderQuery
, il quale restituisce un oggetto di tipo StorageFolderQueryResult
che enumera i file nella directory corrispondente, filtrandoli e raggruppandoli sulla base della proprietà indicata dall'enum CommonFolderQuery
(in questo caso, per anno). A questo punto, il metodo GetFolderAsync
recupera l'elenco dei gruppi di file.
Per testare il codice qui presentato, puoi usare la seguente definizione HTML come riferimento per la pagina di defaul della tua app:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Demo.Html.it.StorageSample.JS</title>
<!-- WinJS references -->
<link href="//Microsoft.WinJS.2.0/css/ui-dark.css" rel="stylesheet" />
<script src="//Microsoft.WinJS.2.0/js/base.js"></script>
<script src="//Microsoft.WinJS.2.0/js/ui.js"></script>
<!-- Demo.Html.it.StorageSample.JS references -->
<link href="/css/default.css" rel="stylesheet" />
<script src="/js/default.js"></script>
</head>
<body>
<button id="btnGetAllFiles">Seleziona tutte le immagini</button>
<button id="btnGroupPhotos">Raggruppa immagini per anno</button>
</body>
</html>
Nel file JavaScript, non dimentichiamo di "abbonarci" all'evento di click:
app.onloaded = function () {
btnGetAllFiles.addEventListener("click", getAllUserPictures_click);
btnGroupPhotos.addEventListener("click", groupPhotoByYear_click);
};
Le librerie utente non sono le uniche a cui è possibile accedere programmaticamente (previa aggiunta della relativa capability nell'application manifest; sull'argomento si veda la documentazione ufficiale MSDN). L'enumeration KnowFolders
, in particolare, elenca i vari percorsi associati alle Windows Storage API:
CameraRoll
DocumentsLibrary
HomeGroup
MediaServerDevices
MusicLibrary
PicturesLibrary
Playlists
RemovableDevices
SavedPictures
VideosLibrary
È importante notare come nel passaggio da Windows 8 a Windows 8.1, alcuni di questi percorsi abbiano subito dei cambiamenti: ad esempio, in un'applicazione Windows Store 8.1, non è più possibile accedere programmaticamente alla Documents Library (che resta dunque accessibile solo mediante picker), mentre la StorageFolder
CameraRoll è specifica per Windows 8.1.
Lavorare con cartelle, file e stream
Una volta recuperata la reference verso un file o una directory, è possibile eseguire operazioni di lettura e/o scrittura. Ad esempio, il prossimo snippet mostra come creare un file di testo all'interno di una cartella per loggare, ad esempio, eventuali operazioni dell'utente:
try {
var folder = Windows.Storage.KnownFolders.documentsLibrary;
folder.createFileAsync("log.txt", Windows.Storage.CreationCollisionOption.replaceExisting)
.then(function (logFile) {
Windows.Storage.FileIO.writeTextAsync(logFile, "Operazione da loggare").done();
});
}
catch (e) {
// gestire l'eccezione
}
Se invece volessimo scrivere un array di byte, anziché del testo, potremmo usare il seguente codice:
try {
var buffer = Windows.Security.Cryptography.CryptographicBuffer.convertStringToBinary("Ciao da Html.it",
Windows.Security.Cryptography.BinaryStringEncoding.utf8);
var folder = Windows.Storage.KnownFolders.documentsLibrary;
folder.createFileAsync("sample.txt", Windows.Storage.CreationCollisionOption.replaceExisting)
.then(function (doc) {
// doc è di tipo StorageFile
Windows.Storage.FileIO.writeBufferAsync(doc, buffer);
});
} catch (e) {
// gestire l'eccezione
});
Questo codice converte la stringa in un oggetto binario e quindi scrive il buffer nel file passato come primo parametro al metodo WriteBufferAsync
.
Il prossimo snippet mostra invece come salvare uno stream su file:
function saveStream_click(args) {
var folder = Windows.Storage.KnownFolders.documentsLibrary;
folder.createFileAsync("sample.txt", Windows.Storage.CreationCollisionOption.replaceExisting)
.then(function (doc) {
return doc.openTransactedWriteAsync()
})
.done(writeTheStreamToDoc);
}
function writeTheStreamToDoc(tx) {
var dataWriter = new Windows.Storage.Streams.DataWriter(tx.stream);
dataWriter.writeString("Ciao da Html.it");
dataWriter.storeAsync()
.then(function () {
return tx.commitAsync();
}).done(function () {
tx.close();
});
}
Il codice crea in primo luogo un nuovo file, quindi invoca il metodo OpenTransactedWriteAsync
(esposto dalla classe StorageFile
). Questa funzione apre uno stream verso il file, restituendo un oggetto di tipo StorageStreamTransaction
che rappresenta un'operazione di scrittura (transazionale) verso il file stesso. L'operazione di scrittura vera e propria è portata avanti dalla classe DataWriter
tramite il suo metodo WriteString
. Una volta che la scrittura è terminata, il codice invoca il metodo CommitAsync
per concludere la transazione, quindi il metodo Close
chiude lo stream e rilasciare le risorse.
Per leggere un file di testo, invece, il codice è decisamente più semplice:
var folder = Windows.Storage.KnownFolders.documentsLibrary;
folder.getFileAsync("sample.txt")
.then(function (doc) {
return Windows.Storage.FileIO.readTextAsync(doc);
}).then(function (content) {
// content rappresenta il contenuto del file
});
Il codice che segue sfrutta invece il metodo ReadBufferAsync
per leggere un array di byte:
var folder = Windows.Storage.KnownFolders.documentsLibrary;
folder.getFileAsync("sample.txt")
.then(function (doc) {
return Windows.Storage.FileIO.readBufferAsync(doc)})
.done(function (buffer) {
var dataReader = Windows.Storage.Streams.DataReader.fromBuffer(buffer);
var text = dataReader.readString(buffer.length);
});
});
Infine, per leggere uno stream da file, possiamo usare un codice simile a quello che segue:
function openStream_click(args) {
var folder = Windows.Storage.KnownFolders.documentsLibrary;
folder.getFileAsync("sample.txt")
.then(function (doc) {
return doc.openAsync(Windows.Storage.FileAccessMode.readWrite);
})
.then(readTheStreamFromDoc);
}
function readTheStreamFromDoc(stream) {
var dataReader = new Windows.Storage.Streams.DataReader(stream);
dataReader.loadAsync(stream.size).done(function (bytes) {
var text = dataReader.readString(bytes);
dataReader.close();
});
}
Gestire estensioni e associazioni di file
Windows consente a un'applicazione Windows Store di registrarsi come handler di default per determinati tipi di file. In questo caso, l'app verrà eseguita ogni volta che l'utente eseguirà quel particolare tipo di file. Se decidi di registrare la tua applicazione, è importante che questa sia in grado di offrire agli utenti tutte quelle funzionalità che questi si aspettano per quel tipo di file.
La prima cosa da fare è dichiarare la relativa funzionalità nell'application manifest, come mostrato nella prossima immagine:
Una volta aggiunta la relativa dichiarazione, per conoscere quali sono i file eseguiti dall'utente è sufficiente intercettare il corrispondente tipo di attivazione (espresso tramite l'enum Windows.ApplicationModel.ActivationKind
) e ispezionare gli argomenti ricevuti come parametro dal relativo event handler, come mostrato qui di seguito:
app.onactivated = function (args) {
...
if (args.detail.kind === Windows.ApplicationModel.Activation.ActivationKind.file) {
// i file selezionati dall'utente sono accessibili tramite la proprietà
// args.detail.files
}
};
Come abbiamo visto, WinRT espone numerose API per soddisfare qualunque esigenza applicativa, dalla semplice memorizzazione di dati nel profilo dell'utente, alla gestione di dictionary con i dati e settaggi applicativi per arrivare alla gestione di file, stream ed estensioni.