Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial

Lavorare con lo storage

Link copiato negli appunti

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 mette a disposizione 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:

private async void ChooseFile_Click(Object sender, RoutedEventArgs e)
{
    var picker = new FileOpenPicker();
    picker.FileTypeFilter.Add(".jpg");
    var file = await picker.PickSingleFileAsync();
}

La proprietà FileTypeFilter specifica il tipo di file da mostrare nell'interfaccia utente. È possibile aggiungere nuovi tipi alla collection, semplicemente indicando l'estensione del file preceduta da un punto. È 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 come parametro "*", come mostrato qui di seguito:

picker.FileTypeFilter.Add("*");

Il metodo Clear permette invece di eliminare dala 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à:

private async void ChooseFile_Click(Object sender, RoutedEventArgs e)
{
    var picker = new FileOpenPicker();
    picker.ViewMode = PickerViewMode.Thumbnail;
    picker.SuggestedStartLocation = PickerLocationId.PicturesLibrary;
    picker.FileTypeFilter.Add(".jpg");
    picker.FileTypeFilter.Add(".jpeg");
    picker.FileTypeFilter.Add(".png");
    StorageFile file = await picker.PickSingleFileAsync();
    if (file != null)
    {
        // esegue operazioni sul file
    }
    else
    {
        // l'operazione è stata cancellata dall'utente
    }
}

Infine, per consentire all'utente di selezionare più file, la classe FileOpenPicker espone anche il metodo asincrono PickMultipleFileAsync. Questo metodo restituisce una collezione di tipo IReadOnlyCollection<StorageFile>. Il prossimo snippet mostra questo punto:

private async void ChooseMultipleFile_Click(object sender, RoutedEventArgs e)
{
    var picker = new FileOpenPicker();
    picker.FileTypeFilter.Add(".jpg");
    try
    {
        var files = await picker.PickMultipleFilesAsync();
        foreach (var file in files)
        {
        }
    }
    catch (Exception ex)
    {
        // gestire eccezione
    }
}

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:

private async void ChooseFolder_Click(object sender, RoutedEventArgs e)
{
    try
    {
        var picker = new FolderPicker();
        picker.FileTypeFilter.Add("*");
        picker.SuggestedStartLocation = PickerLocationId.PicturesLibrary;
        StorageFolder folder = await picker.PickSingleFolderAsync();
    }
    catch (Exception ex)
    {
        // gestire l'eccezione
    }
}

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:

private async void SaveSampleFile(object sender, RoutedEventArgs e)
{
    try
    {
        var picker = new FileSavePicker();
        picker.SuggestedFileName = "sample.txt";
        picker.SuggestedStartLocation = PickerLocationId.DocumentsLibrary;
        picker.FileTypeChoices.Add("Plain Text", new List<string>() { ".txt" });
        picker.DefaultFileExtension = ".txt";
        StorageFile file = await picker.PickSaveFileAsync();
        if (file != null)
        {
            await FileIO.WriteTextAsync(file, "ciao da Html.it");
        }
    }
    catch (Exception ex)
    {
        // gestire l'eccezione
    }
}

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:

private async void SaveSampleFile(object sender, RoutedEventArgs e)
{
    try
    {
        var picker = new FileSavePicker();
        picker.SuggestedFileName = "sample.txt";
        picker.SuggestedStartLocation = PickerLocationId.DocumentsLibrary;
        picker.FileTypeChoices.Add("Plain Text", new List() { ".txt" });
        picker.DefaultFileExtension = ".txt";
        StorageFile file = await picker.PickSaveFileAsync();
        if (file != null)
        {
                CachedFileManager.DeferUpdates(file);
                await FileIO.WriteTextAsync(file, "ciao da Html.it");
                FileUpdateStatus status = await CachedFileManager.CompleteUpdatesAsync(file);
        }
    }
    catch (Exception ex)
    {
        // gestire l'eccezione
    }
}

