Condividi tramite


[VB.NET] Deserializzazione JSON e relativo binding su ListBox WPF (it-IT)


Finalità

In questo articolo vedremo un metodo per deserializzare informazioni provenienti dal Web in formato JSON, legando tali dati ad una ListBox WPF, in modo da fornire una semplice interfaccia attraverso cui l'utente possa fruire in maniera comoda di quanto recuperato remotamente. A questo fine utilizzeremo Visual Basic .NET, più la famosa libreria Newtonsoft.Json, ed un pizzico di XAML per quanto concerne la parte grafica.

Il nostro obiettivo

Nulla di troppo complesso, ed al tempo stesso qualcosa di utile e divertente - almeno per gli utenti di TechNet. Dal momento che i servizi TechNet e MSDN ci danno la possibilità di consultare i nostri rispettivi profili, l'obiettivo finale sarà quello di realizzare un semplice programma che legga attraverso la Rete le informazioni che costituiscono il bagaglio di medaglie TechNet di un dato utente, e le visualizzino in un'applicazione desktop opportunamente predisposta.

Prerequisiti

Per poter iniziare lo sviluppo, necessitiamo di due informazioni essenziali, ovvero: 1) quale sia l'indirizzo a cui rispondono le API inerenti al conteggio e dettaglio delle nostre medaglie TechNet e 2) come poter identificare in modo univoco la nostra specifica utenza. A questo fine, è necessario visualizzare in ambito browser la pagina del profilo TechNet personale, e ricercare - nel suo codice HTML - l'esecuzione dello script loadReputation()

Nell'immagine si nota come allo script venga passato un URL. Copiando tale indirizzo in una nuova pagina del browser, vediamo comparire un output serializzato, che corrisponde alle informazioni di base dell'utente (Id, Punti attuali, ecc.), più il dettaglio delle medaglie possedute al momento, suddivise per tipologia.

A questo punto conosciamo quindi l'URL dal quale ricavare le informazioni serializzate di interesse, ovvero: https://api.recognition.microsoft.com/v1/user/\<USER-ID>/detail?locale=<LOCALE>, dove - naturalmente - i tag <USER-ID> e <LOCALE> devono essere sostituiti dai rispettivi valori corretti: il primo, deve rappresentare un utente valido (e per conoscere il nostro ID o quello di un altro utente, rimane valido il principio di estrarlo dal codice HTML del profilo stesso), mentre il secondo rappresenta la lingua in cui i risultati devono essere recuperati (nel proseguo dell'articolo utilizzerò il locale "en-US" per uniformità alla traduzione inglese).

I dati recuperati dovranno essere deserializzati, ovvero si dovrà fare in modo che possano essere resi in una forma maggiormente fruibile - sia dal punto di vista della lettura utente, che come gestione vera e propria lato sviluppo - rispetto al formato JSON in cui vengono forniti. Per fare ciò, come accennato in apertura, ci appoggeremo alla libreria Newtonsoft.Json, che fornisce strumenti di semplice utilizzo sia per operazioni di serializzazione che deserializzazione. La libreria è scaricabile all'indirizzo https://json.codeplex.com/. Procediamo quindi al suo download, per vedere successivamente come referenziarla a progetto.

A questo punto abbiamo tutto ciò che serve per procedere allo sviluppo dell'applicazione.

Referenziare la DLL Newtonsoft.Json

All'interno del pacchetto ZIP scaricato da https://json.codeplex.com/ troveremo diverse cartelle. Sarà necessario decomprimere il file Newtonsoft.Json.dll dalla cartella Bin/<VERSIONE>, dove il tag <VERSIONE> identifica la cartella contenente la versione compatibile con quella del Framework .NET che andremo ad usare. Nel mio caso, la DLL è stata estratta dalla cartella Bin/Net45, in quanto 4.5 è la versione di Framework che impiegherò nel programma. La DLL può essere salvata in un percorso qualsiasi: per praticità, creerò tra le directory del mio progetto una subfolder di nome "thirdparts".

Apriamo Visual Studio, e creiamo un nuovo progetto Visual Basic WPF. Per referenziare la libreria appena salvata, dovremo cliccare su "Project" > "Add Reference". Si aprirà una maschera dalla quale potremo eseguire la ricerca di un determinato file, spuntarlo, e renderlo quindi disponibile al progetto. Procediamo quindi a cercare e referenziare Newtonsoft.Json.dll.

 

