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

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

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



giovedì 31 luglio 2014

WebSocket con StreamLayer

L'evoluzione delle applicazioni web ha anche determinato una crescente richiesta di comunicazioni in tempo reale: basti pensare ad applicazioni di chat, aggiornamenti in tempo reale, giochi online ecc. Il metodo più semplice fino ad ora utilizzato è quello del polling: ad intervalli prestabiliti il client chiede e il server risponde. Lo sviluppatore tramite script invia la richiesta e mediante la risposta del server verifica o meno la presenza delle informazioni richieste; la latenza però potrebbe non essere accettabile. La richiesta può essere tenuta anche più a lungo mantenendo il collegamento con il server (long polling). Questi ed altri metodi però presentano aspetti negativi quali l'inefficienza e la complessità.
Il protocollo WebSocket risolve questi problemi perchè viene mantenuta una connessione TCP persistente, bidirezionale, full-duplex assicurata da un sistema di handshaking client-key ed un modello basato sull'origine; inoltre la trasmissione è mascherata per evitare attacchi. Per maggiori dettagli vedere The WebSocket protocol e WebSocket API.
Il framework 4.5 NET fornisce un'implementazione gestita del protocollo WebSocket  mentre i moderni browser come Chrome, Firefox, Safari, Opera e IE10 supportano la specifica.
Nativamente è supportato da Windows Server 2012 e Windows 8 con IIS8 e IIS8 Express. Con altre versioni di Windows si potrebbe utilizzare SignalR e Socket.IO che hanno anche il grande vantaggio di supportare strategie di fallback (ad esempio browser client che non supportano WebSocket).
Per installare WebSocket su Windows Server 2012:
- Aprire Server Manager;
- cliccare su Add Roles and Features;
- selezionare Role-based or Feature-based Installation e poi cliccare su Next;
- selezionare il server (il server locale è selezionato di default) e poi cliccare su Next;
- espandere Web Server (IIS) in Roles, poi espandere Web Server e poi espandere Application Development;
- selezionare WebSocket Protocol e poi cliccare su Next;
- se non sono necessarie funzionalità aggiuntive cliccare su Next;
- cliccare su Install;
- quando l'installazione è completata chiudere il wizard.



A questo punto l'IIS è abilitato a gestire il WebSocket.

Ora ci creiamo un generico handler HTTP asincrono (disponibile con .NET 4.5) e ne deriviamo una classe specializzata. Registriamo il nuovo HTTP handler nel web.config dell'applicazione e ci creiamo una pagina javascript di test utilizzando le API Esri Javascript. Nello specifico possiamo utilizzare la classe StreamLayer che permette di visualizzare feature in real time da GEP ma anche da un WebSocket che fornisce feature in formato Esri JSON.


namespace StreamLayerDemo
{
    using System;
    using System.Diagnostics;
    using System.Diagnostics.CodeAnalysis;
    using System.Net.WebSockets;
    using System.Text;
    using System.Threading;
    using System.Threading.Tasks;
    using System.Web;
    using System.Web.WebSockets;
 
    [SuppressMessage("StyleCop.CSharp.DocumentationRules""SA1600:ElementsMustBeDocumented", Justification = "Demo code")]
    public abstract class WebSocketAsyncHandler : HttpTaskAsyncHandler
    {
        /// <summary>
        /// Gets a value indicating whether this handler can be reused for another request.
        /// Should return false in case your Managed Handler cannot be reused for another request, or true otherwise.
        /// Usually this would be false in case you have some state information preserved per request.
        /// You will need to configure this handler in the Web.config file of your 
        /// web and register it with IIS before being able to use it. For more information
        /// see the following link: <see cref="http://go.microsoft.com/?linkid=8101007" />
        /// </summary>
        public override bool IsReusable
        {
            get
            {
                return false;
            }
        }
 
        private WebSocket Socket { getset; }
 
        public override async Task ProcessRequestAsync(HttpContext httpContext)
        {
            await Task.Run(() =>
            {
                if (httpContext.IsWebSocketRequest)
                {
                    httpContext.AcceptWebSocketRequest(async delegate(AspNetWebSocketContext context)
                    {
                        this.Socket = context.WebSocket;
 
                        while (this.Socket != null || this.Socket.State != WebSocketState.Closed)
                        {
                            try
                            {
                                switch (this.Socket.State)
                                {
                                    case WebSocketState.Connecting:
                                        this.OnConnecting();
                                        break;
                                    case WebSocketState.Open:
                                        this.OnOpen();
                                        break;
                                    case WebSocketState.CloseSent:
                                        this.OnClosing(falsestring.Empty);
                                        break;
                                    case WebSocketState.CloseReceived:
                                        this.OnClosing(truestring.Empty);
                                        break;
                                }
                            
                            
                                ArraySegment<byte> buffer = new ArraySegment<byte>(new byte[1024]);
                                WebSocketReceiveResult receiveResult = await this.Socket.ReceiveAsync(buffer, CancellationToken.None);
 
 
                                switch (receiveResult.MessageType)
                                {
                                    case WebSocketMessageType.Text:
                                        string message = Encoding.UTF8.GetString(buffer.Array, 0, receiveResult.Count);
                                        this.OnMessageReceived(message);
                                        break;
                                    case WebSocketMessageType.Binary:
                                        this.OnMessageReceived(buffer.Array);
                                        break;
                                    case WebSocketMessageType.Close:
                                        this.OnClosing(true, receiveResult.CloseStatusDescription);
                                        break;
                                }
                            }
                            catch (Exception ex)
                            {
                                this.OnError(ex);
                            }
                        }
                    });
                }
            });
        }
 
        protected virtual void OnConnecting()
        {
        }
 
        protected virtual void OnOpen()
        {
        }
 
        protected virtual void OnMessageReceived(string message)
        {
        }
 
        protected virtual void OnMessageReceived(byte[] bytes)
        {
        }
 
        protected virtual void OnClosing(bool isClientRequest, string message)
        {
        }
 
        protected virtual void OnClosed()
        {
        }
 
        protected virtual void OnError(Exception ex)
        {
        }
 
        [DebuggerStepThrough]
        protected async Task SendMessageAsync(byte[] message)
        {
            await this.SendMessageAsync(message, WebSocketMessageType.Binary);
        }
 
