Guest post: Mappe, pushpin e MVVM nelle Universal Windows app

Questo post è stato scritto da Matteo Pagani, Windows AppConsult Engineer in Microsoft

Tra le novità più interessanti introdotte da Windows Phone 8.1, a suo tempo, c'era un nuovo controllo per le mappe, basato sulla cartografia di Here e con molte funzionalità aggiuntive rispetto a quelle presenti nelle versioni precedenti della piattaforma, come:

  1. API native per il calcolo dei percorsi e la loro visualizzazione sulla mappa
  2. API per ricavare un indirizzo date le coordinate geografiche e viceversa
  3. Supporto a layer multipli, che possono essere sovrapposti alla mappa

Questo controllo, disponibile inizialmente solo su smartphone, è stato reso universale con Windows 10: ciò significa che ora possiamo utilizzarlo all'interno di una Universal Windows app per smartphone, pc, tablet, Xbox, ecc.

Tale controllo si chiama MapControl e si trova all'interno del namespace Windows.UI.Xaml.Controls.Maps. Non fa parte dei controlli base di sistema: ciò significa che, per poterlo aggiungere all'interno di una pagina XAML, è necessario specificarne il namespace, come nell'esempio seguente.

<Page

x:Class="MapsSample.MainPage"

xmlns:maps="using:Windows.UI.Xaml.Controls.Maps"

mc:Ignorable="d">

 

<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">

<maps:MapControl Grid.Row="0 />

</Grid>

</Page>

Uno degli scenari più frequenti quando si lavora con la mappa è la necessità di mostrare dei segnaposto (in inglese, pushpin) che mettano in evidenza alcuni luoghi specifici. Esistono due modi per raggiungere questo risultato:

  1. Sfruttare lo XAML. Il controllo supporta la possibilità, in maniera simile a quanto si fa con controlli come ListView o GridView, di associare alla mappa una collezione di segnaposto e di definire il template con cui devono essere rappresentati. Questo è l'approccio che garantisce la massima flessibilità, in quanto il template può contenere qualsiasi elemento visuale (immagini, pulsanti, ecc.). Di contro, però, è molto più oneroso per il sistema e, in caso di numero elevato di segnaposto da visualizzare, potreste avere problemi di lentezza o di crash dovuti all'esaurimento della memoria disponibile.
  2. Sfruttare la classe MapIcon, che nasce specificatamente per rappresentare un segnaposto della mappa descritto da un'immagine e un'etichetta. Questa classe adotta una logica di "best effort": la visualizzazione del segnaposto non viene sempre garantita, ma viene regolata da una serie di fattori quali la presenza di elementi sovrapposti, il livello di zoom, ecc. Ciò consente di ottimizzare particolarmente le performance, rendendola adatta per scenari in cui il numero di segnaposto da visualizzare è molto elevato.

Se avete dimestichezza con la gestione di collezioni, il prima scenario vi risulterà molto semplice da implementare. Innanzitutto ci serve una classe che rappresenti il nostro segnaposto, come nell'esempio seguente.

public class Pushpin

{

public string Name { get; set; }

 

public Geopoint Point { get; set; }

}

Non ci sono vincoli particolari nella sua definizione, se non che deve contenere almeno una proprietà di tipo Geopoint con le coordinate del segnaposto. Ipotizzando che il nostro codice contenga la definizione di una collezione di oggetti di tipo Pushpin, rappresentante i segnaposto che vogliamo visualizzare, ecco come appare il nostro codice XAML:

<maps:MapControl Grid.Row="0">

<maps:MapItemsControl ItemsSource="{Binding Path=Pushpins}">

<maps:MapItemsControl.ItemTemplate>

<DataTemplate>

<Image Source="/Assets/MicrosoftLogo.png" maps:MapControl.Location="{Binding Point}" />

</DataTemplate>

</maps:MapItemsControl.ItemTemplate>

</maps:MapItemsControl>

</maps:MapControl>

All'interno del controllo MapControl si definisce un oggetto di tipo MapItemsControl, che si comporta in maniera molto simile ad un controllo ListView o GridView:

  1. Espone una proprietà di nome ItemsSource, ovvero la collezione di segnaposto che vogliamo mostrare.
  2. Espone una proprietà di nome ItemTemplate, all'interno della quale specificare il DataTemplate che definisce l'aspetto visivo del singolo segnaposto.