Per testare il codice relativo ai diversi picker illustrati finora, possiamo usare la seguente definizione XAML come riferimento per la pagina principale dell'applicazione:

<Page
    x:Class="Demo.Html.it.StorageSample.CS.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:Demo.Html.it.StorageSample.CS"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">
    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <StackPanel>
            <Button Click="ChooseFile_Click">Scegli un file</Button>
            <Button Click="ChooseMultipleFile_Click">Selezione multipla</Button>
            <Button Click="ChooseFolder_Click">Scegli una cartella</Button>
            <Button Click="SaveSampleFile">Salva un file</Button>
        </StackPanel>
    </Grid>
</Page>

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:

<Page
    x:Class="Demo.Html.it.StorageSample.CS.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:Demo.Html.it.StorageSample.CS"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">
    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <StackPanel>
            <Button Content="Elenca i file" Click="ListFiles_Click" />
            <ListBox x:Name="FileList" DisplayMemberPath="Name" />
        </StackPanel>
    </Grid>
</Page>

Il codice collegato al click sul pulsante è il seguente:

private async void ListFiles_Click(object sender, RoutedEventArgs e)
{
    StorageFolder picturesFolder = KnownFolders.PicturesLibrary;
    IReadOnlyList fileList = await picturesFolder.GetFilesAsync();
    FileList.ItemsSource = fileList;
}

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, la classe StorageFolder espone il metodo GetItemsAsync.

È 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:

private async void GroupPhotosByYear_Click(object sender, RoutedEventArgs e)
{
    StorageFolder picturesFolder = KnownFolders.PicturesLibrary;
    StorageFolderQueryResult queryResult = picturesFolder.CreateFolderQuery(CommonFolderQuery.GroupByYear);
    IReadOnlyList folderList = await queryResult.GetFoldersAsync();
    FileList.ItemsSource = folderList;
}

Dopo aver recuerato 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. A questo punto, il metodo GetFolderAsync recupera l'elenco dei gruppi di file per poi mostrarli a video.

Per testare questo codice, è sufficiente aggiungere un nuovo controllo Button al codice XAML illustrato in precedenza:

<StackPanel>
    <Button Content="Elenca i file" Click="ListFiles_Click" />
    <Button Content="Raggruppa foto" Click="GroupPhotosByYear_Click"></Button>
    <ListBox x:Name="FileList" DisplayMemberPath="Name" />
</StackPanel>

Come si può vedere nella prossima immagine, i file sono mostrati raggruppati per anno:

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'enum 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 hanno 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 all'interno di una cartella:

var folder = KnownFolders.DocumentsLibrary;
var logFile = await folder.CreateFileAsync("log.txt");

A questo punto, è possibile scrivere sul file tramite il metodo Windows.Storage.FileIO.WriteTextAsync:

await Windows.Storage.FileIO.WriteTextAsync(logFile, "Esempio di log");

Se invece volessimo scrivere un array di byte, anziché del testo, potremmo usare il seguente codice:

var buffer = Windows.Security.Cryptography.CryptographicBuffer.ConvertStringToBinary("Ciao da Html.it", Windows.Security.Cryptography.BinaryStringEncoding.Utf8);
var folder = KnownFolders.DocumentsLibrary;
var file = await folder.CreateFileAsync("document.txt");
await Windows.Storage.FileIO.WriteBufferAsync(file, buffer);

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:

var folder = KnownFolders.DocumentsLibrary;
var file = await storageFolder.CreateFileAsync("document.txt");
var stream = await file.OpenAsync(Windows.Storage.FileAccessMode.ReadWrite);
using (var outputStream = stream.GetOutputStreamAt(0))
{
    DataWriter dataWriter = new DataWriter(outputStream);
    dataWriter.WriteString("Ciao da Html.it");
    await dataWriter.StoreAsync();
    await outputStream.FlushAsync();
}

Il codice apre il file in modalità ReadWrite, crea lo stream tramite il metodo GetOutputStreamAt e quindi usa la classe DataWriter per scrivere una stringa.

