20

Feb

Effettuare il parsing di un file XML in un’applicazione Metro style per Windows 8

Mi sto cimentando nel compito di portare una delle applicazioni sviluppate da me e Marco Leoncini (la serie dedicata alle ricette italiane) da Windows Phone a Windows 8: una delle prime sfide che mi sono trovato ad affrontare è il porting dei dati. L’applicazione Windows Phone si appoggia infatti ad un database SQL CE, che al momento non è compatibile con Windows 8. La soluzione più semplice da noi scelta è l’utilizzo di un file XML: alla fine la struttura del database era molto semplice ed era costituita da un’unica tabella con l’elenco delle ricette.

Nel mio caso, non ho la necessità di memorizzare dati nello storage: il file XML contenente l’elenco delle ricette è incluso nel progetto di Visual Studio, dato che deve essere di sola lettura, in quanto nella mia applicazione l’utente può solo consultare le ricette e e non modificarle.

Il primo problema era perciò capire come leggere il file presente all’interno del progetto: in Windows Phone avrei utilizzato ad esempio il metodo GetResourceStream esposto dalla classe Application, che permette di accedere proprio ai file inclusi all’interno del progetto. Dopodichè avrei sfruttato la classe XDocument per effettuare il parsing dell’XML e avrei utilizzato LINQ to XML per manipolare i dati. Ecco un esempio:

Uri uri = new Uri("recipe.xml", UriKind.Relative);
      StreamResourceInfo sri = Application.GetResourceStream(uri);
      XDocument doc = XDocument.Load(sri.Stream);

      // eseguo le query per recuperare i dati sul file XML

 

Il problema è che la classe Application non è disponibile in Windows 8 e quindi non si è in grado di utilizzare il metodo GetResourceStream. Spulciando nella documentazione di Windows 8 ho scoperto che tramite WinRT possiamo accedere a diversi tipi di storage: c’è quello locale dell’applicazione, ci sono le librerie di sistema (come la cartella Documents o Pictures) e così via. Uno di questi storage consente proprio di accedere alla “cartella” dell’applicazione, cartella che viene nascosta dal pacchetto preparato da Visual Studio nel formato .appx (che è l’equivalente dello XAP delle applicazioni Windows Phone, ovvero un pacchetto che contiene tutto il necessario per far funzionare l’applicazione, come librerie, contenuti, manifest, ecc.).

Una volta fatto il build della nostra applicazione Metro sytle andiamo nella cartella bin/Debug all’interno del nostro progetto: troveremo una cartella AppX, che rappresenta il contenuto del nostro pacchetto.

SNAGHTML73789ec

La prima cosa da fare è perciò quella di selezionare il file XML incluso nel nostro progetto e, dalla finestra Proprietà, impostare la Build Action su Content e Copy to Output Directory su Copy if newer. In questo modo il file XML verrà incluso all’interno del pacchetto.

A questo punto da codice possiamo accedervi grazie allo storage chiamato InstalledLocation, che espone il metodo GetFileAsync() che permette di recuperare un file dal pacchetto.

Importante! Eravamo già abituati con Windows Phone, ma in Windows 8 questo concetto è ancora più importante: tutte le operazioni che possono impiegare più di 50 millisecondi per essere completate sono asincrone in WinRT! La differenza, che ci semplifica un po’ l’utilizzo di questo metodo di programmazione, è che WinRT supporta nativamente le parole chiave async e await, introdotte in C# 5, che permettono di scrivere codice che all’apparenza sembra sincrono ma in realtà viene eseguito su thread differenti.

Il trucco in questo caso sta nel:

  • marcare la funzione che stiamo creando con la parola chiave async
  • anteporre alla chiamata al metodo asincrono la parola chiave await. In questo modo il codice che scriveremo sembrerà asincrono (una riga sotto l’altra), ma in realtà, grazie alla parola chiave await, le righe successive non verranno eseguite fino a che l’operazione asincrona non ha restituito un risultato.

Occhio all’inghippo

Prima di spiegarvi l’inghippo di cui sto parlando, eccovi l’XML di esempio che ho usato per questi test:


      
      
      Pasta al pomodoro
      Pasta, pomodoro, formaggio
      
      
      Pasta al ragù
      Pasta, pomodoro, ragù
      
      

Ovviamente non è completo di tutti i dati relativi alla ricetta, ma ho volutamente usato un XML “minimale” con lo scopo di capire la procedura più corretta per effettuarne il parsing con WinRT.