        [DebuggerStepThrough]
        protected async Task SendMessageAsync(string message)
        {
            await this.SendMessageAsync(Encoding.UTF8.GetBytes(message), WebSocketMessageType.Text);
        }
 
        private async Task SendMessageAsync(byte[] message, WebSocketMessageType messageType)
        {
            await this.Socket.SendAsync(
                new ArraySegment<byte>(message),
                messageType,
                true,
                CancellationToken.None);
        }
    }
}

Deriviamo dalla classe astratta HttpTaskAsyncHandler ed eseguiamo l'override della proprietà IsReusable e del metodo ProcessRequest ed utilizzeremo async/await e la classe task perchè processiamo task asincroni.

In questa classe ci limitiamo a  memorizzare l'istanza del WebSocket con una proprietà, verificare se la richiesta http è una richiesta WebSocket (IsWebSocketRequest) e in funzione dello stato del WebSocket richiamare il corrispondente metodo virtuale. Inoltre implementiamo il codice per inviare messaggi al client. Il WebSocket permette di inviare e ricevere messaggi (testo e binario) in modalità asincrona. Questa classe generica permette di implementare la propria classe specializza handler  WebSocket.

Le implementazioni dei metodi virtuali saranno nella classe derivata:

namespace StreamLayerDemo
{
    using System;
    using System.Diagnostics.CodeAnalysis;
    using System.Threading.Tasks;
 
    [SuppressMessage("StyleCop.CSharp.DocumentationRules""SA1600:ElementsMustBeDocumented", Justification = "Demo code")]
    public class StreamLayerWebSocketAsyncHandler : WebSocketAsyncHandler
    {
        protected override void OnOpen()
        {
            PointTicker.DefaultInstance.Update += this.PointTicker_Update;
            base.OnOpen();
        }
 
        protected override void OnClosing(bool isClientRequest, string message)
        {
            PointTicker.DefaultInstance.Update -= this.PointTicker_Update;
            base.OnClosing(isClientRequest, message);
        }
 
        protected override void OnMessageReceived(string message)
        {
            // Assignment prevents warning "Because this call is not awaited...Consider applying the 'await' operator
            // This is intentional => fire and forget
            
            //Task task = this.SendMessageAsync("Your message is: " + message);
            
        }
 
        protected override void OnError(Exception ex)
        {
            // Assignment prevents warning "Because this call is not awaited...Consider applying the 'await' operator
            // This is intentional => fire and forget
            var task = this.SendMessageAsync(string.Format("Something exceptional happened: {0}", ex.Message));
        }
 
        private void PointTicker_Update(object sender, PointTickerEventArgs e)
        {
            // Assignment prevents warning "Because this call is not awaited...Consider applying the 'await' operator
            // This is intentional => fire and forget
            var task = this.SendMessageAsync(e.Feature);
 
        }
    }
}


Per simulare l'invio di dati (in questo esempio dei Point casuali in un certo extent) utilizziamo una singola istanza di una classe che implementa un semplice Timer che ogni 5 secondi invia una feature Point al client. I metodi Start e Stop della classe vengo richiamati negli eventi globali (global.asax.cs) Start e Stop dell'applicazione.

namespace StreamLayerDemo
{
    using System;
    using System.Diagnostics.CodeAnalysis;
 
    [SuppressMessage("StyleCop.CSharp.DocumentationRules""SA1600:ElementsMustBeDocumented", Justification = "Demo code")]
    public class Global : System.Web.HttpApplication
    {
        protected void Application_Start(object sender, EventArgs e)
        {
            // var hostFactory = new PointTickerHostFactory();
            // var route = new ServiceRoute("PointTicker", hostFactory, typeof(PointTickerService));
            // System.Web.Routing.RouteTable.Routes.Add(route);
            PointTicker.DefaultInstance.Start();
        }
 
        protected void Application_End(object sender, EventArgs e)
        {
            PointTicker.DefaultInstance.Stop();
        }
    }
}


Classe per simulare l'invio di dati al client:

namespace StreamLayerDemo
{
    using System;
    using System.Diagnostics.CodeAnalysis;
    using System.Timers;
 
    [SuppressMessage("StyleCop.CSharp.DocumentationRules""SA1600:ElementsMustBeDocumented", Justification = "Demo code")]
    public class PointTicker
    {
        private const int TimerInterval = 5000;
 
        private static object lockField = new object();
 
        private static PointTicker defaultInstanceField;
 
        private PointTicker()
        {
        }
 
        public event EventHandler<PointTickerEventArgs> Update;
 
        public static PointTicker DefaultInstance
        {
            get
            {
                lock (PointTicker.lockField)
                {
                    if (PointTicker.defaultInstanceField == null)
                    {
                        PointTicker.defaultInstanceField = new PointTicker();
                        PointTicker.defaultInstanceField.Initialize();
                    }
                }
 
                return PointTicker.defaultInstanceField;
            }
        }
 
        private static Timer Timer { getset; }
 
        public void Start()
        {
            lock (PointTicker.lockField)
            {
                if (!PointTicker.Timer.Enabled)
                {
                    PointTicker.Timer.Start();
                }
            }
        }
 
        public void Stop()
        {
            lock (PointTicker.lockField)
            {
                if (PointTicker.Timer.Enabled)
                {
                    PointTicker.Timer.Stop();
                }
            }
        }
 
        protected virtual void OnUpdate(string feature)
        {
            if (this.Update != null)
            {
                this.Update(
                    this,
                    new PointTickerEventArgs()
                    {
                        Feature = feature
                    });
            }
        }
 
        private void Initialize()
        {
            PointTicker.Timer = new Timer(PointTicker.TimerInterval);
            PointTicker.Timer.Elapsed += this.Timer_Elapsed;
        }
 
        private void Timer_Elapsed(object sender, ElapsedEventArgs e)
        {
            Random random = new Random();
            string feature = string.Format("{{\"geometry\" : {{\"x\" : {0}, \"y\" : {1} }}, \"attributes\" : {{\"ObjectId\" : {2}, \"RouteID\" : 1, \"DateTimeStamp\" : {3} }}}}", random.NextDouble(8.40, 8.95).ToString(new System.Globalization.CultureInfo("en-US")), random.NextDouble(45.23, 45.85).ToString(new System.Globalization.CultureInfo("en-US")), random.Next(1, Int32.MaxValue), DateTime.Now.UnixTicks().ToString(new System.Globalization.CultureInfo("en-US")));
 
            this.OnUpdate(feature);
        }
    }
}