L'unica particolarità rispetto al template di una lista tradizionale è l'utilizzo della attached property MapControl.Location, che ci serve per definire la posizione del segnaposto sulla mappa: va perciò collegata con la proprietà di tipo Geopoint della classe Pushpin. Nell'esempio precedente, il segnaposto è rappresentato da una semplice immagine. Trattandosi, però, di un DataTemplate, avremmo potuto aggiungere qualsiasi altro controllo XAML e rendere il nostro segnaposto più complesso.

Come anticipato in precedenza, però, questo approccio può avere un forte impatto sulle performance, soprattutto se si devono mostrare molti segnaposto. In questi scenari sarebbe preferibile, perciò, utilizzare la classe MapIcon la quale, però, introduce alcune sfide nel nostro progetto, soprattutto se basato sul pattern MVVM. Il limite principale è che non abbiamo la possibilità di sfruttare il binding come abbiamo fatto in precedenza con l'oggetto MapItemsControl. I segnaposto di tipo MapIcon, infatti, possono essere aggiunti solamente nel code-behind, tramite la proprietà MapElements del controllo MapControl, come nell'esempio seguente:

private void AddMapIcon(object sender, RoutedEventArgs e)

{

BasicGeoposition position = new BasicGeoposition() { Latitude = 47.620, Longitude = -122.349 };

Geopoint point = new Geopoint(position);

 

MapIcon mapIcon = new MapIcon();

mapIcon.Location = point;

mapIcon.Title = "Space Needle";

 

Map.MapElements.Add(mapIcon);

}

 

Nell'ottica dello sviluppo con il pattern MVVM questo vincolo introduce un grosso limite, dato che dal ViewModel (che è una semplice classe indipendente dalla View) non possiamo accedere al controllo.

Nel corso di questo post vedremo come possiamo risolvere entrambe le sfide grazie ad una libreria realizzata da Joost Van Schaik, MVP olandese esperto di cartografia che ha collaborato spesso con il team che gestisce la piattaforma di mappe di Windows. Si tratta di una libreria di terze parti, open source e ben supportata.

Configurare il progetto

La libreria è disponibile sotto forma di progetto open source pubblicato su GitHub all'indirizzo https://github.com/LocalJoost/WpWinNl, ma è installabile semplicemente grazie a NuGet. Unico accorgimento: per trovarla, dovrete abilitare la ricerca anche tra i pacchetti marcati come pre-release, dato che l'ultima versione stabile non include ancora il supporto a UWP.

Per il nostro progetto di prova ci aiuteremo anche con MVVM Light: sfrutteremo le sue classi sia per gettare le basi dell'applicazione in ottica MVVM, sia perché utilizzeremo un'architettura a messaggi (di cui ho parlato in questo post) per gestire l'interazione con i segnaposto. Nel corso di questo post darò per scontata una famigliarità minima con il pattern MVVM: potete approfondire l'argomento con la serie di post che sono stati pubblicati recentemente su questo blog. Trovate la prima parte all'indirizzo http://blogs.msdn.com/b/italy/archive/2015/10/29/guest-post-il-pattern-mvvm-nelle-universal-windows-app-introduzione.aspx

Quello che andremo a creare sarà:

  1. Una classe che contiene la definizione del segnaposto
  2. Un ViewModel, che si farà carico di creare e popolare una collezione di segnaposto
  3. Una pagina XAML, con il controllo MapControl, che si farà carico di trasformare la collezione di segnaposto passata dal ViewModel in una serie di oggetti MapIcon

La classe Pushpin

Dal punto di vista della definizione della classe che rappresenta il segnaposto, non ci sono differenze rispetto a quanto visto in precedenza. Anche in questo caso non abbiamo vincoli particolari, se non la necessità di includere almeno una proprietà che contenga le coordinate geografiche, che deve essere espressa con il tipo BasicGeoposition. Ecco un esempio:

public class Pushpin

{

public string Name { get; set; }

 

public BasicGeoposition Point { get; set; }

 

public Pushpin(string name, double latitude, double longitude)

{

this.Name = name;

this.Point = new BasicGeoposition {Latitude = latitude, Longitude = longitude};

}

}

In questo caso abbiamo definito un segnaposto che, oltre alla posizione, contiene una proprietà Name all'interno della quale memorizzare una descrizione del luogo.

