-----------------------------------

Acquista i software ArcGIS tramite Studio A&T srl, rivenditore autorizzato dei prodotti Esri.

I migliori software GIS, il miglior supporto tecnico!

I migliori software GIS, il miglior supporto tecnico!
Azienda operante nel settore GIS dal 2001, specializzata nell’utilizzo della tecnologia ArcGIS e aderente ai programmi Esri Italia Business Network ed Esri Partner Network

-----------------------------------



Visualizzazione post con etichetta c#. Mostra tutti i post
Visualizzazione post con etichetta c#. Mostra tutti i post

sabato 10 ottobre 2015

ArcGIS Pro: Nun te regghe Queued(Task)

ArcGIS Pro si differenzia nettamente dalle applicazioni ArcGIS desktop esistenti, poichè è sviluppato secondo un'architettura multithread per sfruttare le moderne CPU/GPU con più core di esecuzione. Per lo sviluppatore di add-in che vuole estendere le funzionalità di ArcGIS Pro, questo significa modificare il modello di programmazione e familiarizzare, in un primo momento, con alcuni nuovi concetti che possono apparire bizzarri. Ovviamente, come con qualsiasi novità, lavorare con questi modelli diventerà gradualmente sempre più facile, ed i benefici del multithreading saranno sempre più evidenti.
Sfide che affronterai nella programmazione multithreading:
Le seguenti quattro differenze fondamentali distinguono le applicazioni multithread tra cui ArcGIS Pro, da una classica applicazione a singolo thread singolo:
  • Per garantire un’esperienza d’uso (UX) responsiva, il thread dell'interfaccia grafica utente (GUI) deve essere in grado di ricevere l'input da parte dell'utente e produrre output senza intoppi e senza interruzioni. Ciò significa che le esecuzioni delle azioni programmate devono essere eseguite in modo asincrono su thread di lavoro separati; il thread della GUI non dovrebbe mai svolgere un lavoro di qualsiasi tipo che blocchi nell’attesa. Ciò è in contrasto con le esistenti applicazioni ArcGIS desktop dove la maggior parte del lavoro viene eseguito direttamente sul singolo thread della GUI.
  • Mentre il lavoro è in esecuzione sui thread in background, agli utenti deve essere presentata un’interfaccia utente logicamente coerente e informativa. Comandi, strumenti, e altre parti dell'interfaccia utente devono essere attivate o disattivate opportunamente basandosi su quali operazioni sono in esecuzione, e dovrebbe essere fornito un adeguato feedback. Se un'operazione di lunga esecuzione è logicamente annullabile, dovrebbe essere fornita la possibilità di annullarla.
  • Operazioni che possono generare conflitto non dovrebbero essere eseguite simultaneamente e devono sempre essere eseguite in una sequenza logica appropriata. Ad esempio, le operazioni su una mappa non possono essere eseguite mentre il progetto che contiene la mappa è ancora in fase di caricamento ed un insieme di feature selezionate non può essere cancellato finché la selezione stessa non è stata completata. La maggior parte delle operazioni avviate tramite l'interazione dell'utente sono logicamente dipendenti dall’ordine e dovrebbero essere eseguite in serie.
  • Bisogna fare attenzione a garantire che l'accesso allo stato volatile, cioè l'accesso alle variabili all'interno del programma, sia correttamente sincronizzato quando tale stato è condiviso tra i thread. Ad esempio, se un oggetto collection è condiviso tra un thread di lavoro e il thread della GUI, entrambi i thread devono essere coordinati per l’accesso alla collection in modo che un thread non legga gli elementi della collection, mentre un altro thread simultaneamente sta aggiungendo o rimuovendo elementi. Questo tipo di protezione è comune a tutti i tipi di programmazione multithreaded e viene normalmente realizzato utilizzando un lock. In un'applicazione in cui più parti indipendenti possono estendere il comportamento dell'applicazione, le operazioni di coordinamento possono diventare complesse e fuori controllo senza un comune framework che orchestra i vari componenti a lavorare insieme.


Il modello di threading interno ad ArcGIS Pro

Gli ingegneri di Esri hanno posto la massima priorità per rendere ArcGIS Pro facile da programmare il più possibile nella nuova architettura multithread. A tal fine, ArcGIS Pro incorpora le più recenti funzionalità della programmazione asincrona fornita da Microsoft insieme a nuove infrastrutture threading specifiche dell'applicazione su misura per ridurre la complessità del codice.
Nella maggior parte dei casi, gli sviluppatori di add-in dovrebbero solo aver bisogno di trattare con due thread: il thread dell'interfaccia utente e un thread di lavoro specializzato forniti dall'applicazione.
Internamente, ArcGIS Pro utilizza un gran numero di thread per vari scopi, tra cui la rasterizzazione, il rendering dei grafici, il caricamento dei dati e la selezione degli algoritmi di geoprocessing che sfruttano il parallelismo per velocizzare il calcolo. Mantenere tutte queste attività senza blocchi e senza conflitti richiede una notevole attività di coordinamento e di complessità associata; per questo motivo, questi thread sono completamente interni e isolati agli sviluppatori nell'ambito delle implementazioni con l’SDK pubblico. Quando viene chiamato un metodo nella API pubblica, l'implementazione interna può, quando applicabile, dividere l’operazione e delegare parti ad uno o più di questi thread interni specializzati, o accodare le operazioni che alla fine vengono eseguite all'interno di un processo esterno o un servizio web .



Attività (Tasks) e il modello asincrono basato su attività (TAP)

I metodi all'interno ArcGIS Pro SDK rientrano in tre categorie:
- Metodi asincroni che possono essere chiamati in qualsiasi thread. Metodi di questo tipo sono nominati utilizzando il suffisso Async e di solito restituiscono Tasks. In alcuni casi, può essere fornita sia la versione sincrona che asincrona del metodo.
- Metodi sincroni che dovrebbero essere chiamati solo sul thread di lavoro. Metodi di questo tipo sono segnalati nella guida in linea delle API e un tip sul codice appariranno quando siamo col cursore sul metodo.
- Metodi sincroni che dovrebbero essere chiamati solo sul thread della GUI. Questi tipi di metodi sono di solito associati con WPF.
Se un metodo su un particolare oggetto viene chiamato sul thread sbagliato, la chiamata genera un'eccezione di tipo ArcGIS.Core.CalledOnWrongThreadException. Se non sei sicuro su un particolare caso, è possibile consultare la guida del componente SDK o l’help fornito da Microsoft per determinare se un particolare metodo o una proprietà ha delle restrizioni.
All'interno dell’SDK - in particolare all'interno del namespace ArcGIS.Core – il thread di lavoro associa metodi e proprietà che tendono ad essere a grana fine. Per ridurre il tempo associato con la schedulazione e la commutazione di contesto, questi metodi sono sincroni e devono essere utilizzati nel codice usando task.
Il .NET Task Parallel Library di Microsoft (TPL) e il modello di programmazione associata noto come il modello asincrono basato su attività (TAP) semplificano la creazione di codice asincrono all'interno di un'applicazione multithread. La classe Task viene utilizzata per rappresentare un'operazione eseguita in modo asincrono.
Nel seguente esempio, il metodo PrintReportAsync viene richiamato e restituisce immediatamente un oggetto Task al chiamante. Nel frattempo, la funzione di stampa continua l’esecuzione in background su un altro thread.

    private void Button_Click(object sender, RoutedEventArgs e)
    {
      Task t = PrintReportAsync ("HP1");
      // Attendo che il task abbia finito.
      t.Wait ();
      MessageBox.Show ("Il Report è pronto!");
    }