È importante ricordarsi di chiamare i metodi StoreAsync e FlushAsync per, rispettivamente, salvare il file e chiudere lo stream, nonché di effettuare la dispose dello stream (in questo caso tramite una using).

Per leggere un file, invece, il codice è decisamente più semplice:

var folder = KnownFolders.DocumentsLibrary;
var file = await folder.GetFileAsync("document.txt");
string text = await Windows.Storage.FileIO.ReadTextAsync(file);

Il codice che segue sfrutta invece il metodo ReadBufferAsync per leggere un array di byte:

var folder = KnownFolders.DocumentsLibrary;
var file = await folder.GetFileAsync("document.txt");
var buffer = await Windows.Storage.FileIO.ReadBufferAsync(file);
DataReader dataReader = Windows.Storage.Streams.DataReader.FromBuffer(buffer);
String text = dataReader.ReadString(buffer.Length);

Infine, per leggere uno stream da file, possiamo usare un codice simile a quello che segue:

var folder = KnownFolders.DocumentsLibrary;
var file = await folder.GetFileAsync("document.txt");
var stream = await file.OpenAsync(Windows.Storage.FileAccessMode.Read);
var size = stream.Size;
using (var inputStream = stream.GetInputStreamAt(0))
{
    DataReader dataReader = new DataReader(inputStream);
    uint numBytesLoaded = await dataReader.LoadAsync((uint)size);
    string text = dataReader.ReadString(numBytesLoaded);
}

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, è necessario implementare il metodo OnFileActivated nella classe App della tua applicazione, come mostrato di seguito:

protected override void OnFileActivated(FileActivatedEventArgs args)
{
    foreach (var file in args.Files)
    {
        // codice omesso
    }
}

Comprimere file per risparmiare spazio su disco

Dal momento che in un'applicazione Windows Store non è possibile referenziare l'assembly System.IO.Compression.FileSystem, né sfruttare la classe ZipArchive, per comprimere o decomprimere dei file è possibile sfruttare, rispettivamente, le classi GZipStream e Compressor, e le classi DeflateStream e Decompressor.

Il prossimo snippet mostra un esempio di compressione di un file:

public async void Compress(StorageFile fileToCompress)
{
    using (var originalFileStream = await fileToCompress.OpenReadAsync())
    {
        var compressedFileStream = await KnownFolders.DocumentsLibrary.CreateFileAsync(fileToCompress.Name + ".gz");
        using (var compressedOutput = await compressedFileStream.OpenAsync(FileAccessMode.ReadWrite))
        {
            using (var compressor = new Compressor(compressedOutput.GetOutputStreamAt(0)))
            {
                var bytesCompressed = await RandomAccessStream.CopyAsync(originalFileStream, compressor);
                var finished = await compressor.FinishAsync();
            }
        }
    }
}

Una volta creato il file di destinazione tramite il metodo StorageFile.CreateFileAsync, il codice crea un nuovo oggetto Compressor, copia il file originale in quello compresso tramite il metodo statico RandomAccessStream.CopyAsync, quindi finalizza l'operazione invocando il metodo FinishAsync della classe Compressor. Il prossimo snippet mostra invece il procedimento inverso, ovvero di decompressione di un file zippato.

public async void Decompress(StorageFile fileToDecompress)
{
    using (var compressedFileStream = await fileToDecompress.OpenSequentialReadAsync())
    {
        var decompresedFileStream = await KnownFolders.DocumentsLibrary.CreateFileAsync(fileToDecompress.Name + ".original");
        using (var decompressedOutput = await decompresedFileStream.OpenAsync(FileAccessMode.ReadWrite))
        {
            using (var decompressor = new Windows.Storage.Compression.Decompressor(compressedFileStream))
            {
                var bytesDecompressed = await Windows.Storage.Streams.RandomAccessStream.CopyAsync(decompressor, decompressedOutput);
            }
        }
    }
}

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 impostazioni applicative per arrivare alla gestione di file, stream e estensioni.


Ti consigliamo anche