Tale operazione renderà disponibile il namespace Newtonsoft.Json nel nostro progetto, del quale sfrutteremo il metodo DeserializeObject, presente nella classe JsonConvert. Ma, come prima cosa, dovremo interrogare il sito Web per ottenere, in forma testuale, le informazioni serializzate su cui lavorare.

Interrogare la pagina Web

Dal momento che non necessitiamo di nulla di troppo complesso, dovendo soltanto recuperare la risposta di una pagina Web, utilizzeremo la classe WebClient e la sua funzione DownloadString. Ipotizzando di aver valorizzato la variabile stringa userId con il nostro identificativo, il flusso JSON potrà essere recuperato nella variabile result con le seguenti istruzioni:

 Dim tnaddress As String  = "https://api.recognition.microsoft.com/v1/user/" & userId & "/detail?locale=en-US"
 Dim webClient As New  System.Net.WebClient
 Dim result As String  = webClient.DownloadString(tnaddress)

Si prepara cioé l'URL da interrogare, e lo si passa come argomento alla funzione DownloadString, la quale si occuperà di realizzare la connessione e di leggere i dati in risposta, rendendoli disponibili per l'assegnazione ad una variabile. A questo punto, a partire dalla variabile result, possiamo sfruttare le funzioni di deserializzazione per «spacchettare» il flusso JSON.

Deserializzazione JSON

La funzione JsonConvert.DeserializeObject può essere utilizzata su strutture tipizzate. Dovremo pertanto definire una o più classi conformi alla struttura del flusso da deserializzare. Nell'immagine 2, qui sotto riproposta per praticità, notiamo che nel flusso sono presenti, a livello "radice", le variabili UserId, Points, Percentile, GoldAchievements, SilverAchievements, BronzeAchievements. Le ultime tre definiscono tre classi separate di medaglie: oro, argento, bronzo. Ciascuna di esse contiene diverse entità, ovvero Title, Description, AchievementId, Earned.

Ipotizzando di voler rendere tale struttura come classi, potremo abbozzare qualcosa del genere:

Public Class  UserFeed
    Public Property  UserId As  String
    Public Property  Points As  Long
    Public Property  Percentile As  Integer
    Public Property  GoldAchievements As  Medal()
    Public Property  SilverAchievements As Medal()
    Public Property  BronzeAchievements As Medal()
End Class
 
Public Class  Medal
    Public Property  Title As  String
    Public Property  Description As  String
    Public Property  AchievementId As  Integer
    Public Property  Earned As  String
End Class

In altri termini, esiste una classe "base" che chiameremo arbitrariamente UserFeed. Al suo interno, definiamo le proprietà UserId, Points, Percentile su tipi standard, mentre GoldAchievements, SilverAchievements, BronzeAchievements, referenziando le medesime proprietà, potranno essere ricondotti ad una generica classe che chiameremo Medal, al cui interno definiremo le proprietà viste sopra, ovvero Title, Description, AchievementId, Earned.

Una volta preparata la struttura mediante la quale deserializzare i dati, il processo vero e proprio viene svolto con una singola riga di codice:

Dim uf As UserFeed = JsonConvert.DeserializeObject(Of UserFeed)(result)

Ovvero, data una variabile uf, definita come classe UserFeed, le sue proprietà verranno valorizzate tramite la funzione DeserializeObject, applicata al tipo UserFeed, utilizzando come parametro in ingresso la stringa JSON (ovvero, la variabile precedentemente valorizzata tramite lettura della pagina Web). Da questo momento in poi, ogni riferimento alla variabile uf (e sue proprietà), esporrà il contenuto delle variabili JSON così come derivate da risposta Web.

Se ipotizzassimo di voler visualizzare in un'unico contenitore tutte le medaglie, qualunque sia la tipologia, avremo necessità di unificare i tre array di tipo Medal in una sola lista. Al tempo stesso, dovremo dotare tale lista di una nuova proprietà, che permetta di discernere la tipologia di medaglia (se oro, argento, bronzo), informazione che nei tre array iniziali non abbiamo, se non a livello di nome proprietà. Nonostante esistano metodi più eleganti, un rapido workaround può essere il seguente.

Preparare la struttura dati con LINQ

A partire dalla variabile uf (che abbiamo visto sopra contenere i dati deserializzati relativi ad un dato profilo TechNet), potremmo realizzare una struttura univoca delle medaglie appartenenti alle varie tipologie, operando in questo modo:

Creiamo una nuova classe, che erediti le proprietà della classe Medal, ma che ad esse aggiunga un proprietà di nome "Level", atta a definire la tipologia di una certa medaglia.

Public Class  TypedMedal
    Inherits Medal
    Public Property  Level As  String