Questo esempio vuole mostrare un messaggio quando la stampa è completata e utilizza il metodo Wait sull'oggetto Task restituito per sospendere il thread chiamante fino a quando il task ha completato il suo compito.
Questo approccio presenta due grandi inconvenienti: in primo luogo il thread chiamante non può fare altro mentre è in attesa; in realtà è meno efficiente che semplicemente chiamare una versione sincrona della funzione di stampa. In secondo luogo, poiché il thread chiamante è un thread della GUI, l'interfaccia utente risulta ‘congelata’. Un thread sospeso, ovviamente, non è in grado di elaborare l'input dell'utente, aggiornare elementi grafici o fare qualsiasi altra cosa. Per queste ragioni, non si dovrebbe mai usare il metodo Wait su un thread della GUI.
Fortunatamente, .NET introduce le funzionalità al linguaggio async e await. Il modificatore async segna il metodo in modo che il compilatore sappia che il metodo è asincrono e usi l'operatore await. L'operatore await è molto utile in quanto è utilizzato per chiamare i metodi in modo asincrono e dopo per forzare il thread chiamante a tornare automaticamente alla riga successiva e continuare l'esecuzione una volta che l'operazione asincrona è stata completata. Il thread chiamante - normalmente il thread della GUI - non è bloccato ed è libero di adottare altre azioni mentre il task sul thread di lavoro è ancora in esecuzione.
Si noti che possiamo modificare ora l'obiettivo originale con poche variazioni, ma in questo caso l'interfaccia utente non si blocca.

    private async void Button_Click(object sender, RoutedEventArgs e)
    {
      Task t = PrintReportAsync ("HP1");
      // Attendere (senza blocco) fino a quando il task è completato.
      await t;
      // riparte da qui quando il task è completato.
      MessageBox.Show ("Il report è pronto!");
    }

L’utilizzo di Run

Quando una funzione asincrona non è disponibile, è possibile scrivere facilmente le proprie funzioni wrapper che eseguono internamente uno o più metodi sincroni. L'esempio seguente utilizza il metodo statico Run per accodare l'esecuzione della funzione WorkFunc ad una thread casuale nel pool di thread dei Task. Si noti che il metodo click restituisce immediatamente il controllo al chiamante, mentre il WorkFunc continua a eseguire sul thread di lavoro.

    private void Button_Click(object sender, RoutedEventArgs e)
    {
      Task t = Task.Run ((Azione) WorkFunc);
    }
    private void WorkFunc ()
    {
      // my work
    }

Invece di utilizzare una funzione separata, può essere impiegata una funzione anonima chiamata anche lambda. Utilizzando le lambda, mantieniamo il codice della funzione di lavoro nella funzione stessa consentendoci di utilizzare gli argomenti e le variabili locali all'interno della lambda come se fossero parte della funzione stessa.

    private void Button_Click(object sender, RoutedEventArgs e)
    {
      int steps = GetSteps();
      Task t = Task.Run (() =>
      {
        // Posso utilizzare la variabile steps qui anche se mi trovo in una
        // diversa funzione in esecuzione su un diverso thread!
        // my work
      });
    }

I Task possono anche essere parametrizzati per restituire un tipo particolare, come risultato di un calcolo della lambda.

      Task<double> t = Task.Run<double>(() =>
      {
        double risultato;
        // Calcolo della variabile risultato …
        return risultato;
      });

L'operatore await può essere utilizzato anche in linea per ottenere il risultato della funzione asincrona e senza doverlo estrarre dal Task restituito.

    private async void Button_Click(object sender, RoutedEventArgs e)
    {
      double computedValue = await Task.Run <double>(() =>
      {
        double risultato;
        // Calcolo della variabile risultato ...
        return risultato;
      });
      // L’esecuzione riprende automaticamente qui quando il task sopra ha  completato!
      MessageBox.Show (String.Format ("Il risultato era {0}", computedValue.ToString ()));
    }

C'è un piccolo ‘carico’ associato ad await, quindi è sempre più efficiente chiamare più metodi sincroni all'interno della propria lambda che chiamare molte funzioni asincrone utilizzando await. Questo è particolarmente vero quando si scrive codice in un loop, dove il costo di utilizzo await attraverso centinaia o migliaia di iterazioni diventa sostanziale.


L’utilizzo di QueuedTask

Mentre i Task sono un appuntamento fisso all'interno di qualsiasi codice dell’add-in, i task devono essere forniti in ArcGIS Pro in modo diverso dal tradizionale TAP. Il framework fornisce una schedulazione personalizzata del Task che dovrebbe essere utilizzato quando fornisci Task che effettuano chiamate ai metodi sincroni di ArcGIS Pro SDK. Invece di chiamare Task.Run, gli sviluppatori di add-in devono chiamare QueuedTask.Run.

      Task t = QueuedTask.Run (() =>
      {
        // Chiama metodi SDK sincroni
      });

La classe QueuedTask viene utilizzata al posto della classe Task per i seguenti motivi:

Controllo della concorrenza e delle code

Quando i Task vengono inviati utilizzando Task.Run, il Task associato sarà in esecuzione su un thread a caso nel pool di thread gestito ogni volta che viene chiamato. Se una chiamata successiva a Task.Run viene fatta da qualsiasi altra parte dell'applicazione, il nuovo Task inizierà a funzionare immediatamente su un altro thread mentre il primo Task è ancora in esecuzione sul primo thread. Tornando alla lista delle sfide da affrontare nel codice multithread, dovrebbe essere ovvio che l'esecuzione delle operazioni non organizzate e concorrenti rischia di far andare in crash l’applicazione o di corrompere lo stato dell'applicazione. Il comportamento di accodamento QueuedTask.Run garantisce il corretto ordine delle chiamate e riduce il rischio di conflitti. Ricordate che il parallelismo all'interno di ArcGIS Pro si realizza internamente; questo semplifica il modello di programmazione pubblica e riduce notevolmente la probabilità di conflitti.

Affinità e stato

Per motivi di prestazioni, ArcGIS Pro mantiene un notevole stato su specifici thread e, in molti casi, utilizza oggetti che hanno affinità di thread. Affinità di thread significa che un oggetto è legato ad un particolare thread e non deve interagire con qualsiasi thread ma con il thread col quale ha affinità. Vincoli di affinità sono comuni in sistemi operativi e componenti, tra cui connessioni a database, windows, controlli, code di input, timer, WPF Bitmap e server COM. In WPF, per esempio, chiamate a metodi su un qualsiasi oggetto derivato dalla classe WPF DependencyObject si tradurrà in un'eccezione se la chiamata viene effettuata da un thread da dove l'oggetto non è stato creato.
Thread nel pool di thread gestiti sono anche incompatibili con la maggior parte dei componenti COM, per cui non si dovrebbe tentare di utilizzare Task.Run con codice che potrebbe eseguire direttamente o indirettamente, componenti COM.

Integrazione dell’applicazione

Quando i Task vengono eseguiti utilizzando QueuedTask.Run, essi vengono integrati automaticamente con varie funzionalità all'interno dell'applicazione come segue:
  • L’estensione della Progress/Cancellation del framework, in cui la progress include la finestra di dialogo con lo stato di avanzamento programmabile, viene visualizzata e nascosta automaticamente e lo stato cancellazione è correttamente comunicato tra le parti interessate dell’applicazione.
  • Lo stato occupato del sistema dell’applicazione dove gli elementi dell'interfaccia utente come pulsanti e strumenti sono attivati e disattivati automaticamente quando i Task sono in esecuzione. Esecuzione dei task possono essere coordinati anche con fasi critiche quali la creazione di viste e chiusura dell'applicazione.
  • La coda dei Task è scritta nelle strutture di diagnostica del framework, quando abilitate. Questo consente agli sviluppatori di monitorare la sequenza di esecuzione dei task, i task in esecuzione e la durata dell'esecuzione. Questo tipo di informazione è preziosa per il debugging e l'analisi delle prestazioni.

Casi in cui è accettabile l'utilizzo di Task.Run

Ci sono casi in cui l'uso di Task.Run è accettabile, ad esempio quando si eseguono operazioni in background indipendenti costituite interamente da codice in modo gestito a condizione che i particolari componenti gestiti in uso non abbiano affinità di thread. Lo sviluppatore si assume la piena responsabilità della gestione della cancellazione, della visualizzazione dello stato di avanzamento, dell'abilitazione/disabilitazione dell'interfaccia utente in modo appropriato e del coordinamento delle operazioni e della gestione della logica dei conflitti.

Progress e cancellation