Il ViewModel

La struttura del ViewModel, ovvero la classe che si fa carico di gestire i dati mostrati nella View, è molto semplice, dato che si occupa solamente di definire la collezione di segnaposto che vogliamo mostrare. Ecco il codice completo:

public class MainViewModel : ViewModelBase

{

private ObservableCollection<Pushpin> _pushpins;

public ObservableCollection<Pushpin> Pushpins

{

get { return _pushpins; }

set { Set(ref _pushpins, value); }

}

 

private double _zoomlevel;

public double ZoomLevel

{

get { return _zoomlevel; }

set { Set(ref _zoomlevel, value); }

}

 

private Geopoint _center;

public Geopoint Center

{

get { return _center; }

set { Set(ref _center, value); }

}

 

private RelayCommand _showPushpins;

public RelayCommand ShowPushpins

{

get

{

_showPushpins = _showPushpins ?? new RelayCommand(() =>

{

Center = new Geopoint(new BasicGeoposition { Latitude = 45.4646, Longitude = 9.1882 });

ZoomLevel = 15;

List<Pushpin> points = new List<Pushpin>

{

new Pushpin("Location 1", 45.4646, 9.1882),

new Pushpin("Location 2", 45.4615, 9.1915),

new Pushpin("Location 3", 45.4583, 9.1913),

new Pushpin("Location 4", 45.4620, 9.1836),

new Pushpin("Location 5", 45.4662, 9.2026)

};

Pushpins = new ObservableCollection<Pushpin>(points);

 

});

return _showPushpins;

}

}

}

Il ViewModel contiene la definizione di tre proprietà:

  1. Pushpins, ovvero la collezione di oggetti di tipo Pushpin da mostrare.
  2. ZoomLevel ovvero il livello di zoom.
  3. Center, ovvero un oggetto di tipo Geopoint con le coordinate geografiche sulle quali centrare la mappa.

In più, viene definito un comando (sfruttando la classe RelayCommand di MVVM Light) che va a:

  1. Centrare la mappa su una posizione specifica, assegnando un valore alla proprietà Center.
  2. Impostare il livello di zoom su 15.
  3. Creare 5 segnaposto e aggiungerli alla collezione Pushpins.

La View

Veniamo ora alla parte più interessante, in cui utilizzeremo la libreria WpWinNl, senza la quale non saremmo in grado di trasformare la nostra collezione di oggetti Pushpin in una serie di segnaposto di tipo MapIcon posizionati nella mappa. Per raggiungere questo obiettivo la libreria mette a disposizione alcuni behavior, ovvero degli oggetti che consentono di gestire direttamente nello XAML logica e comportamenti che, in condizioni normali, richiederebbero l'uso del code behind.

Il primo passo è aggiungere alcuni namespace nella definizione della pagina XAML: il primo è Microsoft.Xaml.Interactivity e serve proprio per gestire i behavior. Fa parte della Behaviors SDK, che viene inclusa in automatico nel nostro progetto nel momento in cui installiamo la libreria WpWinNL da NuGet. Il secondo, invece, è specifico della libreria e permette di accedere ai behavior che ci serviranno per raggiungere il nostro scopo: il percorso completo è WpWinNl.Maps.

Ecco, perciò, come appare la definizione della pagina:

<Page

x:Class="MapsSample.MainPage"

xmlns:maps="using:Windows.UI.Xaml.Controls.Maps"

xmlns:maps1="using:WpWinNl.Maps"

xmlns:interactivity="using:Microsoft.Xaml.Interactivity">

 

</Page>

Grazie a questi namespace, siamo in grado di introdurre un po' di novità nella definizione del nostro controllo MapControl:

<maps:MapControl Grid.Row="0" ZoomLevel="{Binding Path=ZoomLevel, Mode=TwoWay}" Center="{Binding Path=Center}" x:Name="Map">

<interactivity:Interaction.Behaviors>

<maps1:MapShapeDrawBehavior LayerName="Points" ItemsSource="{Binding Path=Pushpins}" PathPropertyName="Point">

<maps1:MapShapeDrawBehavior.ShapeDrawer>

<maps1:MapIconDrawer ImageUri="ms-appx:///Assets/MicrosoftLogo.png" CollisionBehaviorDesired="RemainVisible" />