Ora registriamo l'handler nel web.config per indicare ad IIS di utilizzare questo handler quando richiamato.

<?xml version="1.0"?>
<!--
  For more information on how to configure your ASP.NET application, please visit
  http://go.microsoft.com/fwlink/?LinkId=169433
  -->
<configuration>
  <system.web>
    <compilation debug="true" targetFramework="4.5" />
    <httpRuntime targetFramework="4.5" />
  </system.web>
  <system.webServer>
    <handlers>
      <add name="StreamLayerWebSocketAsyncHandler" verb="*" path="wsStreamLayer" type="StreamLayerDemo.StreamLayerWebSocketAsyncHandler, StreamLayerDemo" resourceType="Unspecified" />
    </handlers>
  </system.webServer>
</configuration>

Nel Type indicheremo la classe comprensiva del namespace affinchè IIS possa trovarla e nel path indicheremo il nome del WebSocket che utilizzeremo per chiamarlo (in questo caso l'ho chiamato wsStreamLayer):
ws://<yourdomain>/<site:port>/wsStreamLayer

Per le creazione del collegamento, i WebSocket sfruttano il comando Http 'Upgrade' che indica al server che stiamo tentando di passare ad una connessione WebSocket




Come possiano notare da fiddler abbiamo anche Origin: questa origine è quella visionata dal server per capire da dove provengono i messaggi. Mentre Sec-WebSocket-Key è la chiave che compone la prima parte dell'handshake. E' generata in modo causale e codificata come stringa base64 di 16 byte. Sec-WebSocket-Version permette al server di rispondere con la versione del protocollo più adeguata alla versione supportata dal client.
In risposta dal server Sec-WebSocket-Accept ha la chiave non codificata inviata dal client concatenata con 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 e codificata in SHA-1 e successivamente in base64.
Inoltre come avviene per l'http è possibile effettuare la comunicazione WebSocket su ssl/tls tramite wss:
wss://<yourdomain>/<site:port>/wsStreamLayer

A questo punto testiamo con la classe StreamLayer delle API js Esri:

 

<!doctype html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="initial-scale=1, maximum-scale=1,user-scalable=no">
    <title>StreamLayer using ArcGIS API for JavaScript</title>
    <link rel="stylesheet" href="http://js.arcgis.com/3.10/js/dojo/dijit/themes/tundra/tundra.css">
    <link rel="stylesheet" href="http://js.arcgis.com/3.10/js/esri/css/esri.css">
    <style type="text/css">
        htmlbody {
            height100%;
            width100%;
            margin0;
            padding0;
        }
 
        body {
            background-color#fff;
            overflowhidden;
            font-familysans-serif;
        }
 
        #map {
            width100%;
            height80%;
        }
    </style>
    <script src="http://js.arcgis.com/3.10/"></script>
</head>
<body class="tundra">
    <div id="map"></div>
    <div>
        <span>Enter websocket connection: </span><input type="text" id="txtWsUrl" value="ws://localhost:55555/wsStreamLayer" style="width400px" /><br />
        <input type="button" id="cmdNewStream" value="Make Stream Layer" />
        <input type="button" id="cmdDisconnect" value="Disconnect Stream Layer" />
    </div>
 
 
    <script>
        var curTime = new Date();
        var curTimeStamp = Date.parse(curTime.toUTCString());
        var layerDefinition = {
            "geometryType""esriGeometryPoint",
            "timeInfo": {
                "startTimeField""DateTimeStamp",
                "endTimeField"null,
                "trackIdField""RouteID",
                "timeReference"null,
                "timeInterval": 1,
                "timeIntervalUnits""esriTimeUnitsMinutes",
                "exportOptions": {
                    "useTime"true,
                    "timeDataCumulative"false,
                    "timeOffset"null,
                    "timeOffsetUnits"null
                },
                "hasLiveData"true
            },
            "fields": [
              {
                  name: "ObjectId",
                  type: "esriFieldTypeOID",
                  alias: "ObjectId"
              },
              {
                  name: "DateTimeStamp",
                  type: "esriFieldTypeDate",
                  alias: "DateTimeStamp"
              },
              {
                  name: "RouteID",
                  type: "esriFieldTypeInteger",
                  alias: "RouteID"
              }
            ]
        };
 
        var map, featureCollection, streamLayer;
 
        require(["esri/map",
          "esri/TimeExtent",
          "esri/layers/StreamLayer",
          "esri/InfoTemplate",
          "esri/symbols/SimpleMarkerSymbol",
          "esri/symbols/SimpleLineSymbol",
          "esri/renderers/SimpleRenderer",
          "esri/renderers/TimeClassBreaksAger",
          "esri/renderers/TemporalRenderer",
          "esri/Color",
          "dojo/dom",
          "dojo/on",
          "dojo/domReady!"
        ], function (Map, TimeExtent, StreamLayer, InfoTemplate, SimpleMarkerSymbol, SimpleLineSymbol, SimpleRenderer, TimeClassBreaksAger, TemporalRenderer, Color, dom, on) {
            var trackedBusses = {}, cnt = 0;
 
            map = new Map("map", {
                basemap: "gray",
                center: [8.675, 45.54],
                zoom: 10
            });
 
            // event listeners for button clicks
            on(dom.byId("cmdNewStream"), "click", makeNewStreamLayer);
            on(dom.byId("cmdDisconnect"), "click", disconnectStreamLayer);
 
            function makeStreamLayer() {
                //Make FeatureCollection to define layer without using url
                featureCollection = {
                    "layerDefinition"null,
                    "featureSet": {
                        "features": [],
                        "geometryType""esriGeometryPoint"
                    }
                };
                featureCollection.layerDefinition = layerDefinition;
 
                // Instantiate StreamLayer
                // 1. socketUrl is the url to the GeoEvent Processor web socket.
                // 2. purgeOptions.displayCount is the maximum number of features the
                //    layer will display at one time
                // 3. trackIdField is the name of the field that groups features
                var layer = new StreamLayer(featureCollection, {
                    socketUrl: txtWsUrl.value,
                    purgeOptions: { displayCount: 500 },
                    trackIdField: featureCollection.layerDefinition.timeInfo.trackIdField,
                    infoTemplate: new InfoTemplate("Route Id: ${RouteID}""Timestamp: ${DateTimeStamp}")
                });
                console.log("TrackID: ", featureCollection.layerDefinition.timeInfo.trackIdField);
                console.log("TrackID: ", layer.timeInfo.trackIdField);
 
                //Make renderer and apply it to StreamLayer
                var renderer = makeRenderer();
                layer.setRenderer(renderer);
 
                //Subscribe to onMessage event of StreamLayer so can adjust map time
                layer.on("message", processMessage);
                layer.on("connect", connectevt);
                layer.on("error", errorevt);
                return layer;
            }
 
            function connectevt() {
                console.log("Connesso");
            }
 
            function errorevt() {
                console.log("error");
            }
 
            // Process message that StreamLayer received.
            function processMessage(message) {
                if (featureCollection.layerDefinition.timeInfo &&
                    featureCollection.layerDefinition.timeInfo.startTimeField) {
                    var timestamp = message.attributes[featureCollection.layerDefinition.timeInfo.startTimeField];
                    if (!map.timeExtent) {
                        map.setTimeExtent(new esri.TimeExtent(new Date(timestamp), new Date(timestamp)));
                        console.log("TIME EXTENT: ", map.timeExtent);
                    } else {
                        var tsEnd = Date.parse(map.timeExtent.endTime.toString());
                        if (timestamp > tsEnd) {
                            map.setTimeExtent(new esri.TimeExtent(map.timeExtent.startTime, new Date(timestamp)));
                            console.log("TIME EXTENT: ", map.timeExtent);
                        }
                    }
                }
            }
 
            // Make new StreamLayer and add it to map.
            function makeNewStreamLayer() {
                disconnectStreamLayer();
                streamLayer = makeStreamLayer();
                map.addLayer(streamLayer);
            }
 
            // Disconnect StreamLayer from websocket and remove it from the map
            function disconnectStreamLayer() {
                if (streamLayer) {
                    streamLayer.suspend();
                    streamLayer.disconnect();
                    streamLayer.clear();
                    map.removeLayer(streamLayer);
                    streamLayer = null;
                    //map.timeExtent = null;
                }
            }
 
            // Make temporal renderer with latest observation renderer
            function makeRenderer() {
                var obsRenderer = new SimpleRenderer(
                  new SimpleMarkerSymbol("circle", 8,
                  new SimpleLineSymbol("solid",
                  new Color([5, 112, 176, 0]), 1),
                  new Color([5, 112, 176, 0.4])
                ));
 
                var latestObsRenderer = new SimpleRenderer(
                  new SimpleMarkerSymbol("circle", 12,
                  new SimpleLineSymbol("solid",
                  new Color([5, 112, 176, 0]), 1),
                  new Color([5, 112, 176])
                ));
 
                var temporalRenderer = new TemporalRenderer(obsRenderer, latestObsRenderer, nullnull);
                return temporalRenderer;
            }
        });
    </script>