End Class

Successivamente, si crea una List(Of TypedMedal), e tramite tre query LINQ si aggiungono ad essa gli elementi degli array di medaglie. Ad ogni query, passeremo il valore della proprietà "Level" come fisso. 

In codice, ciò si traduce in:
 

Dim myMedals As New  List(Of TypedMedal)
myMedals.AddRange(From m As  Medal In  uf.GoldAchievements Select New  TypedMedal With  {.Title = m.Title, .Description = m.Description,  .AchievementId = m.AchievementId, .Earned = m.Earned, .Level = "Gold Achievements"})
 
myMedals.AddRange(From m As  Medal In  uf.SilverAchievements Select New  TypedMedal With  {.Title = m.Title, .Description = m.Description, .AchievementId = m.AchievementId, .Earned = m.Earned, .Level = "Silver Achievements"})
 
myMedals.AddRange(From m As  Medal In  uf.BronzeAchievements Select New  TypedMedal With  {.Title = m.Title, .Description = m.Description, .AchievementId = m.AchievementId, .Earned = m.Earned, .Level = "Bronze Achievements"})

Sostanzialmente, tramite il metodo AddRange, potremo aggiungere alla nostra List(Of TypedMedal) un numero arbitrario di elementi di tipo TypedMedal. Come otteniamo questi elementi? Processando i tre array di medaglie, contenuti in uf. Analizziamo il primo caso, in quanto i successivi due sono speculari.

From m As  Medal In  uf.GoldAchievements

La prima parte della query è semplicissima: viene definita una variabile locale m, di tipo Medal (quindi, con accesso alle proprietà tipiche), da utilizzare nel ciclare gli elementi contenuti all'intero (In) dell'array GoldAchievements, proprietà di uf. Potremo affermare che questa prima parte di query corrisponda perfettamente ad un ciclo For/Each, ed in effetti potremo scriverla come:

For Each  m As  Medal In  uf.GoldAchievements
    ' Fai qualcosa
Next

La seconda parte della query definisce cosa debba essere fatto ad ogni iterazione su uf.GoldAchievements. Nel nostro caso, desideriamo che venga creata un nuovo elemento di tipo TypedMedal, valorizzando le proprietà ereditate con quelle presenti nel tipo Medal, ma assegnando alla proprietà Level, esclusiva di TypedMedal, un valore fisso (che, in questo caso, sarà "Gold Achievements"). Detto in altri termini, per ogni elemento Medal analizzato, verrà aggiunto alla lista myMedals un nuovo elemento di tipo TypedMedal, contenente le informazioni di Medal, ma anche la tipologia di medaglia. Eseguite le tre query, la lista myMedals sarà pronta per essere impiegata come vista di un controllo grafico.

Binding verso ListBox, e personalizzazione layout

Supponendo di avere, sulla nostra pagina WPF, un controllo ListBox di nome MedalList, potremo voler collegare ad esso la variabile myMedals, in modo da esporre le occorrenze (e relative proprietà) di cui essa è composta, fornendo un layout grafico attraverso cui l'utente finale possa visualizzarla. A questo punto, desidereremo suddividere le medaglie per tipologia, in modo che risulti evidente quali di esse vanno a collocarsi nella tipologia oro, quali in argento e quali in bronzo. Occupiamoci anzitutto del code-behind, per poi analizzare gli accorgimenti XAML volti ad ottenere il risultato completo.

Dim view As CollectionView = CollectionViewSource.GetDefaultView(myMedals)
view.GroupDescriptions.Add(New PropertyGroupDescription("Level"))
MedalList.ItemsSource = view

La proprietà ItemsSource permette di definire la collezione dati da impiegare per una determinata visualizzazione. Dal momento che desideriamo esporre i nostri dati con un raggruppamento per tipologia, sarà necessario dichiarare una CollectionView dotata di raggruppamento sulla proprietà "Level", definita nella classe TypedMedal. La CollectionView avrà naturalmente come base dati di partenza la List(Of TypedMedal) myMedals, e definirà al suo interno una PropertyGroupDescription sul campo "Level". Tale vista, così definita, sarà poi passata alla proprietà ItemsSource della ListBox, in modo che si possa effettuare il binding dei dati della vista stessa.

Nel codice XAML della ListBox, definiremo due sezioni particolari: ItemTemplate, ovvero la definizione degli elementi grafici che costituiranno una singola riga della lista, e GroupStyle, ossia i controlli mediante i quali verrà realizzata la divisione tra un gruppo di elementi ed il successivo (al cambio della proprietà "Level", precedentemente impostata nella vista).