I metodi asincroni possono talvolta accettare un argomento Progressor, un oggetto che viene utilizzato dal chiamante per configurare le impostazioni della finestra di dialogo per lo stato di avanzamento e annullamento e di coordinare la comunicazione tra il chiamante e il chiamato. I metodi asincroni che non sono annullabili prendono una classe Progressor, mentre i metodi annullabili prendono una classe CancelableProgressor.
Gli oggetti Progressor seguono il modello stabilito dal CancelationToken di Microsoft e non possono essere creati direttamente; invece, lo sviluppatore deve creare un ProgressorSource o CancelableProgressorSource.
Gli oggetti "source" consentono di configurare come il progressor gestirà il progress senza esporre queste impostazioni al codice esterno, che potrebbero accedere al Progressor. L'oggetto ProgressorSource espone i seguenti costruttori:

  public ProgressorSource (Action<Progressor> callback)
  public ProgressorSource (ProgressDialog progDlg)
  public ProgressorSource (string message, bool delayedShow = false)

Il primo override prende un delegato che sarà chiamato, a intervalli regolari, mentre il Task è in esecuzione. Questa opzione è appropriata quando si desidera fornire un feedback specializzato durante l'esecuzione del Task.
Il secondo override prende una finestra dialogo di avanzamento, oggetto costruito separatamente. Se non è già visualizzato, il progressor mostrerà automaticamente questa finestra di dialogo di avanzamento quando il Task inizia l’esecuzione e si nasconderà automaticamente al completamento del Task. Se la finestra di dialogo è già visibile, il progressor aggiornerà il contenuto della finestra di dialogo durante l'esecuzione e sarà compito dello sviluppatore nascondere la finestra di dialogo di avanzamento al momento opportuno. Questa opzione è appropriata quando si desidera controllare manualmente la visibilità della finestra dello stato di avanzamento, ad esempio quando è necessario mantenere la finestra dello stato di avanzamento tra diverse attività separate.
Il terzo override creerà automaticamente e mostrerà una finestra di dialogo di avanzamento quando il Task inizia l'esecuzione e lo nasconde al termine del completamento del Task. Il parametro delayedShow controlla se la finestra di dialogo di avanzamento deve essere mostrata immediatamente o ritardare la sua apparizione per consentire operazioni rapide da completare ed evitare di apparire, se non necessario. Se si prevede che il Task sia rapido nel completamento dell’esecuzione, impostare questo parametro a true. Se si prevede che il Task abbia bisogno di più di uno o due secondi per completare l’operazione, impostare delayedShow a false in modo che la finestra di dialogo di avanzamento appaia immediatamente per trasmettere responsività.
CancelableProgressors richiedono un ulteriore argomento che specifica cosa dovrebbe visualizzare il messaggio di annullamento. Verrà visualizzato il messaggio di annullamento, non appena l'utente fa clic sul pulsante Annulla nella finestra di dialogo.

  public CancelableProgressorSource(Action <CancelableProgressor> callback)
  public CancelableProgressorSource(ProgressDialog progDlg)
  public CancelableProgressorSource(string message, string cancelMessage, bool delayedShow = false)

Esempio di implementazione del metodo utilizzando cancellation

Lo specializzato CancelableProgressor espone una proprietà CancellationToken che può essere utilizzata per comunicare l’annullamento. All'interno dell'implementazione del metodo, il codice in esecuzione nel loop dovrebbe controllare la proprietà IsCancellationRequested e uscire dal metodo gettando l’eccezione OperationCanceledException (che riconosce la richiesta di cancellazione) come illustrato di seguito:

public Task<long> CalcFactorialAsync(int x, CancelableProgressor progressor)
{
  return QueuedTask.Run<long>(() =>
  {
    long result = 1;
    for (int i = 1; i < x; ++i)
    {
      if (progressor.CancellationToken.IsCancellationRequested)
        throw new OperationCanceledException();
      result *= i;
    }
    return result;
  });
}

Utilizzare con i metodi asincroni la finestra di dialogo integrata

Se il Progressor è stato configurato per mostrare lo stato di avanzamento, il Task in esecuzione può aggiornare le informazioni visualizzate nella finestra di avanzamento utilizzando il progressor (entrambe Progressor e CancelableProgressor supportano finestre di dialogo di avanzamento):

public Task<long> CalcFactorialAsync(int x, Progressor progressor)
{
  return QueuedTask.Run<long>(() =>
  {
    long result = 1;
    for (int i = 1; i < x; ++i)
    {
      progressor.Message = string.Format("Working on step:{0}", i);
      result *= i;
    }
    return result;
  }, progressor);
}

Complicazioni comuni

Ipotesi di stato costante

Si consideri il seguente esempio. Questa chiamata viene richiamata dal thread della GUI e l'intento è quello di eliminare lo specifico layer dalla mappa della vista attiva.

private Task DeleteSelectedLayerAsync(Layer layer)
{
  return QueuedTask.Run(() =>
  {
      MapView.Active.Map.RemoveLayer(layer);
  });
}

Anche se semplice in apparenza, questa funzione a volte può causare un'eccezione quando in uso all'interno dell'applicazione. L'errore qui è stato quello di pensare che lo stato del sistema rimanga statico attraverso i thread. Ci possono essere precedentemente delle operazioni in coda in esecuzione e queste devono essere completate prima che un'altra operazione possa iniziare l'esecuzione. Durante questo tempo, lo stato dell’applicazione può cambiare a causa dell’interazione con l'utente o il risultato di una operazioni ancora in esecuzione. In questo caso, l’oggetto attivo potrebbe diventare una tabella prima che la lambda effettivamente inizi l’esecuzione, nel qual caso la mappa sarà nulla determinando un'eccezione. L'approccio sicuro è quello di evitare "concatenamenti" di chiamate su variabili membro o variabili passate tra thread; utilizzare variabili locali come istantanee dello stato dell'applicazione quando il metodo è stato chiamato, dal momento che non cambieranno al loro interno.

private Task DeleteSelectedLayerAsync (Layer layer)
{
  // Prendere una "istantanea" della mappa sulla vista attiva.
  Map m = MapView.Active.Map;
  return QueuedTask.Run (() =>
  {
      m.RemoveLayer(layer);
  });
}

In un ambiente multithreading si dovrebbe gestire il codice allestendo una strategia difensiva. Si consideri un Task che modifica come un particolare layer debba essere simboleggiato. Se tale Task finisce in una coda dietro un altro Task che rimuove questo stesso layer dalla mappa, la seconda operazione è logicamente invalidata dalla prima. Per gestire questo caso correttamente, il secondo Task dovrebbe essere gestito per visualizzare un avviso o annullare l'operazione in modalità silente quando viene a sapere che il layer è stato eliminato.

Thread safe con associazione dati  in WPF

Per impostazione predefinita, i dati di collection associati a WPF devono essere modificati sul thread dove è stata creata l’associazione al controllo WPF. Questa limitazione diventa un problema quando si desidera riempire la collection da un thread di lavoro per produrre una buona esperienza per l’utente. Ad esempio, un elenco dei risultati di ricerca che viene riempito gradualmente man mano che vengono trovate corrispondenze, senza costringere l'utente ad attendere fino a quando l'intera ricerca è completata.
Per ovviare a questa limitazione, WPF fornisce una classe statica BindingOperations che permette di stabilire un'associazione tra un lock e una collection (ad esempio, ObservableCollection<T>). Tale associazione consente a collection associate di essere aggiornate dai thread esterni al thread principale della GUI, in modo coordinato senza generare la consueta eccezione.
BindingOperations.EnableCollectionSynchronization(Items, _lockObj);
Nell'esempio sopra, la variabile _lockObj - di tipo oggetto - è in genere istanziata quando viene creata la classe contenitore e servirà come lock per il coordinamento. Una volta che si chiama EnableCollectionSynchronization, WPF entrerà nello specificato lock quando c’è lettura o scrittura alla collection associata. Come proprietari della collection, si è obbligati a inserire il lock durante la lettura alla collection o scrittura della collection.
I wrapper ReadOnlyObservableCollection sono comunemente usati per forzare le sole letture semantiche sulle proprietà della collection osservabile. Per impostare correttamente la sincronizzazione con multithreading, è necessario chiamare EnableCollectionSynchronization sul wrapper, invece che sulla collection stessa, dal momento che è il wrapper al quale il WPF sarà associato effettivamente.