</body>
</html> 
 
 
 
 
Scaricare qui la soluzione.

sabato 31 maggio 2014

TypeScript

Oramai l'importanza di JavaScript nello sviluppo di applicazioni è ben noto a tutti. Il problema principale però rimane quando si sviluppano applicazioni di una certa complessità.
La risposta di Microsoft è stata TypeScript che ha anche utilizzato il linguaggio in casa propria per progetti come Monaco, la versione web-based di Visual Studio.
Il compilatore di TypeScript è implementato in linguaggio TypeScript ed è su CodePlex.
TypeScript è un superset di JavaScript, per la precisione un superset della sintassi ECMAScript 5 (ES5), cioè un linguaggio che estende JavaScript ma che produce in output codice JavasScript a seguito della compilazione. Essendo un superset, ogni programma JavaScript è anche un programma TypeScript. Inoltre la sintassi TypeScript include diverse caratteristiche proposte dell'ECMAScript 6 (ES6) che comprendono classi e moduli. Estendendo Javascript con tipi, classi, interfacce e moduli, si agevola lo sviluppo di applicazioni facilmente scalabili e si permette una maggiore produttività con i tool di sviluppo (refactoring, debugging, intellisense, tipi verificati staticamente ecc.). In sostanza tramite la type-safety ed una programmazione object-oriented si tutela lo sviluppo senza perdere i vantaggi di javascript, cross-platform in primis.
TypeScript 1.0 è disponibile con l'update 2 di Visual Studio 2013 , come plug-in Visual Studio 2012 o anche come package di Node.js ( npm install -g typescript) ma è disponibile anche in altri IDE/Editor (Eclipse, JetBrains IDE, Sublime Text, Emacs, Vim ecc.).
Sul sito typescriptlang.org potete trovare tutte le risorse (tutorial, help, playground ecc.) riguardo a TypeScript.
In Javascript si utilizzano molte librerie di terze parti (dojo, jquery ecc.) e per poterle utilizzare con TypeScript occorre innanzitutto dichiarare la libreria che si vuole utilizzare tramite il classico tag script. Per quel che riguarda lo sviluppo essa ha esclusivamente l'obiettivo di consentirci il normale funzionamento ma, dal punto di vista delle definizioni, non ha alcun effetto pratico.
Per permettere al compilatore TypeScript di conoscere le definizioni, occorre innanzitutto procurarsi il file di definizione "libreria.d.ts" reperibile ad esempio su github. Esiste una collezione di definizioni molto estesa e aggiornata che è possibile trovare a questo indirizzo: https://github.com/borisyankov/DefinitelyTyped o tramite nuget da console Get-Package -Filter DefinitelyTyped -ListAvailable. Il file di definizione permette di avere a disposizione l'intellisense ma soprattutto di conoscere i tipi validando il codice.
Il file di definizione per ArcGIS API for JavaScript lo trovate nel repository Esri su github all'indirizzo: https://github.com/Esri/jsapi-resources/tree/master/typescript

Infine vediamo un semplicissimo esempio con ArcGIS API for JavaScript.

Creiamo un nuovo progetto in Visual Studio 2013 utilizzando il template TypeScript:


 Importiamo il file di definizione esri.d.ts nella soluzione:


Ora ci creiamo nel progetto un nuovo file TypeScript Point.ts (i file hanno estensione ts) e, ad esempio, estendiamo la classe Point di Esri. Le classi TypeScript supportano anche l'ereditarietà.
Referenziamo il file di definizione esri.d.ts mediante una sintassi basata su un commento:

/// <reference path="../esri.d.ts" />

Ora aggiungiamo un metodo alla classe Point. Importiamo il modulo esterno di interesse assegnandoci un alias (AGSPoint)  ed estendiamo la nostra classe (Point) con la parola chiave extends aggiungendo il metodo log() ed infine indichiamo che la classe potrà essere visibile all'esterno con export (può anche essere utilizzato anche per proprietà, metodi, tipi statici e anche semplici variabili)



e questo è il relativo js prodotto (Point.js):



Ora aggiungiamo un altro file TypeScript (app.ts) dove utilizziamo, a titolo di esempio, la classe Point del modulo esterno Point.ts nel metodo di ingresso della pagine html per testarne il funzionamento:

Importiamo il modulo esterno (Point.ts) ed utilizziamo la classe Point (in questo caso ho impostato l'alias p).
Come possiamo notare quando scriviamo il codice ci viene in aiuto l'intellisense:


e questa è la nostra pagina html che richiama la funzione in app.ts



 Eseguiamo e verifichiamo dalla console del browser il risultato:


 e possiamo, come detto, andare in debug:

















venerdì 28 febbraio 2014

Custom Identity store ASP.NET

L'identity store è dove vengono gestiti gli utenti e i ruoli. ArcGIS Server ingloba un suo identity store pronto all'uso.
Nell'admin rest di ArcGIS Server possiamo vedere la configurazione di default che imposta l'identity store integrato (built-in), nella sezione security -> config -> updateIdentityStore

Ma tra le varie possibilità ArcGIS Server permette anche di crearsi il proprio identity store. Difatti la nostra organizzazione potrebbe già gestire ad esempio un database con i propri utenti ed i ruoli ai quali appartengono gli utenti e quindi se dovessimo affidarci al built-in di ArcGIS Server avremmo una duplicazione di informazioni.
ArcGIS Server fornisce il supporto per i provider ASP.NET Membership e Role.

Per poter implementare i provider personalizzati occorre implementare i seguenti metodi:

Membership provider:
  • FindUsersByName
  • GetAllUsers
  • GetUser
  • ValidateUser
Role provider:
  • GetAllRoles
  • GetRolesForUser
  • GetUsersInRole
A titolo di esempio ci creiamo il nostro database su SQL Server con le tre tabelle necessarie per poter memorizzare il minimo di informazione sul quale andremo a gestire i ruoli e gli utenti. Chiaramente nulla ci vieta di utilizzare qualsiasi database o store dove memorizzare le nostre informazioni visto che l'implementazione è a carico dello sviluppatore.

In questo caso impostiamo anche le azioni di CASCATE sulle integrità referenziali così da eliminare automaticamente le associazioni ruoli-utenti quando si eliminano ruoli o utenti. Anche in questo caso è una scelta progettuale se si opta per forzare la cancellazione.

A questo punto in Visual Studio ci creiamo una libreria utilizzando il framework 3.5.

Ci creamo due classi: una che eredita dal MembershipProvider e che rappresenta il nostro Membership Provider ed un'altra che eredita da RoleProvider e che rappresenta il nostro Role Provider.

Vediamo nel dettaglio il MembershipProvider.
Il primo metodo che andiamo ad implementare è l'Initialize dove vengono passate le proprietà di configurazione del Membership Provider. A titolo esemplificativo impostiamo una proprietà dove indicare la stringa di connessione al database e una proprietà per indicare se desideriamo ricevere eventuali errori sull'event viewer di Windows o direttamente sulla pagina. Le proprietà successivamente verranno impostate in ArcGIS Server quando indicheremo il nostro custom provider.

        /// <summary>
        /// initialize provider
        /// </summary>
        /// <param name="name">name provider</param>
        /// <param name="config">properties in config</param>
        public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config)
        {
            if (config == null)
            {
                throw new ArgumentNullException("config");
            }
 
            if (string.IsNullOrEmpty(name))
            {
                name = string.Format(CultureInfo.InvariantCulture, "{0}.{1}"this.GetType().Namespace, this.GetType().Name);
            }
 
            base.Initialize(name, config);
            this.providerName = name;
 
            string connectionStringName = config["connectionStringName"];
            if (string.IsNullOrEmpty(connectionStringName))
            {
                throw new ArcGisServerCustomProviderException("connectionStringName");
            }
            else
            {
                this.connectionString = connectionStringName;
            }
 
            this.WriteExceptionsToEventLog = Convert.ToBoolean(Helper.GetConfigValue(config["writeExceptionsToEventLog"], "false"), CultureInfo.InvariantCulture);
 
            // test for connection
            try
            {
                using (SqlConnection connection = new SqlConnection(this.connectionString))
                {
                    connection.Open();
                }
            }
            catch
            {
                throw new ArcGisServerCustomProviderException("Check your DB connection!");
            }  
        }
 
Il secondo metodo che andiamo a riscrivere è il CreateUser che è quello che viene richiamato quando in ArcGIS Server Manager aggiungiamo un utente (pulsante New User):

        /// <summary>
        /// create a user
        /// </summary>
        /// <param name="username">value of username</param>
        /// <param name="password">value of password</param>
        /// <param name="email">value email</param>
        /// <param name="passwordQuestion">password Question</param>
        /// <param name="passwordAnswer">password Answer</param>
        /// <param name="isApproved">is Approved</param>
        /// <param name="providerUserKey">provider User Key</param>
        /// <param name="status">value of status</param>
        /// <returns>object MembershipUser</returns>
        public override MembershipUser CreateUser(string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object providerUserKey, out MembershipCreateStatus status)
        {
            MembershipUser newUser = null;
            try
            {
                newUser = this.GetUser(username, false);
                if (newUser == null)
                {
                    using (SqlConnection connection = new SqlConnection(this.connectionString))
                    {
                        using (SqlCommand cmd = new SqlCommand("INSERT INTO Users (Username, Password) VALUES (@UserName, @Password)", connection))
                        {
                            cmd.Parameters.Add("@UserName"SqlDbType.NVarChar).Value = username;
                            cmd.Parameters.Add("@Password"SqlDbType.NVarChar).Value = password;
                            connection.Open();
 
                            int recordAdded = cmd.ExecuteNonQuery();
 
                            if (recordAdded > 0)
                            {
                                status = MembershipCreateStatus.Success;
                                newUser = this.GetUser(username);
                            }
                            else
                            {
                                status = MembershipCreateStatus.UserRejected;
                            }
                        }
                    }
                }
                else
                {
                    status = MembershipCreateStatus.DuplicateUserName;
                }
            }
            catch (Exception e)
            {
                status = MembershipCreateStatus.ProviderError;
 
                if (this.WriteExceptionsToEventLog)
                {
                    ArcGisServerMembershipProvider.WriteToEventLog(e, MethodBase.GetCurrentMethod().Name);
                }
                else
                {
                    throw;
                }
            }
 
            if (status != MembershipCreateStatus.Success)
            {
                throw new ArcGisServerCustomProviderException(ArcGisServerMembershipProvider.GetErrorMessage(status));
            }
 
            return newUser;
        }