<ListBox Name="MedalList" HorizontalAlignment="Stretch" Margin="10,96,10,10" VerticalAlignment="stretch" Width="Auto" AlternationCount="2">
       <ListBox.ItemTemplate>
           <DataTemplate>
               <StackPanel Orientation="Vertical" Margin="0,6,5,2" Width="auto">
                   <TextBlock FontSize="14" FontWeight="Bold"  Foreground="Black"  Text="{Binding Title}" Margin="0,0,0,0"/>
                   <TextBlock FontSize="10" FontWeight="Normal" Foreground="Black" TextWrapping="Wrap" FontStyle="Italic" Text="{Binding Description}"
                                 Margin="0,2,0,0"/>
               </StackPanel>
           </DataTemplate>
       </ListBox.ItemTemplate>
 
       <ListBox.GroupStyle >
           <GroupStyle>
               <GroupStyle.HeaderTemplate>
                   <DataTemplate>
                       <StackPanel Orientation="Horizontal" Background="LightBlue">
                           <TextBlock FontWeight="Bold" FontSize="15" Foreground="Black" Text="{Binding Name}" Margin="5,4,10,4"/>
                           <TextBlock FontWeight="Normal" FontSize="15" Foreground="blue" Text="{Binding ItemCount}" Margin="0,4,0,0" FontStyle="Italic" />
                       </StackPanel>
                   </DataTemplate>
               </GroupStyle.HeaderTemplate>
           </GroupStyle>
       </ListBox.GroupStyle>
   </ListBox>

Per quanto concerne la sezione ItemTemplate, al suo interno si nota uno StackPanel verticale. Lo StackPanel contiene due TextBlock, la prima delle quali riceve il binding della proprietà Title, mentre la seconda da quella Description. Risalendo alla definizione della classe Medal, ed a come siano stati popolati i dati a partire da JSON, si nota come queste due proprietà definiscano, rispettivamente, il nome del riconoscimento ottenuto e la descrizione del riconoscimento stesso. Possiamo quindi aspettarci che ogni elemento della ListBox sia rappresentato da una doppia riga di testo, con la prima di tali righe su cui ritroveremo il titolo/nome della medaglia, ed il secondo in cui visualizzeremo la descrizione.

La sezione GroupStyle indica invece l'aspetto che dovrà assumere il separatore di gruppo. Ovvero, data una proprietà in merito alla quale effettuare una divisione dei dati presentati, come deve essere resa graficamente la linea di demarcazione tra gruppi diversi. Nel nostro caso, avremo nuovamente uno StackPanel - questa volta orizzontale - di colore blu chiaro. Al suo interno, due TextBlock (affiancati per effetto dell'orientamento dello StackPanel che li contiene) nei quali bindare non le proprietà specifiche della struttura dati, bensì della PropertyGroupDescription realizzata nel code-behind. Name è sempre riferita al valore contenuto nel raggruppamento (che nel nostro caso equivale al contenuto della proprietà Level), mentre ItemCount definisce il numero di elementi presenti in quel dato raggruppamento.

Esempio conclusivo