internal class HelloWorld
{
  private ObservableCollection<string> _items = new ObservableCollection<string>();
  private ReadOnlyObservableCollection<string> _itemsRO;
  private Object _lockObj = new Object();
  internal HelloWorld()
  {
    _itemsRO = new ReadOnlyObservableCollection<string>(_items);
    BindingOperations.EnableCollectionSynchronization(_itemsRO, _lockObj);
  }
//la proprietà pubblica utilizzata per il binding
public ReadOnlyObservableCollection<string> Items { get { return _itemsRO; } }
//all’interno della funzione di lavoro, il lock è inserito prima di modificare //la collection:
public void FillCollectionAsync()
{
  QueuedTask.Run(() =>
  {
    // letture e scritture dovrebbe essere fatte all’interno del lock
    lock (_lockObj)
    {
       _items.Add( GetData() );
    }
  });
}

Oggetti "Live" come proprietà

Si deve prestare attenzione quando si espongono oggetti – soprattutto collection - come proprietà pubbliche se la collezione è destinata a cambiare in un thread separato. Se qualcuno ottiene e trattiene tale proprietà e poi inizia l'enumerazione attraverso un thread A, un'eccezione può essere generata se il proprio codice modifica la collection sul thread B in quanto non vi è alcun lock di collaborazione tra essi. Distribuire in sola lettura istantanee della collection è più sicuro.

Eseguire codice sul thread della GUI

Ci sono casi in cui di tanto in tanto, mentre il codice viene eseguito su un thread di lavoro, si incontrano situazioni in cui è necessario chiedere input da parte dell'utente prima di procedere. Non si dovrebbe cercare di presentare una finestra direttamente dal thread di lavoro poiché le finestre hanno affinità di thread. Una finestra di dialogo creata sul thread di lavoro non si collega alla coda di input del thread della GUI e non rispetta lo z-order e la politica di focus stabilita dal thread GUI. In generale, è possibile eseguire codice sul thread della GUI da un thread di lavoro usando l'oggetto dispatcher dell'applicazione.
Questo può essere fatto in modo sincrono.

      FrameworkApplication.Current.Dispatcher.Invoke (() =>
      {
        // Fare qualcosa sul thread  della GUI
        System.Windows.MessageBox.Show ("Pronto!");
      });

O in modo asincrono:

      FrameworkApplication.Current.Dispatcher.BeginInvoke (() =>
      {
        // Fare qualcosa sul thread della GUI
        System.Windows.MessageBox.Show ("Pronto!");
      });

Si dovrebbe cercare di raccogliere le informazioni necessarie da parte dell'utente sul thread della GUI prima di eseguire il lavoro in modo da non dover utilizzare questa scorciatoia. Bloccando chiamate tra thread c’è rischio di deadlock e di trattenere operazioni in esecuzione sul thread di lavoro.

La gestione delle eccezioni in ambiente asincrono

Come le funzioni sincrone, le funzioni asincrone possono gettare eccezioni. Questo introduce un problema interessante da quando il chiamante fornisce il try/catch in un thread, e l'eccezione viene generata su un altro. Inoltre, il frame del chiamante non è solitamente ancora nello stack quando viene generata l'eccezione.
Tuttavia, .NET permette di utilizzare async/await con try/catch in modo che, se viene generata un'eccezione dal codice in esecuzione all'interno del Task, sarete in grado di catturare di nuovo da dove la funzione asincrona è stata chiamata. Si noti che la funzione asincrona deve restituire Task o Task<T> per eccezioni asincrone per essere adeguatamente rediretta (non void).

      try
      {
        var result = await PrintMapAsync ();
      }
      catch (Exception e)
      {
        // gestione eccezione.
      }

Se viene generata un'eccezione da parte del worker e non è stato fornito un try/catch intorno a dove si attende il worker, il runtime .NET si collegherà all'eccezione come eccezione inner, UnobservedException.
Eccezioni Unobserved solitamente compaiono solo quando l'oggetto eccezione viene raccolto dalla garbage collection di .NET, in nessun posto vicino a dove in realtà si è verificata l'eccezione. Se si ottiene uno di questi, esaminare l'eccezione interna per ottenere lo stack delle chiamate fallite. Nella finestra di controllo di VisualStudio, è possibile utilizzare la pseudo variabile $exception per esaminare il corrente oggetto eccezione.

Oggetti Freezable

WPF definisce un modello in cui determinati tipi di oggetti possono essere "congelati". Una volta che l'oggetto è congelato, modifiche non possono essere apportate all'oggetto senza che si generi un'eccezione. Oggetti freezable possono migliorare le prestazioni in alcune situazioni, e permettono anche di condividere l'oggetto tra i thread (vedi affinità di thread). Ad esempio, se un BitmapImage viene creato su un thread di lavoro, non è possibile utilizzarlo in seguito sul thread della GUI a meno di congelarlo prima.
Si consideri un caso comune in cui l'associazione venga utilizzata in combinazione con le immagini che sono state generate su un thread di lavoro. La classe di esempio VM qui sotto espone una proprietà chiamata Img:

public class VM: INotifyPropertyChanged
  {
    public BitmapImage Img {get {return _image; }}
    ...
  }

Questa proprietà restituisce un'istanza di BitmapImage (un oggetto Freezable), che è poi associata ad un pulsante in XAML:

<Button>
<Image Source = "{Binding Img}"> </ Immagine>
</ Button>

La bitmap sottostante viene periodicamente aggiornata sul thread di lavoro come segue; notare che verrà creata la bitmap sul thread di lavoro:

public Task Refresh ()
    {
      return QueuedTask.Run (() =>
      {
        var uri = GenerateThumbnail();
        Img = new BitmapImage (uri);
      });
    }

Nel processo di rendering dell'interfaccia utente, WPF tenterà di accedere alla proprietà bitmap dal thread della GUI ... ma questo si tradurrà in un'eccezione perché la bitmap è ancora "scongelata" e quindi ancorata al suo thread di lavoro principale. Questo problema può essere risolto semplicemente "congelando" la Bitmap dopo il suo l'aggiornamento.

var uri = GenerateThumbnail ();
Img = new BitmapImage (uri);
Img.Freeze ();

Nota: non tutte le classi che ereditano da System.Windows.Freezable possono essere congelate. Utilizzare la proprietà CanFreeze per verificare se l’oggetto può essere congelato.

domenica 31 marzo 2013

Tutto il resto è Layar ... e non ho detto layer ma Layar Layar Layar ...

Nel 2010 avevo fatto riferimento alla realtà aumentata accennando a Layar. In questo post vediamo come esporre i nostri servizi ArcGIS Server a Layar e visualizzarli geolocalizzati in prossimità del luogo in cui si trova l'utente. I dati sono forniti sotto forma di livelli chiamati layer, che non sono altro che servizi web di tipo REST. I layer sono mantenuti dai pubblicatori mentre Layar è responsabile della loro convalida nel processo di pubblicazione che normalmente avviene entro 5 giorni dalla richiesta di pubblicazione. Poiché stiamo parlando di realtà aumentata su dispositivi mobile (abbiamo a disposizione sia l'app per iOS che per Android) per poter funzionare Layar necessità di GPS, magnetometro (bussola), fotocamera e accelerometro, nonché connessione a internet per ricevere i dati dei servizi online. Il nostro dispositivo inquadra in tempo reale l'ambiente circostante e al mondo reale vengono sovrapposti i livelli di contenuto (layer).