Il metodo FindUsersByName serve ad ArcGIS Server per filtrare gli utenti con criterio parte-del-campo sul nome utente quindi in questo caso utilizzeremo un LIKE (casella di testo Find User) mentre il pageIndex e il pageSize è per la gestione del paging:

        /// <summary>
        /// Find Users By Name
        /// </summary>
        /// <param name="usernameToMatch">username To Match</param>
        /// <param name="pageIndex">page Index</param>
        /// <param name="pageSize">page Size</param>
        /// <param name="totalRecords">total Records</param>
        /// <returns>object MembershipUserCollection</returns>
        public override MembershipUserCollection FindUsersByName(string usernameToMatch, int pageIndex, int pageSize, out int totalRecords)
        {
            MembershipUserCollection users = new MembershipUserCollection();
            try
            {
                using (SqlConnection connection = new SqlConnection(this.connectionString))
                {
                    using (SqlCommand cmd = new SqlCommand("SELECT Count(*) FROM Users WHERE Username LIKE @Username", connection))
                    {
                        cmd.Parameters.Add("@Username"SqlDbType.NVarChar).Value = string.Format(CultureInfo.InvariantCulture, "%{0}%", usernameToMatch);
                        connection.Open();
                        totalRecords = (int)cmd.ExecuteScalar();
 
                        if (totalRecords <= 0)
                        {
                            return users;
                        }
                    }
 
                    using (SqlCommand cmd = new SqlCommand("SELECT Id, Username FROM Users WHERE Username LIKE @Username ORDER BY Username ASC", connection))
                    {
                        cmd.Parameters.Add("@Username"SqlDbType.NVarChar).Value = string.Format(CultureInfo.InvariantCulture, "%{0}%", usernameToMatch);
 
                        using (SqlDataReader reader = cmd.ExecuteReader())
                        {
                            if (reader.HasRows)
                            {
                                int counter = 0;
                                int startIndex = pageSize * pageIndex;
                                int endIndex = startIndex + pageSize - 1;
 
                                while (reader.Read())
                                {
                                    if (counter >= startIndex)
                                    {
                                        MembershipUser user = this.GetUserByReader(reader);
 
                                        users.Add(user);
                                    }
 
                                    if (counter >= endIndex) 
                                    { 
                                        cmd.Cancel(); 
                                    }
 
                                    counter++;
                                }
                            }
                        }
                    }
                }
            }
            catch (Exception e)
            {
                if (this.WriteExceptionsToEventLog)
                {
                    ArcGisServerMembershipProvider.WriteToEventLog(e, MethodBase.GetCurrentMethod().Name);
 
                    throw new ProviderException(ExceptionMessage);
                }
                else
                {
                    throw;
                }
            }
            
            return users;
        }

Il metodo GetAllUsers è per visualizzare tutti gli utenti presenti. Anche in questo caso abbiamo la gestione del paging:

        /// <summary>
        /// get all users
        /// </summary>
        /// <param name="pageIndex">page Index</param>
        /// <param name="pageSize">page Size</param>
        /// <param name="totalRecords">total Records</param>
        /// <returns>objects MembershipUserCollection</returns>
        public override MembershipUserCollection GetAllUsers(int pageIndex, int pageSize, out int totalRecords)
        {
            MembershipUserCollection users = new MembershipUserCollection();
            
            try
            {
                using (SqlConnection connection = new SqlConnection(this.connectionString))
                {
                    using (SqlCommand cmd = new SqlCommand("SELECT Count(*) FROM Users", connection))
                    {
                        connection.Open();
                        totalRecords = (int)cmd.ExecuteScalar();
 
                        if (totalRecords <= 0)
                        {
                            return users;
                        }
                    }
 
                    using (SqlCommand cmd = new SqlCommand("SELECT Id, Username FROM Users ORDER BY Username", connection))
                    {
                        using (SqlDataReader reader = cmd.ExecuteReader())
                        {
                            if (reader.HasRows)
                            {
                                int counter = 0;
                                int startIndex = pageSize * pageIndex;
                                int endIndex = startIndex + pageSize - 1;
 
                                while (reader.Read())
                                {
                                    if (counter >= startIndex)
                                    {
                                        MembershipUser user = this.GetUserByReader(reader);
                                        users.Add(user);
                                    }
 
                                    if (counter >= endIndex)
                                    { 
                                        cmd.Cancel(); 
                                    }
 
                                    counter++;
                                }
                            }
                        }
                    }
                }
            }
            catch (Exception e)
            {
                if (this.WriteExceptionsToEventLog)
                {
                    ArcGisServerMembershipProvider.WriteToEventLog(e, MethodBase.GetCurrentMethod().Name);
                    throw new ProviderException(ExceptionMessage);
                }
                else
                {
                    throw;
                }
            }
 
            totalRecords = users.Count;
 
            return users;
        }