</maps1:MapShapeDrawBehavior.ShapeDrawer>

</maps1:MapShapeDrawBehavior>

</interactivity:Interaction.Behaviors>

</maps:MapControl>

I behavior vengono aggiunti ad un controllo tramite la proprietà complessa Interaction.Behaviors: si tratta di una collezione, in quanto siamo liberi di associare più di un behavior allo stesso controllo, nel caso in cui vogliamo gestire comportamenti differenti.

In questo caso ne utilizziamo solamente uno, messo a disposizione dalla libreria WpWinNl: si chiama MapShapeDrawBehavior e, come si evince dal nome stesso, serve per "disegnare" degli elementi aggiuntivi sulla mappa. Tale behavior, infatti, può essere utilizzato non solo per mostrare segnaposto, ma anche per sovrapporre linee o poligoni. Sono tre le proprietà chiave che dobbiamo configurare di questo behavior:

  1. LayerName rappresenta il nome identificativo del layer, che deve essere univoco. Potrebbe capitare, infatti, di voler aggiungere più layer alla stessa mappa, ad ognuno dei quali corrisponderebbe un diverso MapShapeDrawBehavior.
  2. ItemsSource è la collezione di segnaposto che vogliamo mostrare. Nel nostro caso, si tratta della collezione di nome Pushpins che abbiamo definito nel ViewModel.
  3. PathPropertyName è il nome della proprietà della classe che abbiamo creato per rappresentare i segnaposto che contiene le coordinate geografiche. Nel nostro caso, si tratta della proprietà Point della classe Pushpin.

Dopodiché, tramite la proprietà complessa ShapeDrawer, possiamo definire quale tipo di elemento vogliamo disegnare sulla mappa. Nel nostro caso si tratta di segnaposto: utilizziamo perciò l'oggetto MapIconDrawer. Tramite la proprietà ImageUri possiamo specificare un'immagine personalizzata da utilizzare, in caso alternativo sarà utilizzata quella predefinita, che potete vedere nell'immagine sottostante.

L'oggetto MapIconDrawer vi permette di controllare anche il comportamento della classe MapIcon in caso di elementi sovrapposti. Come comportamento predefinito, il controllo mappa può nascondere alcuni segnaposto se si sovrappongono ad altre informazioni sulla mappa, come etichette, informazioni sul traffico, ecc. Impostando la proprietà CollisionBehaviorDesired su RemainVisible si forza, invece, il controllo a mostrare sempre il segnaposto, indipendentemente dalla sua posizione.

Gestire l'interazione

Fino a qui abbiamo raggiunto il primo dei nostri obiettivi: poter rappresentare una collezione di segnaposto tramite oggetti MapIcon, sfruttando il binding e il pattern MVVM. Ora vediamo il passo successivo, ovvero gestire l'interazione dell'utente con i segnaposto. Come anticipato all'inizio del post, di base si tratta di uno scenario che la classe MapIcon non ci permette di gestire con un comando. Abbiamo a disposizione solamente un evento, esposto dal controllo MapControl, di nome MapElementClick, che viene scatenato ogni qualvolta l'utente seleziona un elemento sovrapposto sulla mappa. Trattandosi di un evento, però, possiamo gestirlo solo all'interno del code behind. Per supportare meglio l'uso del pattern MVVM ci viene in aiuto anche in questo caso il behavior MapShapeDrawBehavior, che ci permette di sfruttare questi eventi esposti dal controllo MapControl e di tradurli in comandi, che possono essere perciò definiti anche in classi differenti dal code behind. Per mettere in piedi questa architettura ci affideremo ai messaggi: nel momento in cui l'utente farà click su un segnaposto invieremo un messaggio, che un'altra classe (come un ViewModel o una View) intercetterà per eseguire un'azione.

Il primo passo è quello di definire il nostro messaggio, in base ai requisiti della nostra applicazione. Nel nostro caso, ipotizziamo di voler mostrare all'utente una finestra di dialogo con il nome del luogo selezionato. Creeremo, perciò, un messaggio in grado di contenere questa informazione:

 

public class MessageDialogMessage

{

public string Name { get; set; }

 

public MessageDialogMessage(string name)

{

Name = name;

}

}