Vediamo come funziona:
  1. un utente lancia il Layer browser sul suo dispositivo mobile;
  2. il Layar Client invia la richiesta al Layar server;
  3. in base alla richiesta il Layar Server restituisce la definizione dei layer pubblicati;
  4. una lista dei layer viene inviata dal Layer Server e visualizzata sul Layar Client;
  5. un utente seleziona un layer dalla lista;
  6. una richiesta getPOIs è inviata al Layar Server;
  7. il Layar Server trasferisce la richiesta al Layer Service Provider di quel layer;
  8. il Layer Service Provider restituisce il contenuto basandosi sulle specifiche delle Developer API (response getPOIs) al Layar Server;
  9. il Layar Server convalida la response getPOIs e la invia al Layar Client
  10. il Layar Client visualizza i POI. 




Nella documentazione per lo sviluppatore potete trovare tutti i dettagli sull'architettura della piattaforma Layar.

Ora vediamo come creare un semplice Layer Service Providers per esporre i nostri POI direttamente da ArcGIS Server.
Come possiamo vedere dalla documentazione, tutte le chiamate sono RESTful  cosicché aderiscono ad una architettura REST. Correntemente solo una chiamata HTTP GET è stata definita per recuperare le risorse ed è GetPOIs.

In ArcGIS Server possiamo crearci una SOE REST che restituisce un JSON che aderisce a GetPOIs response.
In questo esempio i dati di input (request: layerName, lon, lat) e di output (response:layer, hotspots(id, anchor, text(title, description, footnote)), title, errorCode, errorString) sono solo quelli obbligatori ma nella documentazione potete trovarne molti altri opzionali che possono arricchire le funzionalità dei vostri POI.
Per semplificarci ulteriormente la vita, il raggio di ricerca viene impostato a livello di proprietà SOE ma anche il numero di POI restituiti è impostato a livello di proprietà SOE e restituiamo i più vicini ma le API ci permettono di gestire il raggio, il paging ecc.

//-----------------------------------------------------------------------
// <copyright file="LayarSOE.cs" company="Studio A&T s.r.l.">
//     Copyright (c) Studio A&T s.r.l. All rights reserved.
// </copyright>
// <author>Nicogis</author>
//-----------------------------------------------------------------------
namespace Studioat.ArcGis.Soe.Rest
{
    using System;
    using System.Collections.Generic;
    using System.Collections.Specialized;
    using System.Diagnostics.CodeAnalysis;
    using System.Globalization;
    using System.Linq;
    using System.Reflection;
    using System.Runtime.InteropServices;
    using System.Text;
    using ESRI.ArcGIS.Carto;
    using ESRI.ArcGIS.esriSystem;
    using ESRI.ArcGIS.Geodatabase;
    using ESRI.ArcGIS.Geometry;
    using ESRI.ArcGIS.Server;
    using ESRI.ArcGIS.SOESupport;
 
    [SuppressMessage("StyleCop.CSharp.NamingRules""SA1300:ElementMustBeginWithUpperCaseLetter", Justification = "Viewed.")]
    [SuppressMessage("StyleCop.CSharp.NamingRules""SA1305:FieldNamesMustNotUseHungarianNotation", Justification = "Viewed.")]
    [SuppressMessage("StyleCop.CSharp.NamingRules""SA1306:FieldNamesMustBeginWithLowerCaseLetter", Justification = "Viewed.")]
    [SuppressMessage("StyleCop.CSharp.DocumentationRules""SA1650:ElementDocumentationMustBeSpelledCorrectly", Justification = "viewed.")]
 
    /// <summary>
    /// class Layar SOE
    /// </summary>
    [ComVisible(true)]
    [Guid("ec27c9f5-a7c4-4be6-8543-d170762e8604")]
    [ClassInterface(ClassInterfaceType.None)]
    [ServerObjectExtension("MapServer",
        AllCapabilities = "",
        DefaultCapabilities = "",
        Description = "Layar SOE",
        DisplayName = "Layar SOE",
        Properties = "NearestPOIs = 10;Radius = 500",
        HasManagerPropertiesConfigurationPane = false,
        SupportsREST = true,
        SupportsSOAP = false)]
    public class LayarSOE : IServerObjectExtensionIObjectConstructIRESTRequestHandler
    {
        /// <summary>
        /// name of soe
        /// </summary>
        private string soeName;
 
        /// <summary>
        /// properties of soe
        /// </summary>
        private IPropertySet configProps;
 
        /// <summary>
        /// Helper ServerObject
        /// </summary>
        private IServerObjectHelper serverObjectHelper;
 
        /// <summary>
        /// logger of soe
        /// </summary>
        private ServerLogger logger;
 
        /// <summary>
        /// request handler
        /// </summary>
        private IRESTRequestHandler reqHandler;
 
        /// <summary>
        /// number of nearest POIs
        /// </summary>
        private int nearestPOIs;
 
        /// <summary>
        /// radius for POIs
        /// </summary>
        private int radius;
 
        /// <summary>
        /// List of POILayerInfo
        /// </summary>
        private List<POILayerInfo> poiLayerInfos;
 
        /// <summary>
        /// Initializes a new instance of the <see cref="LayarSOE"/> class
        /// </summary>
        public LayarSOE()
        {
            this.soeName = this.GetType().Name;
            this.logger = new ServerLogger();
            this.reqHandler = new SoeRestImpl(this.soeName, this.CreateRestSchema()) as IRESTRequestHandler;
        }
 
        #region IServerObjectExtension Members
 
        /// <summary>
        /// init event of soe
        /// </summary>
        /// <param name="pSOH">Helper ServerObject</param>
        public void Init(IServerObjectHelper pSOH)
        {
            this.serverObjectHelper = pSOH;
        }
 
        /// <summary>
        /// shutdown event of soe
        /// </summary>
        public void Shutdown()
        {
        }
 
        #endregion
 
        #region IObjectConstruct Members
 
        /// <summary>
        /// costruct event of soe
        /// </summary>
        /// <param name="props">properties of soe</param>
        public void Construct(IPropertySet props)
        {
            this.configProps = props;
            this.nearestPOIs = Convert.ToInt32(this.configProps.GetProperty("NearestPOIs"));
            this.radius = Convert.ToInt32(this.configProps.GetProperty("Radius"));
            this.poiLayerInfos = new List<POILayerInfo>();
            this.GetPOILayerInfos();
        }
 
        #endregion
 
        #region IRESTRequestHandler Members
 
        /// <summary>
        /// get schema of soe
        /// </summary>
        /// <returns>schema of soe</returns>
        public string GetSchema()
        {
            return this.reqHandler.GetSchema();
        }
 
        /// <summary>
        /// Handler of request rest
        /// </summary>
        /// <param name="Capabilities">capabilities of soe</param>
        /// <param name="resourceName">name of resource</param>
        /// <param name="operationName">name of operation</param>
        /// <param name="operationInput">input of operation</param>
        /// <param name="outputFormat">format of output</param>
        /// <param name="requestProperties">list of request properties</param>
        /// <param name="responseProperties">list of response properties </param>
        /// <returns>response in byte</returns>
        public byte[] HandleRESTRequest(string Capabilities, string resourceName, string operationName, string operationInput, string outputFormat, string requestProperties, out string responseProperties)
        {
            return this.reqHandler.HandleRESTRequest(Capabilities, resourceName, operationName, operationInput, outputFormat, requestProperties, out responseProperties);
        }
 
        #endregion
 
        /// <summary>
        /// create schema of soe
        /// </summary>
        /// <returns>resource of soe</returns>
        private RestResource CreateRestSchema()
        {
            RestResource rootRes = new RestResource(this.soeName, falsethis.RootResHandler);
 
            RestResource item = new RestResource("POILayers"truenew ResourceHandler(this.POILayer));
            rootRes.resources.Add(item);
 
            RestOperation getPOIsOperation = new RestOperation("getPOIs"new string[] { "lon""lat""layerName" }, new string[] { "json" }, this.getPOIsOperationHandler);
 
            item.operations.Add(getPOIsOperation);
 
            return rootRes;
        }
 