Il metodo DeleteUser elimina l'utente. In ArcGIS Server Manager viene richiamato quando si clicca sul pulsante di eliminazione dell'utente:

        /// <summary>
        /// delete user
        /// </summary>
        /// <param name="username">value of username</param>
        /// <param name="deleteAllRelatedData">delete All Related Data</param>
        /// <returns>true is ok</returns>
        public override bool DeleteUser(string username, bool deleteAllRelatedData)
        {
            int rowsAffected = 0;
 
            try
            {
                using (SqlConnection connection = new SqlConnection(this.connectionString))
                {
                    using (SqlCommand cmd = new SqlCommand("DELETE FROM Users WHERE Username = @Username", connection))
                    {
                        cmd.Parameters.Add("@Username"SqlDbType.NVarChar).Value = username;
                        connection.Open();
 
                        rowsAffected = cmd.ExecuteNonQuery();
                    }
                }
            }
            catch (Exception e)
            {
                if (this.WriteExceptionsToEventLog)
                {
                    ArcGisServerMembershipProvider.WriteToEventLog(e, MethodBase.GetCurrentMethod().Name);
                    throw new ProviderException(ExceptionMessage);
                }
                else
                {
                    throw;
                }
            }
 
            return rowsAffected > 0;
        }

Il metodo GetUser restituisce l'utente una volta dato il suo username mentre il ValidateUser verifica se l'utente esiste. Per metodi che restituiscono gli oggetti della classe MembershipUser creeremo un oggetto fornendo al costruttore la username e il nome del provider mentre per gli altri argomenti forniremo dati fittizi o nulli poiché non gestiamo altre informazioni per l'utente (e-mail ecc.).
Per tutti gli altri metodi che non andiamo a riscrivere gettiamo un'eccezione di tipo NotImplementedException:

        /// <summary>
        /// Get Number Of Users Online
        /// </summary>
        /// <returns>number of users online</returns>
        public override int GetNumberOfUsersOnline()
        {
            throw new NotImplementedException(string.Format(CultureInfo.InvariantCulture, "Method not implemented: {0}"MethodBase.GetCurrentMethod().Name));
        }

Vediamo ora nel dettaglio il RoleProvider.
Anche qui nell'Initialize come per il Membership Provider vengo passate le proprietà di configurazione che in questo caso d'esempio sono le stesse: stringa di connessione del database e  notifica di errori nell'event viewer o direttamente nella pagina dell'ArcGIS Manager. Pertanto il metodo è analogo a quello del Membership Provider.

Il metodo CreateRole crea il ruolo e viene richiamato quando in ArcGIS Server Manager si clicca sul pulsante New Role. Attenzione che il tipo di ruolo - cioè se User, Publisher o Administrator - è un'informazione che memorizza ArcGIS Server autonomamente.

        /// <summary>
        /// create a role
        /// </summary>
        /// <param name="roleName">role name</param>
        public override void CreateRole(string roleName)
        {
            try
            {
                if (this.RoleExists(roleName))
                {
                    throw new ProviderException("Role exists!");
                }
 
                using (SqlConnection connection = new SqlConnection(this.connectionString))
                {
                    using (SqlCommand cmd = new SqlCommand("INSERT INTO Roles (Rolename) VALUES (@Role)", connection))
                    {
                        cmd.Parameters.Add("@Role"SqlDbType.NVarChar).Value = roleName;
                        
                        connection.Open();
 
                        cmd.ExecuteNonQuery();
                    }
                }
            }
            catch (Exception e)
            {
                if (this.WriteExceptionsToEventLog)
                {
                    ArcGisServerRoleProvider.WriteToEventLog(e, MethodBase.GetCurrentMethod().Name);
                }
                else
                {
                    throw;
                }
            }
        }

I metodi DeleteRole e GetAllRoles sono analoghi a quelli del Membership Provider e servono per eliminare un ruolo e listare tutti i ruoli presenti.

I metodi GetRolesForUser e GetUsersInRole vengono utilizzati da ArcGIS Server Manager per visualizzare l'associazione tra utenti e ruoli dato l'utente o il ruolo.


        /// <summary>
        /// Get of roles for user
        /// </summary>
        /// <param name="username">name of user</param>
        /// <returns>list of roles</returns>
        public override string[] GetRolesForUser(string username)
        {
            List<string> roles = new List<string>();
            try
            {
                using (SqlConnection connection = new SqlConnection(this.connectionString))
                {
                    using (SqlCommand cmd = new SqlCommand("SELECT Roles.Rolename FROM Roles INNER JOIN UsersRoles ON Roles.Id = UsersRoles.IdRolename INNER JOIN Users ON UsersRoles.IdUsername = Users.Id WHERE Users.Username = @UserName ORDER BY Roles.Rolename", connection))
                    {
                        cmd.Parameters.Add("@UserName"SqlDbType.NVarChar).Value = username;
                        connection.Open();
                        using (SqlDataReader reader = cmd.ExecuteReader())
                        {
                            if (reader.HasRows)
                            {
                                while (reader.Read())
                                {
                                    roles.Add(reader.GetString(0));
                                }
                            }
                        }
                    }
                }
            }
            catch (Exception e)
            {
                if (this.WriteExceptionsToEventLog)
                {
                    ArcGisServerRoleProvider.WriteToEventLog(e, MethodBase.GetCurrentMethod().Name);
                    throw new ProviderException(ExceptionMessage);
                }
                else
                {
                    throw;
                }
            }
 
            return roles.ToArray();
        }
 
        /// <summary>
        /// Get Users In Role
        /// </summary>
        /// <param name="roleName">name of role</param>
        /// <returns>list of users</returns>
        public override string[] GetUsersInRole(string roleName)
        {
            List<string> users = new List<string>();
            try
            {
                using (SqlConnection connection = new SqlConnection(this.connectionString))
                {
                    connection.Open();
                    using (SqlCommand cmd = new SqlCommand("SELECT Users.Username FROM Roles INNER JOIN UsersRoles ON Roles.Id = UsersRoles.IdRolename INNER JOIN Users ON UsersRoles.IdUsername = Users.Id WHERE (Roles.Rolename = @RoleName) ORDER BY Users.Username", connection))
                    {
                        cmd.Parameters.Add("@RoleName"SqlDbType.NVarChar).Value = roleName;
                        using (SqlDataReader reader = cmd.ExecuteReader())
                        {
                            if (reader.HasRows)
                            {
                                while (reader.Read())
                                {
                                    users.Add(reader.GetString(0));
                                }
                            }
                        }
                    }
                }
            }
            catch (Exception e)
            {
                if (this.WriteExceptionsToEventLog)
                {
                    ArcGisServerRoleProvider.WriteToEventLog(e, MethodBase.GetCurrentMethod().Name);
                    throw new ProviderException(ExceptionMessage);
                }
                else
                {
                    throw;
                }
            }
 
            return users.ToArray();
        }