Ora ci serve un comando che invii questo messaggio. La libreria WpWinNl purtroppo non consente di implementare la gestione della selezione direttamente nel ViewModel, come faremmo per una lista tradizionale, ma dobbiamo spostare l'ago della bilancia sulla classe stessa che gestisce il segnaposto. Ciò significa che dovremo definire un comando per la selezione all'interno della nostra classe Pushpin, come nell'esempio seguente:

public class Pushpin

{

public string Name { get; set; }

 

public BasicGeoposition Point { get; set; }

 

public Pushpin(string name, double latitude, double longitude)

{

this.Name = name;

this.Point = new BasicGeoposition { Latitude = latitude, Longitude = longitude };

}

 

 

public ICommand SelectCommand => new RelayCommand(Select);

 

public void Select()

{

Messenger.Default.Send(new MessageDialogMessage(Name));

}

}

Anche in questo caso sfruttiamo la classe RelayCommand di MVVM Light per definire il comando, che si limita semplicemente ad utilizzare la classe Messenger e il metodo Send() per inviare un messaggio di tipo MessagDialogMessage con il nome del luogo che è stato selezionato.

Abbiamo detto che, nel nostro caso, vogliamo semplicemente mostrare a video una finestra di dialogo con il nome del luogo scelto. All'interno del code behind della nostra View andiamo, perciò, a intercettare questo messaggio e ad usarne il contenuto per popolare una MessageDialog:

public sealed partial class MainPage : Page

{

public MainPage()

{

this.InitializeComponent();

this.DataContext = new MainViewModel();

 

Messenger.Default.Register<MessageDialogMessage>(this, async message =>

{

MessageDialog dialog = new MessageDialog(message.Name);

await dialog.ShowAsync();

});

}

}

 

L'ultimo passo è collegare questo comando al tap dell'utente sul segnaposto. Come anticipato in precedenza, andremo a sfruttare una proprietà del behavior MapShapeDrawBehavior chiamata EventToHandlerMappers che, come dice il nome stesso, permette di collegare eventi scatenati dal controllo MapControl a comandi definiti all'interno delle nostre classi. Ecco come appare la definizione completa della mappa:

<maps:MapControl Grid.Row="0" ZoomLevel="{Binding Path=ZoomLevel, Mode=TwoWay}" Center="{Binding Path=Center}" x:Name="Map">

<interactivity:Interaction.Behaviors>

<maps1:MapShapeDrawBehavior LayerName="Points" ItemsSource="{Binding Path=Pushpins}" PathPropertyName="Point">

<maps1:MapShapeDrawBehavior.EventToHandlerMappers>

<maps1:EventToHandlerMapper EventName="MapElementClick" CommandName="SelectCommand" />

</maps1:MapShapeDrawBehavior.EventToHandlerMappers>

<maps1:MapShapeDrawBehavior.ShapeDrawer>

<maps1:MapIconDrawer ImageUri="ms-appx:///Assets/MicrosoftLogo.png" CollisionBehaviorDesired="RemainVisible" />

</maps1:MapShapeDrawBehavior.ShapeDrawer>

</maps1:MapShapeDrawBehavior>

</interactivity:Interaction.Behaviors>

</maps:MapControl>

Abbiamo aggiunto all'interno del behavior una proprietà EventToHandlerMappers, che include la definizione di un oggetto EventToHandlerMapper che specifica:

  1. Il nome dell'evento che vogliamo gestire (in questo caso, MapElementClick).
  2. Il comando definito nella classe Pushpin che vogliamo invocare, in questo caso SelectCommand.

Il gioco è fatto: ora, al click su uno qualsiasi dei segnaposto, comparirà la nostra finestra di dialogo.

Linee, forme e altro ancora

La libreria WpWinNl supporta anche altre tipologie di ShapeDrawer, che possono essere sfruttati per sovrapporre altri elementi sulla mappa, come MapPolylineDrawer (per disegnare linee) e MapPolygonDrawer (per disegnare forme complesse). Se volete saperne di più, potete fare riferimento a questo post (in inglese) scritto dall'autore della libreria stessa, nonché al progetto di esempio disponibile su GitHub all'indirizzo https://github.com/LocalJoost/WpWinNl/tree/uwp/uap10.0/WpWinNl.MapBindingDemo

Se, invece, volete giocare con il progetto di esempio realizzato in questo post lo trovate sul mio account GitHub all'indirizzo https://github.com/qmatteoq/MapsPushpinsSample

Happy coding!