        /// <summary>
        /// handler of resource root
        /// </summary>
        /// <param name="boundVariables">list of variables bound</param>
        /// <param name="outputFormat">format of output</param>
        /// <param name="requestProperties">list of request properties</param>
        /// <param name="responseProperties">list of response properties </param>
        /// <returns>root resource in format output in byte</returns>
        private byte[] RootResHandler(NameValueCollection boundVariables, string outputFormat, string requestProperties, out string responseProperties)
        {
            responseProperties = "{\"Content-Type\" : \"application/json\"}";
 
            JsonObject result = new JsonObject();
            AddInPackageAttribute addInPackage = (AddInPackageAttribute)Assembly.GetExecutingAssembly().GetCustomAttributes(typeof(AddInPackageAttribute), false)[0];
            result.AddString("agsVersion", addInPackage.TargetVersion);
            result.AddString("soeVersion", addInPackage.Version);
            result.AddString("author", addInPackage.Author);
            result.AddString("company", addInPackage.Company);
 
            return result.JsonByte();
        }
 
        /// <summary>
        /// Handler operation Get POIs
        /// </summary>
        /// <param name="boundVariables">list of variables bound</param>
        /// <param name="operationInput">input of operation</param>
        /// <param name="outputFormat">format of output</param>
        /// <param name="requestProperties">list of request properties</param>
        /// <param name="responseProperties">list of response properties </param>
        /// <returns>response in byte</returns>
        private byte[] getPOIsOperationHandler(NameValueCollection boundVariables, JsonObject operationInput, string outputFormat, string requestProperties, out string responseProperties)
        {
            responseProperties = "{\"Content-Type\" : \"application/json\"}";
            string layerName = null;
            try
            {
                bool found = operationInput.TryGetString("layerName"out layerName);
                if (!found)
                {
                    throw new ArgumentNullException("layerName");
                }
 
                int poiLayerID = Convert.ToInt32(boundVariables["POILayersID"], CultureInfo.InvariantCulture);
 
                POILayerInfo poiLayerInfo = this.GetPOILayerInfo(poiLayerID);
 
                double? lon;
                found = operationInput.TryGetAsDouble("lon"out lon);
                if ((!found) || (!lon.HasValue))
                {
                    throw new ArgumentNullException("lon");
                }
 
                double? lat;
                found = operationInput.TryGetAsDouble("lat"out lat);
                if ((!found) || (!lat.HasValue))
                {
                    throw new ArgumentNullException("lat");
                }
 
                IFeatureClass featureClass = this.GetPOIFeatureClass(poiLayerInfo.Id);
 
                IPoint point = new PointClass();
                point.X = lon.Value;
                point.Y = lat.Value;
                point.SpatialReference = Helper.GCS1984;
 
                point.Project(poiLayerInfo.Extent.SpatialReference);
 
                ITopologicalOperator topologicalOperator = point as ITopologicalOperator;
                ISpatialFilter spatialFilter = new SpatialFilterClass();
                spatialFilter.Geometry = topologicalOperator.Buffer(this.radius);
                spatialFilter.SpatialRel = esriSpatialRelEnum.esriSpatialRelIntersects;
                spatialFilter.GeometryField = featureClass.ShapeFieldName;
                IFeatureCursor featureCursor = null;
 
                IDictionary<intdouble> dictionaryDistances = new Dictionary<intdouble>();
                try
                {
                    featureCursor = featureClass.Search(spatialFilter, true);
 
                    IFeature feature = featureCursor.NextFeature();
                    while (feature != null)
                    {
                        IGeometry geometry = feature.ShapeCopy as IGeometry;
                        if (geometry == null || geometry.IsEmpty)
                        {
                            continue;
                        }
 
                        IProximityOperator proximityOperator = feature.ShapeCopy as IProximityOperator;
                        double distance = proximityOperator.ReturnDistance(point);
 
                        dictionaryDistances.Add(feature.OID, distance);
                        feature = featureCursor.NextFeature();
                    }
                }
                catch
                {
                    throw;
                }
                finally
                {
                    Marshal.FinalReleaseComObject(featureCursor);
                }
 
                JsonObject result = new JsonObject();
                result.AddString("layer", layerName);
               
                if (dictionaryDistances.Count == 0)
                {
                    result.AddLong("errorCode", 21);
                    result.AddString("errorString""POIs not found!");
                    return Encoding.UTF8.GetBytes(result.ToJson());
                }
 
                var listPOIs = from item in dictionaryDistances orderby item.Value ascending select item.Key;
                if (dictionaryDistances.Count > this.nearestPOIs)
                {
                    listPOIs = listPOIs.Take(this.nearestPOIs);
                }
 
                int[] listPOIOIDs = listPOIs.ToArray();
 
                List<JsonObject> hotspots = new List<JsonObject>();
                try
                {
                    featureCursor = featureClass.GetFeatures(listPOIOIDs, true);
                    int idxField = featureClass.FindField("MyField");
                    IFeature feature = featureCursor.NextFeature();
                    while (feature != null)
                    {
                        string valueField = Convert.ToString(feature.get_Value(idxField));
                        JsonObject hotspot = new JsonObject();
                        hotspot.AddString("id", valueField);
                        IPoint poi = feature.ShapeCopy as IPoint;
                        poi.Project(Helper.GCS1984);
                        hotspot.AddString("anchor"string.Format("geo:{0},{1}", poi.Y.ToString(Helper.CultureUS), poi.X.ToString(Helper.CultureUS)));
                        JsonObject text = new JsonObject();
                        text.AddString("title"string.Format("Id:{0}", valueField));
                        text.AddString("description"string.Empty);
                        text.AddString("footnote""Power by MyLayer");
                        hotspot.AddJsonObject("text", text);
                        hotspot.AddString("title", valueField);
                        ////hotspot.AddString("imageURL", ""); <100Kb 100x75
                        hotspots.Add(hotspot);
                        feature = featureCursor.NextFeature();
                    }
                }
                catch
                {
                    throw;
                }
                finally 
                {
                    Marshal.FinalReleaseComObject(featureCursor);
                }
                
                result.AddArray("hotspots", hotspots.ToArray());
                result.AddLong("errorCode", 0);
                result.AddString("errorString"string.Empty);
 
                return Encoding.UTF8.GetBytes(result.ToJson());
            }
            catch
            {
                JsonObject result = new JsonObject();
                result.AddString("layer", layerName);
                result.AddLong("errorCode", 20);
                result.AddString("errorString""Error return data");
                return Encoding.UTF8.GetBytes(result.ToJson());
            }
        }
 
        /// <summary>
        /// resource POILayer
        /// </summary>
        /// <param name="boundVariables">list of variables bound</param>
        /// <param name="outputFormat">format of output</param>
        /// <param name="requestProperties">list of request properties</param>
        /// <param name="responseProperties">list of response properties </param>
        /// <returns>resource in byte</returns>
        private byte[] POILayer(NameValueCollection boundVariables, string outputFormat, string requestProperties, out string responseProperties)
        {
            responseProperties = "{\"Content-Type\" : \"application/json\"}";
            if (boundVariables["POILayersID"] == null)
            {
                JsonObject[] objectArray = System.Array.ConvertAll(this.poiLayerInfos.ToArray(), i => i.ToJsonObject());
                JsonObject jsonObject = new JsonObject();
                jsonObject.AddArray("poiLayers", objectArray);
                return jsonObject.JsonByte();
            }
            else
            {
                int layerID = Convert.ToInt32(boundVariables["POILayersID"], CultureInfo.InvariantCulture);
                return this.GetPOILayerInfo(layerID).ToJsonObject().JsonByte();
            }
        }
 
        /// <summary>
        /// get POILayerInfo from Id
        /// </summary>
        /// <param name="poiLayerID">value of poiLayerID</param>
        /// <returns>object POILayerInfo</returns>
        private POILayerInfo GetPOILayerInfo(int poiLayerID)
        {
            if (poiLayerID < 0)
            {
                throw new ArgumentOutOfRangeException("poiLayerID");
            }
 
            IMapServer3 serverObject = this.GetMapServer();
            IMapLayerInfos mapLayerInfos = serverObject.GetServerInfo(serverObject.DefaultMapName).MapLayerInfos;
            long count = mapLayerInfos.Count;
            for (int i = 0; i < count; i++)
            {
                IMapLayerInfo mapLayerInfo = mapLayerInfos.get_Element(i);
                if (mapLayerInfo.ID == poiLayerID)
                {
                    return new POILayerInfo(mapLayerInfo);
                }
            }
 
            throw new ArgumentOutOfRangeException("poiLayerID");
        }
 