Infine il metodo RemoveUsersFromRoles rimuove l'associazione ruolo - utente quando c'è una eliminazione di un ruolo o di un utente e pertanto viene chiamata prima del metodo di eliminazione utente o ruolo. Se abbiamo impostato l'azione di CASCATE in eliminazione questo metodo può anche non essere riscritto.


        /// <summary>
        /// Remove Users From Roles
        /// </summary>
        /// <param name="usernames">list of users</param>
        /// <param name="roleNames">list of roles</param>
        public override void RemoveUsersFromRoles(string[] usernames, string[] roleNames)
        {
            SqlTransaction transaction = null;
            try
            {
                int[] idUsers = this.Users(usernames);
                int[] idRoles = this.Roles(roleNames);
 
                using (SqlConnection connection = new SqlConnection(this.connectionString))
                {
                    connection.Open();
                    transaction = connection.BeginTransaction(MethodBase.GetCurrentMethod().Name);
                    SqlCommand cmd = connection.CreateCommand();
 
                    cmd.Connection = connection;
                    cmd.Transaction = transaction;
 
                    foreach (int u in idUsers)
                    {
                        foreach (int r in idRoles)
                        {
                            cmd.CommandText = "DELETE FROM UsersRoles" +
                                    " WHERE IdRolename = @IdRole AND IdUsername = @IdUser";
 
                            cmd.Parameters.Clear();
                            cmd.Parameters.Add("@IdRole"SqlDbType.Int).Value = r;
                            cmd.Parameters.Add("@IdUser"SqlDbType.Int).Value = u;
                            cmd.ExecuteNonQuery();
                        }
                    }
 
                    transaction.Commit();
                }
            }
            catch (Exception e)
            {
                if (this.WriteExceptionsToEventLog)
                {
                    ArcGisServerRoleProvider.WriteToEventLog(e, MethodBase.GetCurrentMethod().Name);
                }
                else
                {
                    throw;
                }
 
                if (transaction != null)
                {
                    try
                    {
                        transaction.Rollback();
                    }
                    catch (Exception e2)
                    {
                        if (this.WriteExceptionsToEventLog)
                        {
                            ArcGisServerRoleProvider.WriteToEventLog(e2, MethodBase.GetCurrentMethod().Name);
                        }
                        else
                        {
                            throw;
                        }
                    }
                }
            }
        }

Il seguente metodo di supporto scrive le eventuali eccezioni nell'event viewer di Windows:

        /// <summary>
        /// WriteToEventLog
        /// A helper function that writes exception detail to the event log. Exceptions
        /// are written to the event log as a security measure to avoid private database
        /// details from being returned to the browser. If a method does not return a status
        /// or boolean indicating the action succeeded or failed, a generic exception is also 
        /// thrown by the caller.
        /// </summary>
        /// <param name="e">object exception</param>
        /// <param name="action">value of action</param>
        private static void WriteToEventLog(Exception e, string action)
        {
            try
            {
                EventLog log = new EventLog();
                log.Source = ArcGisServerMembershipProvider.EventSource;
                log.Log = ArcGisServerMembershipProvider.EventLog;
 
                string message = "An exception occurred communicating with the data source.\n\n";
                message += "Action: " + action + "\n\n";
                message += "Exception: " + e.ToString();
 
                log.WriteEntry(message);
            }
            catch
            {
                throw;
            }
        }


Se si opta per l'event viewer occorre registrare l'event source. E' possibile anche tramite riga di comando aprendo la console di Powershell e digitando:

New-EventLog -LogName Application -Source AGSMembershipProvider
New-EventLog -LogName Application -Source AGSRoleProvider

mentre per deregistrali:

Remove-EventLog -Source AGSMembershipProvider
Remove-EventLog -Source AGSRoleProvider 

In questo esempio abbiamo utilizzato l'event log Application. In questo caso con il filtro sulla source possiamo crearci una Custom View per facilitarci la lettura.
 


La dll, una volta firmata e compilata, la registriamo nella GAC tramite il comando:

gacutil /i ArcGisServerCustomProvider.dll

o se non abbiamo sul server l'utility gacutil (ad esempio non è installato l'sdk .NET) possiamo tramite drag and drop trascinarla nella cartella di assembly.

Per disinstallarla:
gacutil /u ArcGisServerCustomProvider

oppure dalla cartella di assembly selezionare l'assembly e tramite il menu  che compare cliccando il tasto destro del mouse cliccare su disinstalla.

A questo punto registriamo il nostro custom identity store in ArcGIS Server Administrator.

La configurazione dovrà essere in formato json.
Le proprietà sono:
type: indicare 'ASP_NET'
class: indicare i riferimenti dell'assembly e della classe che implementa il Membership e il Role
properties: lista delle proprietà che verranno passate all'initialize

Pertanto per lo user store configuration sarà:

{
  "type": "ASP_NET",
  "class": "ArcGisServerCustomProvider.ArcGisServerMembershipProvider,ArcGisServerCustomProvider,Version=1.0.0.0,Culture=Neutral,PublicKeyToken=e70ef8c9eb62a069",
  "properties": {
    "connectionStringName": "Data Source=.\\SQLEXPRESS;Initial Catalog=YourDB;User Id=YourUser;Password=YourPwd;",
    "writeExceptionsToEventLog": "true"
  }
}

mentre per il role store configuration sarà:
{
  "type": "ASP_NET",
  "class": "ArcGisServerCustomProvider.ArcGisServerRoleProvider,ArcGisServerCustomProvider,Version=1.0.0.0,Culture=Neutral,PublicKeyToken=e70ef8c9eb62a069",
  "properties": {
    "connectionStringName": "Data Source=.\\SQLEXPRESS;Initial Catalog=YourDB;User Id=YourUser;Password=YourPwd;",
    "writeExceptionsToEventLog": "true"
  }
}

Consiglio: prima di aggiornare la configurazione testare il controllo formale dei propri json tramite strumenti tipo jsonlint




Qui è possibile scaricare la soluzione completa.