Guest Post: Realizzare Universal Windows app ibride con il controllo WebView
Questo post è stato scritto da Matteo Pagani, Windows AppConsult Engineer in Microsoft
Se avete seguito gli ultimi eventi targati Microsoft, come Future Decoded, avrete sicuramente sentito parlare di Hosted Web App. Si tratta di una delle novità più interessanti di Windows 10, che porta all’ennesima potenza il concetto di applicazioni ibdride basate su tecnologie web. L’avvento del Windows Runtime e di WinJS avevano giù dato l’opportunità agli sviluppatori web di riutilizzare le loro conoscenze per realizzare applicazioni native al 100%, senza alcuna differenza di prestazioni e funzionalità rispetto ad un’applicazione realizzata in C#, VB.NET o C++. Le Hosted Web App consentono di riutilizzare lo stesso approccio, ma spostando l’ago della bilancia dal client al server: una Hosted Web App non è altro che un sito web che gira sotto forma di applicazione, distribuita sullo Store come qualsiasi altra applicazione.
La particolarità è che il sito web, quando gira all’interno del contesto di un’applicazione, è in grado di accedere alle API della Universal Windows Platform ed interagire, perciò, con le funzionalità del dispositivo: notifiche, fotocamera, GPS, ecc. Il codice, però, risiede sul server e non sul client: nel momento in cui dovete fare una modifica alla vostra applicazione, sarà sufficiente modificare il codice HTML / Javascript del vostro sito. Per questioni di sicurezza, però, l’accesso alle API della Universal Windows Platform è consentito solo quando il contesto è quello di un’applicazione Windows 10: in caso contrario, il codice sarà semplicemete ignorato.
Il codice Javascript incluso nel sito che permette di interagire con la Universal Windows Platform, tipicamente, ha il seguente aspetto:
if (typeof Windows !== 'undefined' &&
typeof Windows.UI !== 'undefined' &&
typeof Windows.UI.Notifications !== 'undefined') {
//Call Windows.UI.Notifications
}
Prima di interagire con le API della UWP verifichiamo se il tipo Windows (o il namespace specifico della API che vogliamo utilizzare) siano definiti: in caso contrario, vuol dire che il sito sta girando nel contesto di un browser e, di conseguenza, tali API non sono accessibili.
Le Hosted Web App, però, non sono l’argomento centrale di questo post: se volete approfondire l’argomento, potete fare riferimento al post di Erica o al sito ufficiale http://microsoftedge.github.io/WebAppsDocs/en-US/win10/HWA.htm In questo articolo vorrei mostrarvi come, sfruttando lo stesso approccio utilizzato dalle Hosted Web App, sia possibile creare delle applicazioni ibride, con delle componenti native utilizzabili da una pagina web ospitata tramite un controllo WebView.
L’architettura del progetto
Nel corso dell’articolo andremo a realizzare il seguente scenario:
- Una libreria in C#, che contiene una classe con dei metodi che interagiscono con la Universal Windows Platform ma che vogliamo utilizzare direttamente dalla WebView
- Una pagina HTML, che contiene del codice Javascript che invoca i metodi definiti nella libreria del punto 1.
- Un’applicazione Windows 10, che contiene una WebView che punta alla pagina web del punto 2.
Nel nostro caso, dato che si tratta di un progetto di test, la pagina HTML sarà ospitata in locale e inclusa all’interno dell’applicazione. In uno scenario reale, può essere tranquillamente ospitata su un server e far parte di un sito web reale.
La libreria
Il primo passaggio è creare le classi che andremo ad utilizzare dal nostro sito web. Per raggiungere questo scopo, le classi devono essere incluse all’interno di un Windows Runtime Component: è un passaggio molto importante, non può essere una normale class library, altrimenti il meccanismo non funzionerà. Da Visual Studio 2015 creiamo perciò un nuovo progetto di tipo Windows Runtime Component e aggiungiamo al suo interno una classe, nella quale includeremo i metodi che richiamerà il sito web.
Le classi contenute all’interno della libreria non hanno alcun vincolo particolare: l’unico requisito è che devono essere decorate con l’attributo [AllowForWeb]. Per il resto, valgono le restrizioni a cui sono sottoposti i Windows Runtime Component, dovute al fatto che si tratta di librerie che possono essere consumate da un’applicazione Windows indipendentemente dal linguaggio in cui è scritta. Questo significa che, ad esempio, un Windows Runtime Component scritto in C# può essere tranquillamente utilizzato da una Windows Store app scritta in HTML e Javascript. Potete approfondire l’argomento leggendo il seguente articolo https://msdn.microsoft.com/en-us/library/windows/apps/xaml/br230301.aspx
Per il nostro progetto di esempio, daremo la possibilità al nostro sito web di inviare una notifica toast all’applicazione. Definiamo, perciò, all’interno della classe contenuta nella libreria un metodo che prepara l’XML che definisce il contenuto di una notifica e lo invia:
namespace HybridApp.Library { [AllowForWeb] public sealed class MessageHelper { public void ShowToastNotification() { var xml = @"<toast> <visual> <binding template='ToastGeneric'> <text>Hello world!</text> <text>This is a a notification</text> </binding> </visual> </toast>"; XmlDocument document = new XmlDocument(); document.LoadXml(xml); ToastNotification notification = new ToastNotification(document); ToastNotifier notifier = ToastNotificationManager.CreateToastNotifier(); notifier.Show(notification); } } }
Se avete già avuto esperienza con le notifiche nelle applicazioni Windows, il codice vi sarà semplice da interpreare: definiamo l’XML di una notifica toast utilizzando i nuovi template inclusi in Window 10, lo incapsuliamo dentro un oggetto di tipo ToastNotification e infine la mostriamo usando la classe ToastNotifier e il metodo Show() .
L’applicazione Windows
L’applicazione Windows si fa carico di due operazioni sfruttando il controllo WebView: mostrare il sito ed esporre la libreria definita in precedenza al controllo, così che il codice Javascript del sito possa utilizzarla. Dobbiamo perciò, come prima cosa, fare clic con il tasto destro sul progetto, scegliere Add reference e aggiungere un riferimento alla librearia creata nel passaggipo precedente. Dopodichè possiamo includere il controllo WebView nella pagina:
<WebView Grid.Column="0" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
NavigationStarting="OnNavigationStarting"
x:Name="Web" Source="ms-appx-web:///WebPage.html" />
Sono due le operazioni fondamentali da fare:
Definire, tramite la proprietà Source, l’indirizzo del nostro sito web. Nel nostro scenario, dato che si tratterà di una pagina HTML locale contenuta nell’applicazione stessa, sfruttiamo il protocollo ms-appx-web. Nell’esempio, stiamo caricando all’interno della WebView la pagina WebPage.html, contenuta nella root del progetto.
Sottoscriverci all’evento NavigationStarting, che viene invocato nel momento in cui viene avviata la navigazione verso la pagina. Ci servirà per registrare la nostra libreria all’interno della pagina web.
Ecco come appare, nel code behind della pagina, la gestione dell’evento NavigationStarting:
private void OnNavigationStarting(WebView sender,
WebViewNavigationStartingEventArgs args)
{
MessageHelper messageHelper = new MessageHelper();
sender.AddWebAllowedObject("messageHelper", messageHelper);
}
Il collegamento tra il mondo nativo e il mondo web avviene grazie al metodo AddWebAllowedObject() , esposto dalla controllo WebView. Tramite questo metodo, siamo in grado di prendere un oggetto nativo (in questo caso, l’istanza di una classe C#) ed esporlo alla pagina web sotto forma di oggetto Javascript. L’operazione è piuttosto semplice:
- Si crea un’istanza della classe inclusa nella nostra libreria che vogliamo esporre al sito web, nel nostro caso MessageHelper.
- Si chiama il metodo AddWebAllowedObject() , passando come parametro un nome identificativo (che useremo lato Javascript) e l’istanza dell’oggetto appena creata.
Il gioco è fatto: ora possiamo passare a creare la pagina web e a vedere come interagire con l’oggetto che abbiamo appena esposto.
La pagina web
Come anticipato all’inizio del post, per questo esempio utilizzeremo una pagina HTML locale. Facciamo perciò clic con il tasto destro sul progetto dell’applicazione Windows 10, scegliamo Add new item e sfruttiamo il template HTML page. Al suo interno, ci limitiamo ad inserire un pulsante che, tramite una funzione Javascript, andrà a invocare il metodo ShowToastNotification() che abbiamo creato nella nostra classe MessageHelper. Ecco come appare il codice:
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8" />
<title>Hybrid app</title>
<script type="text/javascript">
function showToast() {
if (window.messageHelper) {
window.messageHelper.showToastNotification();
}
}
</script>
</head>
<body>
<button onclick="showToast();">Show toast</button>
</body>
</html>
L’oggetto nativo che abbiamo passato tramite il metodo AddWebAllowedObject() viene esposto, in Javascript, all’interno dell’oggetto window, con il nome che abbiamo registrato come identificato. Nel nostro caso, perciò, prima di procedere verifichiamo l’esistenza dell’oggetto messageHelper all’interno della window. Se la risposta è affermativa, vuol dire che ci troviamo all’interno del contesto WebView e quindi possiamo sfruttare i metodi esposti dalla nostra libreria. Potete notare come, a livello di convenzioni, il nome del metodo venga automaticamente adattato per rispettare la sintassi di Javascript: nonostante, all’interno del Windows Runtime Component, il metodo sia stato definito come ShowToastNotification() (quindi con la prima lettera maiuscola), all’interno della pagina web viene esposto come showToastNotification() (quindi con la prima lettera minuscola).
Siamo pronti per testare il nostro lavoro: lanciamo l’applicazione Windows 10, all’interno della quale vedremo renderizzata la pagina web che abbiamo definito. Se abbiamo eseguito tutto correttamente, premendo il pulsante all’interno della WebView comparirà una notifica toast in Windows, come se fosse stata generata dall’applicazione stessa.
Gestire metodi asincroni e valori di ritorno
L’interazione con la Universal Windowss Platform potrebbe aggiungere un ulteriore livello di complessità in scenari ibridi: la maggior parte delle API della UWP sono asincrone e, nel mondo C#, implementate tramite il meccanismo async / await. Javascript ci permette di gestire questo scenario tramite le promises: nello specifico, dopo aver invocato il metodo asincrono possiamo aggiungere a cascata il metodo then() , in cui includere una funzione che sarà richiamata solo nel momento in cui l’operazione asincrona è terminata. In rete esistono molti tutorial (come questo http://www.html5rocks.com/en/tutorials/es6/promises/?redirect_from_locale=it) se volete approfondire il loro funzionamento.
Vediamo un esempio concreto, aggiungendo una nuova classe al nostro Windwos Runtime Component. La chiameremo NavigationHelper e la useremo per recuperare le coordinate geografiche (latitudine e longitudine) della posizione corrente dell’utente. Al suo interno definiamo, perciò, un metodo che, tramite la classe Geolocator, recupera la posizione dell’utente:
namespace HybridApp.Library
{
[AllowForWeb]
public sealed class NavigationHelper
{
public IAsyncOperation<string> GetUserPositionAsync()
{
return GetUserPosition().AsAsyncOperation();
}
internal async Task<string> GetUserPosition()
{
Geolocator locator = new Geolocator();
Geoposition position = await locator.GetGeopositionAsync();
double latitude = position.Coordinate.Point.Position.Latitude;
double longitude = position.Coordinate.Point.Position.Longitude;
return $"Latitude: {latitude}, Longitude: {longitude}";
}
}
La prima cosa che possiamo notare è che, in realtà, di metodi ne abbiamo definiti due: uno di nome GetUserPosition() , che ritorna Task<string> ed esegue l’operazione di geolocalizzazione vera e propria; uno di nome GetUserPositionAsync() , che si limita a prendere l’altro metodo e a convertirlo in una IAsyncOperation. Questo perchè un Windows Runtime Component non può ritornare direttamente Task o Task<T> per un’operazione asincrona, ma deve passare attraverso una IAsyncOperation. Quello che utilizzeremo dalla pagina web, perciò, è proprio GetUserPositionAsync() : come potete notare, l’altro metodo è definito come internal ed è accessibile solamente dalla classe stessa.
Ora che abbiamo definito questa nuova classe, dobbiamo ricordarci di registrare anch’essa all’interno del controllo WebView tramite il metodo AddWebAllowedObject() , sfruttando sempre l’evento NavigationStarting:
private void OnNavigationStarting(WebView sender,
WebViewNavigationStartingEventArgs args)
{
MessageHelper messageHelper = new MessageHelper();
sender.AddWebAllowedObject("messageHelper", messageHelper);
NavigationHelper navigationHelper = new NavigationHelper();
sender.AddWebAllowedObject("navigationHelper", navigationHelper);
}
Ecco, invece, come definire la funzione Javascript che è in grado di richiamare il metodo asincrono GetUserPositionAsync():
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8" />
<title>Hybrid app</title>
<script type="text/javascript">
function getPosition() {
if (window.navigationHelper) {
window.navigationHelper.getUserPositionAsync()
.then(function (result) {
document.getElementById("result").innerHTML = result;
});
}
}
</script>
</head>
<body>
<button onclick="getPosition();">Get position</button>
<div id="result"></div>
</body>
</html>
La prima parte del codice è uguale a quella che abbiamo visto nello scenario precedente: se il sito sta girando nel contesto di una WebView, abbiamo accesso all’oggetto navigationHelper che abbiamo registrato in precedenza. Possiamo perciò chiamare il metodo getUserPositionAsync(): in questo caso, però, si tratta di un metodo asincrono, che non restituisce immediatamente un risultato, ma bisogna attendere che il GPS abbia geolocalizzato l’utente. Sfruttando perciò il metodo then() possiamo andare a definire il codice che vogliamo eseguire solo quando l’operazione è terminata: in questo caso, dato che il metodo getUserPositionAsync() restituisce anche un risultato (le coordinate dell’utente, sotto forma di stringa) lo possiamo recuperare tramite il parametro result della funzione.
Nell’esempio, ci limitiamo a mostrare la stringa restituita dal metodo nella pagina web, andandola a “scrivere” all’interno di un tag div presente nella pagina.
Quello che abbiamo visto è un ottimo esempio di applicazione ibrida: il contesto principale rimane quello del sito web, ma sfruttiamo le potenzialità della Universal Windows Platform per effettuare un’operazione (la geolocalizzazione dell’utente) che richiede l’interazione diretta con il device (nello specifico, il sensore GPS).
In conclusione
Nel corso di questo post abbiamo visto come, tramite il controllo WebView, siamo in grado di accedere a classi e oggetti della Universal Windows Platform direttamente da un sito web. Potete trovare il progetto di esempio descritto nel corso dell’articolo sul mio repository GitHub all’indirizzo https://github.com/qmatteoq/HybridApp-UWP