        /// <summary>
        /// Feature Class from id of layer
        /// </summary>
        /// <param name="poiLayerID">id poi layer</param>
        /// <returns>feature class</returns>
        private IFeatureClass GetPOIFeatureClass(int poiLayerID)
        {
            IMapServer3 mapServer = this.GetMapServer();
            IMapServerDataAccess dataAccess = (IMapServerDataAccess)mapServer;
            return (IFeatureClass)dataAccess.GetDataSource(mapServer.DefaultMapName, poiLayerID);
        }
 
        /// <summary>
        /// Get object MapServer of ServerObject 
        /// </summary>
        /// <returns>object MapServer</returns>
        private IMapServer3 GetMapServer()
        {
            IMapServer3 mapServer = this.serverObjectHelper.ServerObject as IMapServer3;
            if (mapServer == null)
            {
                throw new LayarSOEException("Unable to access the map server.");
            }
 
            return mapServer;
        }
 
        /// <summary>
        /// From service fills list of layer point
        /// </summary>
        private void GetPOILayerInfos()
        {
            IMapServer3 serverObject = this.GetMapServer();
            IMapLayerInfos mapLayerInfos = serverObject.GetServerInfo(serverObject.DefaultMapName).MapLayerInfos;
 
            this.poiLayerInfos = new List<POILayerInfo>();
            for (int i = 0; i < mapLayerInfos.Count; i++)
            {
                IMapLayerInfo mapLayerInfo = mapLayerInfos.get_Element(i);
                if (mapLayerInfo.IsFeatureLayer)
                {
                    IFields fields = mapLayerInfo.Fields;
                    for (int j = 0; j < fields.FieldCount; j++)
                    {
                        IField field = fields.get_Field(j);
                        if (field.Type == esriFieldType.esriFieldTypeGeometry)
                        {
                            IGeometryDef geometryDef = field.GeometryDef;
                            if (geometryDef.GeometryType == ESRI.ArcGIS.Geometry.esriGeometryType.esriGeometryPoint)
                            {
                                this.poiLayerInfos.Add(new POILayerInfo(mapLayerInfo));
                            }
 
                            break;
                        }
                    }
                }
            }
        }
    }
}


Possiamo proteggere il nostro servizio ArcGIS Server esponendo un proxy per ricevere e trasferire le chiamate dal Layar Platform. Il proxy genera un token ArcGIS Server dinamicamente per poter utilizzare il servizio al quale abbiamo abilitato la SOE o possiamo crearne uno statico che memorizziamo nel file di configurazione. Inoltre nel proxy possiamo gestire, ad esempio, le richieste firmate così da verificare se provengono da Layar e quindi farle passare o meno. La firma è conforme alla firma OAuth. Quando si pubblica il layer su Layar Publishing Website, se si desidera avere delle richieste firmate, si possono impostare i parametri per l'autenticazione OAuth (OAuth signing required,  OAuth consumer key  e OAuth consumer secret).
Il solo metodo di firma supportato è HMAC-SHA1 e la Signature Base String è generata utilizzando:
  1. metodo http: GET;
  2. l'url della richiesta: nel nostro caso l'url del nostro proxy;
  3. i parametri di richiesta normalizzati inclusi quelli di oauth ad esclusione di oauth_signature. I parametri di richiesta oauth usati da Layar Developer API sono: oauth_consumer_key, oauth_signature_method, oauth_timestamp, oauth_nonce, oauth_body_hash e oauth_version.

L'OAuth utilizzata è il 2-Legged che in sostanza è una OAuth senza utente. In pratica è un modo nel quale il consumer (Layar Platform) può fare una richiesta firmata ad un provider (nostro proxy) facendo leva sull'algoritmo di firma OAuth. Questo significa che il provider ha un livello di fiducia ulteriore con il consumer ed inoltre fornisce i dati al consumer senza la necessità di ottenere l'autorizzazione (token) dall'utente.


Ed ecco il nostro proxy. Per la verifica della firma della richiesta potete scaricare le due dll (OAuth.Net.Common e OAuth.Net.Components) da http://code.google.com/p/oauth-dot-net/

<%@ WebHandler Language="C#" Class="proxy" %>
/*
  This proxy page does not have any security checks. It is highly recommended
  that a user deploying this proxy page on their web server, add appropriate
  security checks, for example checking request path, username/password, target
  url, etc.
*/
using System;
using System.Drawing;
using System.IO;
using System.Web;
using System.Collections.Generic;
using System.Text;
using System.Xml.Serialization;
using System.Web.Caching;
using System.Net;
using System.Security.Cryptography.X509Certificates;
using System.Security.Cryptography;
using OAuth.Net.Common;
using OAuth.Net.Components;
 
/// <summary>
/// Forwards requests to an ArcGIS Server REST resource.
/// </summary>
public class proxy : IHttpHandler
{
    public void ProcessRequest(HttpContext context)
    {
        System.Collections.Specialized.NameValueCollection queryString = context.Request.QueryString;
        string layerName = queryString["layerName"];
        Dictionary<stringstring> dataConfig = this.getDataFromConfigFile(layerName);
        
        if (Convert.ToBoolean(dataConfig["oauth"]))
        {
            ISigningProvider SigningProvider = new HmacSha1SigningProvider();
            var param = new OAuthParameters()
            {
                ConsumerKey = queryString["oauth_consumer_key"],
                SignatureMethod = queryString["oauth_signature_method"],
                Version = queryString["oauth_version"],
                Nonce = queryString["oauth_nonce"],
                Timestamp = queryString["oauth_timestamp"]
            };
 
            param.AdditionalParameters.Add("oauth_body_hash"HttpUtility.HtmlDecode(queryString["oauth_body_hash"]));
 
            foreach (string s in queryString.Keys)
            {
                if (!s.StartsWith("oauth_"))
                {
                    param.AdditionalParameters.Add(s, queryString[s]);
                }
            }
 
            var signatureBase = SignatureBase.Create(WebRequestMethods.Http.Get, new Uri(context.Request.Url.GetLeftPart(UriPartial.Path)), param);
 
            if (!SigningProvider.CheckSignature(signatureBase, HttpUtility.HtmlDecode(queryString["oauth_signature"]), dataConfig["oauthConsumerSecret"], null))
            {
                throw new Exception("Unauthorized");
            }
             
        }
        
        HttpResponse response = context.Response;
 
        System.Collections.Specialized.NameValueCollection nvc = new System.Collections.Specialized.NameValueCollection();
        foreach (string s in queryString.Keys)
        {
            if (!s.StartsWith("oauth_"))
            {
                nvc.Add(s, queryString[s]);
            }
        }
        
        // Get token for ags service, and url operation soe and append to the request
        if (!string.IsNullOrEmpty(dataConfig["token"]))
        {
            nvc.Add("token", dataConfig["token"]);
        }
 
        nvc.Add("f""json");
 
        System.Net.WebRequest req = System.Net.WebRequest.Create(new Uri(dataConfig["urlOperation"] + ToQueryString(nvc)));
        req.Method = context.Request.HttpMethod;
 
        // Set body of request for POST requests
        if (context.Request.InputStream.Length > 0)
        {
            byte[] bytes = new byte[context.Request.InputStream.Length];
            context.Request.InputStream.Read(bytes, 0, (int)context.Request.InputStream.Length);
            req.ContentLength = bytes.Length;
            req.ContentType = "application/x-www-form-urlencoded";
            using (Stream outputStream = req.GetRequestStream())
            {
                outputStream.Write(bytes, 0, bytes.Length);
            }
        }
 
        // Send the request to the server
        System.Net.WebResponse serverResponse = null;
        try
        {
            serverResponse = req.GetResponse();
        }
        catch (System.Net.WebException webExc)
        {
            response.StatusCode = 500;
            response.StatusDescription = webExc.Status.ToString();
            response.Write(webExc.Response);
            response.End();
            return;
        }
 
        // Set up the response to the client
        if (serverResponse != null)
        {
            response.ContentType = serverResponse.ContentType;
            using (Stream byteStream = serverResponse.GetResponseStream())
            {
 
                // Text response
                if (serverResponse.ContentType.Contains("text"))
                {
                    using (StreamReader sr = new StreamReader(byteStream))
                    {
                        string strResponse = sr.ReadToEnd();
                        response.Write(strResponse);
                    }
                }
                else
                {
                    // Binary response (image, lyr file, other binary file)
                    BinaryReader br = new BinaryReader(byteStream);
                    byte[] outb = br.ReadBytes((int)serverResponse.ContentLength);
                    br.Close();
 
                    // Tell client not to cache the image since it's dynamic
                    response.CacheControl = "no-cache";
 
                    // Send the image to the client
                    // (Note: if large images/files sent, could modify this to send in chunks)
                    response.OutputStream.Write(outb, 0, outb.Length);
                }
 
                serverResponse.Close();
            }
        }
        response.End();
    }
 