Applicando lo stile di cui sopra, ed eseguendo il programma completo, otterremo un risultato come il seguente (nella videata esemplificativa, l'interrogazione corrispondente al mio profilo, al momento in cui sto scrivendo)

Classe di esempio

Segue il listato completo dell'applicazione

Code-behind, MainWindow.xaml.vb

Imports System.Net
Imports Newtonsoft.Json
Imports System.ComponentModel
 
Class MainWindow
 
    Public Class  UserFeed
        Public Property  UserId As  String
        Public Property  Points As  Long
        Public Property  Percentile As  Integer
        Public Property  GoldAchievements As  Medal()
        Public Property  SilverAchievements As Medal()
        Public Property  BronzeAchievements As Medal()
    End Class
 
    Public Class  Medal
        Public Property  Title As  String
        Public Property  Description As  String
        Public Property  AchievementId As  Integer
        Public Property  Earned As  String
    End Class
 
    Public Class  TypedMedal
        Inherits Medal
        Public Property  Level As  String
    End Class
 
    Private Sub  QueryTechnet(userId As String)
        If userId.CompareTo("") = 0 Then
            MsgBox("Please enter a valid UserId in textbox", MsgBoxStyle.Exclamation)
            Exit Sub
        End If
 
        Dim tnaddress As String  = "https://api.recognition.microsoft.com/v1/user/" & userId & "/detail?locale=en-US"
 
        Dim webClient As New  System.Net.WebClient
        Dim result As String  = webClient.DownloadString(tnaddress)
 
        Dim uf As UserFeed = JsonConvert.DeserializeObject(Of UserFeed)(result)
 
 
        Dim myMedals As New  List(Of TypedMedal)
        myMedals.AddRange(From m As  Medal In  uf.GoldAchievements Select New  TypedMedal With  {.Title = m.Title,
                                                                                             .Description = m.Description,
                                                                                             .AchievementId = m.AchievementId,
                                                                                             .Earned = m.Earned,
                                                                                             .Level = "Gold Achievements"})
 
        myMedals.AddRange(From m As  Medal In  uf.SilverAchievements Select New  TypedMedal With  {.Title = m.Title,
                                                                                               .Description = m.Description,
                                                                                               .AchievementId = m.AchievementId,
                                                                                               .Earned = m.Earned,
                                                                                               .Level = "Silver Achievements"})
 
        myMedals.AddRange(From m As  Medal In  uf.BronzeAchievements Select New  TypedMedal With  {.Title = m.Title,
                                                                                               .Description = m.Description,
                                                                                               .AchievementId = m.AchievementId,
                                                                                               .Earned = m.Earned,
                                                                                               .Level = "Bronze Achievements"})
 
 
        Dim view As CollectionView = CollectionViewSource.GetDefaultView(myMedals)
        view.GroupDescriptions.Add(New PropertyGroupDescription("Level"))
        MedalList.ItemsSource = view
    End Sub
 
    Private Sub  Button_Click(sender As Object, e As  RoutedEventArgs)
        QueryTechnet(txtUserId.Text)
    End Sub
End Class

XAML, MainWindow.xaml

<Window x:Class="MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="My Technet Medals" Height="550" Width="562" WindowStartupLocation="CenterScreen" WindowStyle="ThreeDBorderWindow" ResizeMode="NoResize">
 
    <Window.Resources>
        <Style  TargetType="{x:Type ListBoxItem}">
            <Style.Triggers>
                <Trigger Property="ItemsControl.AlternationIndex" Value="0">
                    <Setter Property="Background" Value="WhiteSmoke" ></Setter>
                </Trigger>
                <Trigger Property="ItemsControl.AlternationIndex" Value="1">
                    <Setter Property="Background" Value="White"></Setter>
                </Trigger>
            </Style.Triggers>
        </Style>
    </Window.Resources>
 
    <Grid>
 
        <ListBox Name="MedalList" HorizontalAlignment="Stretch" Margin="10,119,10,10" VerticalAlignment="stretch" Width="Auto" AlternationCount="2">
 
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Vertical" Margin="0,6,5,2" Width="auto">
                        <TextBlock FontSize="14" FontWeight="Bold"  Foreground="Black"  Text="{Binding Title}" Margin="0,0,0,0"/>
                        <TextBlock FontSize="10" FontWeight="Normal" Foreground="Black" TextWrapping="Wrap" FontStyle="Italic" Text="{Binding Description}"
                                      Margin="0,2,0,0"/>
                    </StackPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
 
            <ListBox.GroupStyle >
                <GroupStyle>
                    <GroupStyle.HeaderTemplate>
                        <DataTemplate>
                            <StackPanel Orientation="Horizontal" Background="LightBlue">
                                <TextBlock FontWeight="Bold" FontSize="15" Foreground="Black" Text="{Binding Name}" Margin="5,4,10,4"/>
                                <TextBlock FontWeight="Normal" FontSize="15" Foreground="blue" Text="{Binding ItemCount}" Margin="0,4,0,0" FontStyle="Italic" />
                            </StackPanel>
                        </DataTemplate>
                    </GroupStyle.HeaderTemplate>
                </GroupStyle>
            </ListBox.GroupStyle>
 
        </ListBox>
 
        <StackPanel HorizontalAlignment="Left" Height="109" Margin="10,10,0,0" VerticalAlignment="Top" Width="529" Orientation="Vertical" >
            <Label Content="Insert TechNet User Id, then press «Refresh»" FontWeight="Bold" />
            <Label Content="If you don't know what your User Id is, please read my article to find out!"/>
            <TextBox Height="23" TextWrapping="Wrap" Name="txtUserId" Text="XXXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXX"/>
            <Button Content="Refresh" Margin="415,0,0,0" Height="30" Click="Button_Click"/>
 
        </StackPanel>
    </Grid>
</Window>

Demo

Il progetto di esempio è scaricabile al seguente indirizzo: https://code.msdn.microsoft.com/Parse-a-JSON-stream-to-b50e8a36

Altre lingue