L’inghippo che ho citato prima è  il mio tentativo di sfruttare il metodo Parse della classe XDocument (che permette di trasformare un XML piatto in una struttura dati complessa “navigabile” grazie a LINQ) è fallito miseramente. Ecco un estratto del codice che ho cercato di utilizzare:

public async void GetAllRecipes()
      {
      StorageFile file = await Windows.ApplicationModel.Package.Current.InstalledLocation.GetFileAsync("Recipes.xml");
      string xml = await FileIO.ReadTextAsync(file);
      XDocument doc = XDocument.Parse(xml);
      ...

      }

In prima istanza ho recuperato il file Recipes.xml, sfruttando il metodo GetFileAsync citato poco fa. Dopodichè ho utilizzato un’altra classe di WinRT che permette di effettuare operazioni sui file, chiamata FileIO: nel mio caso, ho usato il metodo ReadTextAsync che, dato un oggetto di tipo StorageFile (ovvero un file recuperato da uno dei vari storage disponibili), lo tratta come testuale e memorizza in una stringa il testo contenuto (ovviamente, anch’esso in maniera asincrona).

Infine, ho utilizzato il metodo XDocument.Parse su tale stringa: il risultato però, durante l’esecuzione, è un’eccezione di tipo ArgumentException, con il messaggio Data at the root level is invalid. Line 1, position 1, che solitamente sta a indicare un file XML non valido o con degli errori di sintassi. Una breve verifica però mi ha fatto capire che c’era qualcosa non andava, dato che il file XML  non conteneva alcun errore di sintassi.

Il trucco sta nel leggere il file XML come stream e di sfruttare il metodo XDocument.Load che, tra le varianti disponibili, accetta anche un oggetto di tipo Stream.

Ecco il codice corretto:

StorageFile storageFile = await Windows.ApplicationModel.Package.Current.InstalledLocation.GetFileAsync("Recipes.xml");
      var file = await storageFile.OpenAsync(FileAccessMode.Read);
      Stream stream = file.AsStreamForRead();
      XDocument doc = XDocument.Load(stream);


      ApplicationContext.Current.Recipes = doc.Descendants("recipes").Elements("recipe").Select(x => new Recipe
      {
      Title = x.Element("title").Value,
      Ingredients = x.Element("ingredients").Value,
      Category = x.Element("category").Value
      });

La differenza rispetto al codice precedente è che apriamo il file in lettura in maniera asincrona, tramite il metodo OpenAsync, e lo convertiamo in uno stream sfruttando il metodo AsStreamForRead esposto dall’oggetto file.

A questo punto possiamo inizializzare un’istanza della classe XDocument: le operazioni successive non differiscono in alcun modo da quelle che avremmo dovuto eseguire in un’applicazione Windows Phone o ASP.NET. Una volta in possesso del documento, ho recuperato tutti i nodi recipe e ho trasformato ogni nodo in un oggetto di tipo Recipe, che non fa altro che mappare 1 ad 1 le informazioni memorizzate nell’XML.

public class Recipe
      {
      public string Title { get; set; }
      public string Ingredients { get; set; }
      }

Una volta in possesso di una collezione di oggetti di tipo Recipe (nel mio caso, una List) possiamo utilizzarla a piacimento nella nostra applicazione: ad esempio, possiamo utilizzarla come sorgente dati di ua ListView o di una GridView, esattamente come faremmo in un’applicazione Windows Phone.

Ecco un esempio di ListView in XAML:


      
      
      
      
      
      
      
      
      

E il codice che si occupa di recuperare le ricette e visualizzare sfruttando la ListView:

public MainPage()
      {
      StorageFile storageFile = await Windows.ApplicationModel.Package.Current.InstalledLocation.GetFileAsync("Recipes.xml");
      var file = await storageFile.OpenAsync(FileAccessMode.Read);
      Stream stream = file.AsStreamForRead();
      XDocument doc = XDocument.Load(stream);


      List recipes = doc.Descendants("recipes").Elements("recipe").Select(x => new Recipe
      {
      Title = x.Element("title").Value,
      Ingredients = x.Element("ingredients").Value,
      Category = x.Element("category").Value
      }).ToList();
      Recipes.ItemsSource = recipes;
      }
by Il blog di Matteo Pagani on 2/20/2012
Post archive