    public bool IsReusable
    {
        get
        {
            return false;
        }
    }
 
    private string ToQueryString(System.Collections.Specialized.NameValueCollection nvc)
    {
        return "?" + string.Join("&"Array.ConvertAll(nvc.AllKeys, key => string.Format("{0}={1}", key, nvc[key])));
    }
 
    // Gets the token and urlOperation for a server URL from a configuration file
    private Dictionary<string,string> getDataFromConfigFile(string layerName)
    {
        try
        {
            LayarConfig config = LayarConfig.GetCurrentConfig();
            if (config != null)
            {
                Dictionary<string,string> dct = new Dictionary<string,string>();
                dct.Add("token",config.GetToken(layerName));
                dct.Add("urlOperation",config.GetUrlOperation(layerName));
                dct.Add("oauth", config.GetOAuth(layerName).ToString());
                dct.Add("oauthConsumerSecret", config.GetOAuthConsumerSecret(layerName));
                return dct;
            }
            else
            {
                throw new ApplicationException("Layar.config file does not exist at application root, or is not readable.");
            }
        }
        catch (InvalidOperationException)
        {
            HttpResponse response = HttpContext.Current.Response;
            response.StatusCode = (int)System.Net.HttpStatusCode.Forbidden;
            response.End();
        }
        catch (Exception e)
        {
            if (e is ApplicationException)
            {
                throw e;
            }
        }
 
        throw new Exception();
    }
}
 
[XmlRoot("LayarConfig")]
public class LayarConfig
{
    #region Static Members
 
    private static object _lockobject = new object();
 
    public static LayarConfig LoadLayarConfig(string fileName)
    {
        LayarConfig config = null;
 
        lock (_lockobject)
        {
            if (System.IO.File.Exists(fileName))
            {
                XmlSerializer reader = new XmlSerializer(typeof(LayarConfig));
                using (System.IO.StreamReader file = new System.IO.StreamReader(fileName))
                {
                    config = (LayarConfig)reader.Deserialize(file);
                }
            }
        }
 
        return config;
    }
 
    public static LayarConfig GetCurrentConfig()
    {
        LayarConfig config = HttpRuntime.Cache["layarConfig"as LayarConfig;
        if (config == null)
        {
            string fileName = GetFilename(HttpContext.Current);
            config = LoadLayarConfig(fileName);
 
            if (config != null)
            {
                CacheDependency dep = new CacheDependency(fileName);
                HttpRuntime.Cache.Insert("layarConfig", config, dep);
            }
        }
 
        return config;
    }
 
    public static string GetFilename(HttpContext context)
    {
        return context.Server.MapPath("~/layar.config");
    }
    #endregion
 
    ServerUrl[] serverUrls;
 
    [XmlArray("serverUrls")]
    [XmlArrayItem("serverUrl")]
    public ServerUrl[] ServerUrls
    {
        get { return this.serverUrls; }
        set { this.serverUrls = value; }
    }
 
    public string GetUrlOperation(string layerName)
    {
        foreach (ServerUrl su in serverUrls)
        {
            if (string.Compare(layerName, su.Layername, true) == 1)
            {
                return su.UrlOperation;
            }
        }
 
        return string.Empty;
    }
    
    public string GetOAuthConsumerSecret(string layerName)
    {
        foreach (ServerUrl su in serverUrls)
        {
            if (string.Compare(layerName, su.Layername, true) == 1)
            {
                return su.OAuthConsumerSecret;
            }
        }
 
        return string.Empty;
    }
 
    public bool GetOAuth(string layerName)
    {
        foreach (ServerUrl su in serverUrls)
        {
            if (string.Compare(layerName, su.Layername, true) == 1)
            {
                return su.OAuth;
            }
        }
 
        return true;
    }
    
    
    
    public string GetToken(string layerName)
    {
        foreach (ServerUrl su in serverUrls)
        {
            if (string.Compare(layerName, su.Layername, true) == 1 && su.DynamicToken)
            {
                // Code to dynamically get the token
                string tokenService = string.Format("https://{0}/{1}/tokens?request=getToken&username={2}&password={3}&expiration=30", su.Host, su.Instance, su.UserName, su.Password);
                string token;
 
                // This script is added to force the application to certify the SSL script (if for example you have a self certificate on server)
                System.Net.ServicePointManager.ServerCertificateValidationCallback += delegate(object sender, System.Security.Cryptography.X509Certificates.X509Certificate certificate, System.Security.Cryptography.X509Certificates.X509Chain chain, System.Net.Security.SslPolicyErrors sslPolicyErrors)
                {
                    return true;
                };
 
                System.Net.WebRequest tokenRequest = System.Net.WebRequest.Create(tokenService);
                System.Net.WebResponse tokenResponse = tokenRequest.GetResponse();
                System.IO.Stream responseStream = tokenResponse.GetResponseStream();
                System.IO.StreamReader readStream = new System.IO.StreamReader(responseStream);
                token = readStream.ReadToEnd();
 
                return token;
            }
            else if (string.Compare(layerName, su.Layername, true) == 1)
            {
                return su.Token;
            }
        }
 
        return string.Empty;
    }
}
 
public class ServerUrl
{
    string urlOperation;
    string token;
    bool dynamicToken;
    string userName;
    string host;
    string password;
    string instance;
    string layername;
    string oauthConsumerSecret;
    bool oauth;
 
    [XmlAttribute("layername")]
    public string Layername
    {
        get { return layername; }
        set { layername = value; }
    }
 
    [XmlAttribute("urlOperation")]
    public string UrlOperation
    {
        get { return urlOperation; }
        set { urlOperation = value; }
    }
 
    [XmlAttribute("token")]
    public string Token
    {
        get { return token; }
        set { token = value; }
    }
 
    [XmlAttribute("dynamicToken")]
    public bool DynamicToken
    {
        get { return dynamicToken; }
        set { dynamicToken = value; }
    }
 
    [XmlAttribute("host")]
    public string Host
    {
        get { return host; }
        set { host = value; }
    }
 
    [XmlAttribute("instance")]
    public string Instance
    {
        get { return instance; }
        set { instance = value; }
    }
 
    [XmlAttribute("userName")]
    public string UserName
    {
        get { return userName; }
        set { userName = value; }
    }
 
    [XmlAttribute("password")]
    public string Password
    {
        get { return password; }
        set { password = value; }
    }
 
    [XmlAttribute("oauthConsumerSecret")]
    public string OAuthConsumerSecret
    {
        get { return oauthConsumerSecret; }
        set { oauthConsumerSecret = value; }
    }
    [XmlAttribute("oauth")]
    public bool OAuth
    {
        get { return oauth; }
        set { oauth = value; }
    }
}
 
 
Ora non ci resta che registrarci sul sito di Layar e pubblicare il nostro url endpoint:


In Additional settings avete la possibilità gestire il flusso di autenticazione degli utenti al proprio layer.

Prima della pubbicazione abbiamo la possibilità di testarlo sia via web che su mobile loggandoci con il nostro utente nell'app.