4. I fondamenti dello sviluppo ASP.NET
4.1. Il concetto di applicazione web ASP.NET
4.1.1. Introduzione
Un'applicazione web è un'applicazione che raggruppa vari documenti (HTML, codice .NET, immagini, suoni, ...). Questi documenti devono trovarsi nella stessa directory principale, chiamata radice dell'applicazione web. A questa radice è associato un percorso virtuale del server web. Abbiamo già incontrato il concetto di cartella virtuale per il server web Cassini. Questo concetto esiste anche per il server web IIS. Una differenza importante tra i due server è che, in un dato momento, IIS può avere un numero qualsiasi di cartelle virtuali, mentre il server web Cassini ne ha solo una, quella specificata al momento del suo avvio. Ciò significa che il server IIS può ospitare più applicazioni web contemporaneamente, mentre il server Cassini ne ospita solo una alla volta. Negli esempi precedenti, il server Cassini veniva sempre avviato con i parametri (<webroot>,/aspnet) che associavano la cartella virtuale /aspnet alla cartella fisica <webroot>. Il server web serviva quindi sempre la stessa applicazione web. Ciò non ci ha impedito di scrivere e testare pagine diverse e indipendenti all’interno di questa singola applicazione web. Ogni applicazione web dispone di risorse proprie che si trovano nella sua radice fisica <webroot>:
- una cartella [bin] in cui è possibile inserire classi precompilate
- un file [global.asax] che consente di inizializzare l’applicazione web nel suo complesso, nonché l’ambiente di esecuzione di ciascuno dei suoi utenti
- un file [web.config] che consente di configurare il funzionamento dell’applicazione
- un file [default.aspx] che funge da porta d'accesso all'applicazione
- ...
Non appena un'applicazione utilizza una di queste tre risorse, necessita di un percorso fisico e virtuale a sé stante. Non vi è infatti alcun motivo per cui due diverse applicazioni web debbano essere configurate allo stesso modo. Tutti i nostri esempi precedenti hanno potuto essere inseriti nella stessa applicazione (<webroot>,/aspnet) poiché non utilizzavano nessuna delle risorse sopra menzionate.
Torniamo all’architettura MVC raccomandata all’inizio di questo capitolo per lo sviluppo di un’applicazione web:

L’applicazione web è costituita dai file di classe (controller, classi di business, classi di accesso ai dati) e dai file di presentazione (documenti HTML, immagini, suoni, fogli di stile, ecc.). L'insieme di questi file sarà collocato in una stessa directory radice che chiameremo talvolta <application-path>. Questa directory radice sarà associata a un percorso virtuale <application-vpath>. L'associazione tra questo percorso virtuale e il percorso fisico avviene tramite la configurazione del server web. Abbiamo visto che per il server Cassini questa associazione avviene all'avvio del server. Ad esempio, in una finestra DOS, si avvierebbe Cassini con:
Nella cartella <application-path>, a seconda delle nostre esigenze, troveremo:
- la cartella [bin] in cui inserire le classi precompilate (dll)
- il file [global.asax] quando sarà necessario eseguire delle inizializzazioni, sia all’avvio dell’applicazione, sia all’avvio di una sessione utente
- il file [web.config] quando sarà necessario configurare l’applicazione
- il file [default.aspx] quando avremo bisogno di una pagina predefinita nell’applicazione
Per rispettare questo concetto di applicazione web, gli esempi che seguiranno saranno tutti collocati in una cartella <application-path> specifica dell’applicazione, alla quale sarà associata una cartella virtuale <application-vpath>, poiché il server Cassini viene avviato in modo da collegare questi due parametri.
4.1.2. Configurare un’applicazione web
Se <application-path> è la radice di un'applicazione ASP.NET, è possibile utilizzare il file <application-path>\web.config per configurarla. Questo file è in formato XML. Ecco un esempio:
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
<appSettings>
<add key="nom" value="tintin"/>
<add key="age" value="27"/>
</appSettings>
</configuration>
Si presti attenzione al fatto che i tag XML distinguono tra maiuscole e minuscole. Tutte le informazioni di configurazione devono essere racchiuse tra i tag <configuration> e </configuration>. Esistono numerose sezioni di configurazione utilizzabili. Qui ne presentiamo solo una, la sezione <appSettings>, che consente di inizializzare i dati con il tag <add>. La sintassi di questo tag è la seguente:
Quando il server Web avvia un'applicazione, verifica se in <application-path> è presente un file denominato web.config. In caso affermativo, lo legge e memorizza le relative informazioni in un oggetto di tipo [ConfigurationSettings], che sarà disponibile in tutte le pagine dell'applicazione fintanto che questa rimane attiva. La classe [ConfigurationSettings] dispone di un metodo statico [AppSettings]:

Per ottenere il valore di una chiave C del file di configurazione, si scrive ConfigurationSettings.AppSettings("C"). Si ottiene una stringa di caratteri. Per utilizzare il file di configurazione precedente, creiamo una pagina [default.aspx]. Il codice VB del file [default.aspx.vb] sarà il seguente:
Imports System.Configuration
Public Class _default
Inherits System.Web.UI.Page
Protected nom As String
Protected age As String
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
'si recuperano le informazioni di configurazione
nom = ConfigurationSettings.AppSettings("nom")
age = ConfigurationSettings.AppSettings("age")
End Sub
End Class
Si nota che, al caricamento della pagina, vengono recuperati i valori dei parametri di configurazione [nom] e [age]. Questi verranno visualizzati dal codice di presentazione di [default.aspx]:
<%@ Page src="default.aspx.vb" Language="vb" AutoEventWireup="false" Inherits="_default" %>
<html>
<head>
<title>Configuration</title>
</head>
<body>
Nom :
<% =nom %><br/>
Age :
<% =age %><br/>
</body>
</html>
Per il test, inseriamo i file [web.config], [default.aspx] e [default.aspx.vb] nella stessa cartella:
D:\data\devel\aspnet\poly\chap2\config1>dir
30/03/2004 15:06 418 default.aspx.vb
30/03/2004 14:57 236 default.aspx
30/03/2004 14:53 186 web.config
Sia <application-path> la cartella in cui si trovano i tre file dell'applicazione. Il server Cassini viene avviato con i parametri (<application-path>,/aspnet/config1). Richiediamo i file URL e [http://localhost/aspnet/config1]. Poiché [config1] è una cartella, il server web cercherà al suo interno un file [default.aspx] e lo visualizzerà se lo trova. In questo caso, lo troverà:

4.1.3. Applicazione, Sessione, Contesto
4.1.3.1. Il file global.asax
Il codice del file [global.asax] viene sempre eseguito prima che la pagina richiesta dalla richiesta corrente venga caricata. Deve trovarsi nella radice <application-path> dell'applicazione. Se presente, il file [global.asax] viene utilizzato in diversi momenti dal server web:
- all’avvio o alla chiusura dell’applicazione web
- quando una sessione utente ha inizio o termina
- quando viene avviata una richiesta utente
Come per le pagine .aspx, il file [global.asax] può essere scritto in diversi modi e, in particolare, separando il codice VB in una classe controller e nel codice di presentazione. Questa è la scelta predefinita dello strumento Visual Studio e qui faremo lo stesso. Normalmente non è prevista alcuna presentazione, poiché tale ruolo è affidato alle pagine .aspx. Il contenuto del file [global.asax] si riduce quindi a una direttiva che fa riferimento al file contenente il codice del controller:
<%@ Application src="Global.asax.vb" Inherits="Global" %>
Si noti che la direttiva non è più [Page] ma [Application]. Il codice del controller [global.asax.vb] associato e generato dallo strumento Visual Studio è il seguente:
Imports System
Imports System.Web
Imports System.Web.SessionState
Public Class Global
Inherits System.Web.HttpApplication
Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)
' Si attiva all’avvio dell’applicazione
End Sub
Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
' Si attiva all’avvio della sessione
End Sub
Sub Application_BeginRequest(ByVal sender As Object, ByVal e As EventArgs)
' Si attiva all'inizio di ogni richiesta
End Sub
Sub Application_AuthenticateRequest(ByVal sender As Object, ByVal e As EventArgs)
' Si attiva durante un tentativo di autenticazione dell'utente
End Sub
Sub Application_Error(ByVal sender As Object, ByVal e As EventArgs)
' Si attiva quando si verifica un errore
End Sub
Sub Session_End(ByVal sender As Object, ByVal e As EventArgs)
' Si attiva al termine della sessione
End Sub
Sub Application_End(ByVal sender As Object, ByVal e As EventArgs)
' Si attiva quando l'applicazione viene chiusa
End Sub
End Class
Si noti che la classe del controller deriva dalla classe [HttpApplication]. Nel ciclo di vita di un'applicazione si verificano diversi eventi importanti. Questi vengono gestiti da procedure il cui schema è riportato sopra.
- [Application_Start]: ricordiamo che un’applicazione web è “racchiusa” in un percorso virtuale. L’applicazione si avvia non appena una pagina situata in tale percorso virtuale viene richiesta da un client. Viene quindi eseguita la procedura [Application_Start]. Ciò avverrà una sola volta. In questa procedura si effettuerà ogni inizializzazione utile all’applicazione, come ad esempio la creazione di oggetti la cui durata corrisponde a quella dell’applicazione.
- [Application-End]: viene eseguita al termine dell’applicazione. A ogni applicazione è associato un tempo di inattività, configurabile in [web.config], al termine del quale l’applicazione viene considerata terminata. È quindi il server web a prendere questa decisione in base alle impostazioni dell’applicazione. Il tempo di inattività di un’applicazione è definito come il periodo durante il quale nessun client ha effettuato una richiesta per una risorsa dell’applicazione.
- [Session-Start]/[Session_End]: A ogni client è associata una sessione, a meno che l’applicazione non sia configurata come priva di sessione. Un client non è un utente davanti al proprio schermo. Se quest’ultimo ha aperto due browser per interagire con l’applicazione, rappresenta due client. Un cliente è identificato da un token di sessione che deve allegare a ciascuna delle sue richieste. Questo token di sessione è una stringa di caratteri generata in modo casuale dal server web ed è unico. Due clienti non possono avere lo stesso token di sessione. Questo token seguirà il cliente nel modo seguente:
- il cliente che effettua la sua prima richiesta non invia alcun token di sessione. Il server web lo rileva e gliene assegna uno. Questo segna l’inizio della sessione e viene eseguita la procedura [Session_Start]. Ciò avverrà una sola volta.
- il cliente effettua le richieste successive inviando il token che lo identifica. Ciò consentirà al server web di recuperare le informazioni associate a tale token. Ciò consentirà di tracciare le diverse richieste del cliente.
- L’applicazione può mettere a disposizione del cliente un modulo per la chiusura della sessione. In questo caso, è il cliente stesso a richiedere la chiusura della propria sessione. Verrà eseguita la procedura [Session_End]. Ciò avverrà una sola volta.
- Il cliente potrebbe non richiedere mai autonomamente la chiusura della propria sessione. In tal caso, dopo un certo periodo di inattività della sessione, anch’esso configurabile tramite [web.config], la sessione verrà chiusa dal server web. Verrà quindi eseguita la procedura [Session_End].
- [Application_BeginRequest]: questa procedura viene eseguita non appena arriva una nuova richiesta. Viene quindi eseguita ad ogni richiesta proveniente da qualsiasi client. È un buon punto in cui esaminare la richiesta prima di inoltrarla alla pagina richiesta. Si può anche decidere di reindirizzarla verso un’altra pagina.
- [Application_Error]: viene eseguita ogni volta che si verifica un errore non gestito esplicitamente dal codice del controller [global.asax.vb]. Qui è possibile reindirizzare la richiesta del cliente verso una pagina che spiega la causa dell’errore.
Se nessuno di questi eventi deve essere gestito, il file [global.asax] può essere ignorato. È quanto è stato fatto con i primi esempi di questo capitolo.
4.1.3.2. Esempio 1
Sviluppiamo un’applicazione per comprendere meglio le tre fasi: l’avvio dell’applicazione, della sessione e di una richiesta del cliente. Il file [global.asax] sarà il seguente:
Il file [global.asax.vb] associato sarà il seguente:
Imports System
Imports System.Web
Imports System.Web.SessionState
Public Class global
Inherits System.Web.HttpApplication
Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)
' Viene generato all'avvio dell'applicazione
' si registra l'ora
Dim startApplication As String = Date.Now.ToString("T")
' viene memorizzata nel contesto dell'applicazione
Application.Item("startApplication") = startApplication
End Sub
Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
' Si attiva all'avvio della sessione
' si registra l'ora
Dim startSession As String = Date.Now.ToString("T")
' la si inserisce nella sessione
Session.Item("startSession") = startSession
End Sub
Sub Application_BeginRequest(ByVal sender As Object, ByVal e As EventArgs)
' si registra l'ora
Dim startRequest As String = Date.Now.ToString("T")
' la si inserisce nella sessione
Context.Items("startRequest") = startRequest
End Sub
End Class
I punti salienti del codice sono i seguenti:
- il server web mette a disposizione della classe [HttpApplication] di [global.asax.vb] un certo numero di oggetti:
- Applicazione di tipo [HttpApplicationState] – rappresenta l’applicazione web – fornisce l’accesso a un dizionario di oggetti [Application.Item] accessibile a tutti i client dell’applicazione – consente la condivisione di informazioni tra diversi client – l’accesso simultaneo di più client agli stessi dati in lettura/scrittura richiede una sincronizzazione dei client.
- Sessione di tipo [HttpSessionState] - rappresenta un cliente specifico - fornisce accesso a un dizionario di oggetti [Session.Item] accessibile a tutte le richieste di quel cliente - consentirà di memorizzare informazioni su un cliente che potranno essere recuperate nel corso delle sue richieste.
- Richiesta di tipo [HttpRequest] - rappresenta la richiesta HTTP corrente del cliente
- Risposta di tipo [HttpResponse]: rappresenta la risposta HTTP che il server sta generando per il cliente
- Server di tipo [HttpServerUtility] - offre metodi di utilità, in particolare per reindirizzare la richiesta a una pagina diversa da quella inizialmente prevista.
- Contesto di tipo [HttpContext] - questo oggetto viene ricreato ad ogni nuova richiesta, ma è condiviso da tutte le pagine coinvolte nell'elaborazione della richiesta - consente di trasmettere informazioni da una pagina all'altra durante l'elaborazione di una richiesta grazie al suo dizionario Items.
- La procedura [Application_Start] registra l’inizio dell’applicazione in una variabile memorizzata in un dizionario accessibile a livello di applicazione
- la procedura [Session_Start] registra l’inizio della sessione in una variabile memorizzata in un dizionario accessibile a livello di sessione
- la procedura [Application_BeginRequest] registra l’inizio della richiesta in una variabile memorizzata in un dizionario accessibile a livello di richiesta (c.a.d disponibile per tutta la durata della sua elaborazione ma persa al termine della stessa)
La pagina di destinazione sarà la seguente pagina [main.aspx]:
<%@ Page src="main.aspx.vb" Language="vb" AutoEventWireup="false" Inherits="main" %>
<html>
<head>
<title>global.asax</title>
</head>
<body>
jeton de session :
<% =jeton %><br/>
début Application :
<% =startApplication %><br/>
début Session :
<% =startSession %><br/>
début Requête :
<% =startRequest %><br/>
</body>
</html>
Questa pagina di presentazione mostra i valori calcolati dal suo controller [main.aspx.vb]:
Public Class main
Inherits System.Web.UI.Page
Protected startApplication As String
Protected startSession As String
Protected startRequest As String
Protected jeton as String
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
' si recuperano le informazioni relative all’applicazione e alla sessione
jeton=Session.SessionId
startApplication = Application.Item("startApplication").ToString
startSession = Session.Item("startSession").ToString
startRequest = Context.Items("startRequest").ToString
End Sub
End Class
Il controller si limita a recuperare le tre informazioni inserite rispettivamente nell'applicazione, nella sessione e nel contesto tramite [global.asax.vb].
Testiamo l'applicazione nel modo seguente:
- i file sono raggruppati in un'unica cartella <application-path>

- il server Cassini viene avviato con i parametri (<application-path>,/aspnet/globalasax1)
- un primo client richiede l’URL [http://localhost/aspnet/globalasax1/main.aspx] e ottiene il seguente risultato:

- lo stesso client effettua una nuova richiesta (opzione Ricarica del browser):

Si può notare che è cambiata solo l’ora della richiesta. Ciò evidenzia due aspetti:
- le procedure [Application_Start] e [Session_Start] di [global.asax] non sono state eseguite durante la seconda richiesta.
- gli oggetti [Application] e [Session], in cui erano memorizzati gli orari di inizio dell’applicazione e della sessione, sono ancora disponibili per la seconda richiesta.
- Si avvia un secondo browser per creare un secondo client e si richiede nuovamente lo stesso URL:

Questa volta, notiamo che l’ora della sessione è cambiata. Il secondo browser, sebbene si trovi sullo stesso computer, è stato considerato come un secondo client e per esso è stata creata una nuova sessione. Si può notare che i due client non hanno lo stesso token di sessione. L’ora di inizio dell’applicazione non è cambiata, il che significa che:
- la procedura [Application_Start] di [global.asax.vb] non è stata eseguita
- l’oggetto [Application], in cui è stata memorizzata l’ora di inizio dell’applicazione, è accessibile al secondo cliente. È quindi in questo oggetto che devono essere memorizzate le informazioni che devono essere condivise dai diversi clienti dell'applicazione, mentre l'oggetto [Session] serve a memorizzare le informazioni che devono essere condivise dalle richieste di uno stesso cliente.
4.1.3.3. Una panoramica
Con quanto appreso finora, siamo in grado di tracciare un primo schema esplicativo del funzionamento di un server web e delle applicazioni web che esso supporta:

Lo schema precedente mostra un server che serve due applicazioni, indicate con A e B, ciascuna con due client. Un server web è in grado di servire più applicazioni web contemporaneamente. Queste sono completamente indipendenti l’una dall’altra. Ci concentreremo sull’applicazione A. L’elaborazione di una richiesta del client-1A all’applicazione A si svolgerà nel modo seguente:
- il client 1A richiede al server web una risorsa appartenente al dominio dell’applicazione A. Ciò significa che richiede una risorsa URL della forma [http://machine:port/VA/ressource], dove VA è il percorso virtuale dell’applicazione A.
- Se il server web rileva che si tratta della prima richiesta di una risorsa dell'applicazione A, attiva l'evento [Application_Start] del file [global.asax] dell'applicazione A. Verrà creato un oggetto [ApplicationA] di tipo [HttpApplicationState]. I vari codici dell’applicazione memorizzeranno in questo oggetto dati con ambito [Application] e c.a.d, relativi a tutti gli utenti. L’oggetto [ApplicationA] rimarrà in memoria fino a quando il server web non chiuderà l’applicazione A.
- se il server web rileva inoltre di avere a che fare con un nuovo client dell'applicazione A, attiverà l'evento [Session_Start] del file [global.asax] dell'applicazione A. Verrà creato un oggetto [Session-1A] di tipo [HttpSessionState]. Questo oggetto consentirà all’applicazione A di memorizzare oggetti con ambito [Session] e c.a.d, ovvero oggetti appartenenti a un cliente specifico. L’oggetto [Session-1A] esisterà fintanto che il cliente 1A effettuerà richieste. Consentirà di monitorare tale cliente. Il server web rileva di avere a che fare con un nuovo cliente in due casi:
- il client non gli ha inviato un token di sessione nelle intestazioni HTTP della sua richiesta
- il client gli ha inviato un token di sessione che non esiste (malfunzionamento del client o tentativo di pirateria informatica) o che non esiste più. Un token di sessione scade infatti dopo un certo periodo di inattività del client (20 minuti per impostazione predefinita con IIS). Questo periodo è configurabile.
- In ogni caso, il server web attiverà l’evento [Application_BeginRequest] del file [global.asax]. Questo evento avvia l’elaborazione di una richiesta del client. È frequente non elaborare questo evento e passare il controllo alla pagina richiesta dal client, che a sua volta elaborerà la richiesta. È anche possibile utilizzare questo evento per analizzare la richiesta, elaborarla e decidere quale pagina inviare in risposta. Utilizzeremo questa tecnica per implementare un’applicazione conforme all’architettura MVC di cui abbiamo parlato.
- Una volta superato il filtro [global.asax], la richiesta del client viene inoltrata a una pagina .aspx che provvederà a elaborarla. Vedremo in seguito che è possibile far passare la richiesta attraverso un filtro composto da più pagine. L’ultima pagina avrà il compito di inviare la risposta al client. Le pagine possono aggiungere alla richiesta iniziale del cliente le informazioni che hanno calcolato. Possono memorizzare queste informazioni nella collezione Context.Items. Infatti, tutte le pagine coinvolte nell’elaborazione della richiesta di un cliente hanno accesso a questo serbatoio di dati.
- Il codice delle diverse pagine ha accesso ai bacini di dati costituiti dagli oggetti [ApplicationA], [Session-1A], ... È importante ricordare che il server web gestisce contemporaneamente più clienti per l’applicazione A. Tutti questi clienti hanno accesso all’oggetto [Application A]. Se devono modificare i dati in questo oggetto, è necessario eseguire un’operazione di sincronizzazione tra i client. Ogni client XA ha inoltre accesso al serbatoio di dati [Session-XA]. Poiché quest’ultimo è a lui riservato, non è necessaria alcuna sincronizzazione.
- Il server web serve contemporaneamente diverse applicazioni web. Non vi è alcuna interferenza tra i client di queste diverse applicazioni.
Da queste spiegazioni si possono trarre i seguenti punti:
- in un dato momento, un server web serve più client contemporaneamente. Ciò significa che non attende la fine di una richiesta per elaborarne un’altra. In un istante T, vi sono quindi diverse richieste in fase di elaborazione appartenenti a client diversi per applicazioni diverse. I codici di elaborazione che si svolgono contemporaneamente all’interno del server web vengono talvolta definiti thread di esecuzione.
- I thread di esecuzione dei client di applicazioni web diverse non interferiscono tra loro. Esiste un isolamento.
- I thread di esecuzione dei client di una stessa applicazione possono dover condividere dei dati:
- i thread di esecuzione delle richieste di due clienti diversi (con token di sessione diversi) possono condividere dati tramite l’oggetto [Application].
- I thread di esecuzione delle richieste successive di uno stesso client possono condividere dati tramite l’oggetto [Session].
- i thread di esecuzione delle pagine successive che elaborano la stessa richiesta di un determinato cliente possono condividere dati tramite l'oggetto [Context].
4.1.3.4. Esempio 2
Sviluppiamo un nuovo esempio che illustri quanto appena visto. Raccogliamo nella stessa cartella i seguenti file:
[global.asax]
[global.asax.vb]
Imports System
Imports System.Web
Imports System.Web.SessionState
Public Class global
Inherits System.Web.HttpApplication
Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)
' Si attiva all’avvio dell’applicazione
' inizializza il contatore dei clienti
Application.Item("nbRequêtes") = 0
End Sub
Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
' Si attiva all’avvio della sessione
' Inizializza il contatore delle richieste
Session.Item("nbRequêtes") = 0
End Sub
End Class
Il principio di funzionamento dell'applicazione consiste nel contare il numero totale di richieste effettuate all'applicazione e il numero di richieste per cliente. All'avvio dell'applicazione [Application_Start], il contatore delle richieste effettuate all'applicazione viene azzerato. Questo contatore è collocato nell'ambito [Application] poiché deve essere incrementato da tutti i clienti. Quando un cliente si presenta per la prima volta a [Session_Start], si azzera il contatore delle richieste effettuate da quel cliente. Questo contatore è collocato nell’ambito [Session] poiché riguarda solo un determinato cliente.
Una volta eseguito [global.asax], verrà eseguito il file successivo [main.aspx]:
<%@ Page src="main.aspx.vb" Language="vb" AutoEventWireup="false" Inherits="main" %>
<html>
<head>
<title>application-session</title>
</head>
<body>
jeton de session :
<% =jeton %>
<br />
requêtes Application :
<% =nbRequêtesApplication %>
<br />
requêtes Client :
<% =nbRequêtesClient %>
<br />
</body>
</html>
Visualizza tre informazioni calcolate dal suo controller:
- l'identità del cliente tramite il suo token di sessione: [jeton]
- il numero totale di richieste effettuate all'applicazione: [nbRequêtesApplication]
- il numero totale di richieste effettuate dal cliente identificato al punto 1: [nbRequêtesClient]
Le tre informazioni vengono calcolate in [main.aspx.vb]:
Public Class main
Inherits System.Web.UI.Page
Protected nbRequêtesApplication As String
Protected nbRequêtesClient As String
Protected jeton As String
Private Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.Load
' Un'altra richiesta per l'applicazione
Application.Item("nbRequêtes") = CType(Application.Item("nbRequêtes"), Integer) + 1
' Un'altra richiesta nella sessione
Session.Item("nbRequêtes") = CType(Session.Item("nbRequêtes"), Integer) + 1
' Inizializza le variabili di presentazione
nbRequêtesApplication = Application.Item("nbRequêtes").ToString
jeton = Session.SessionID
nbRequêtesClient = Session.Item("nbRequêtes").ToString
End Sub
End Class
Quando viene eseguito [main.aspx.vb], stiamo elaborando una richiesta di un determinato cliente. Utilizziamo l’oggetto [Application] per incrementare il numero di richieste dell’applicazione e l’oggetto [Session] per incrementare il numero di richieste del cliente di cui stiamo elaborando la richiesta. Ricordiamo che, sebbene tutti i clienti di una stessa applicazione condividano lo stesso oggetto [Application], ciascuno di essi dispone di un oggetto [Session] proprio.
Testiamo l’applicazione inserendo i quattro file precedenti in una cartella denominata <application-path> e avviamo il server Cassini con i parametri (<application-path>,/aspnet/webapplia). Apriamo un primo browser e richiediamo l’URL [http://localhost/aspnet/webapplia/main.aspx]:

Effettuiamo una seconda richiesta tramite il pulsante [Reload]:

Avviamo un secondo browser per richiedere lo stesso URL. Per il server web, si tratta di un nuovo client:

Si può notare che il token di sessione è cambiato e che quindi abbiamo un nuovo client. Ciò si riflette nel numero di richieste del client. Torniamo ora al primo browser e richiediamo nuovamente lo stesso URL:

Il numero di richieste effettuate all’applicazione viene correttamente conteggiato.
4.1.3.5. La necessità di sincronizzare i client di un’applicazione
Nell’applicazione precedente, il contatore delle richieste inviate all’applicazione viene incrementato nella procedura [Form_Load] della pagina [main.aspx] nel modo seguente:
' un'altra richiesta per l'applicazione
Application.Item("nbRequêtes") = CType(Application.Item("nbRequêtes"), Integer) + 1
Questa istruzione, sebbene semplice, richiede diverse istruzioni del processore per essere eseguita. Supponiamo che ne servano tre:
- lettura del contatore
- incremento del contatore
- riscrivita del contatore
Il server web gira su una macchina multitasking, il che significa che a ogni task viene assegnato il processore per alcuni millisecondi prima di perderlo e poi riottenerlo dopo che anche tutti gli altri task hanno avuto la loro quota di tempo. Supponiamo che due client, A e B, inviino una richiesta contemporaneamente al server web. Supponiamo che il cliente A abbia la precedenza, che entri nella procedura [Form_Load] da [main.aspx.vb], legga il contatore (=100) e poi venga interrotto perché il suo quantum di tempo è esaurito. Supponiamo ora che sia il turno del cliente B e che questi subisca la stessa sorte: riesce a leggere il valore del contatore (=100) ma non ha il tempo di incrementarlo. I clienti A e B sono entrambi in possesso di un contatore pari a 100. Supponiamo che torni il turno del cliente A: incrementa il proprio contatore, lo porta a 101 e poi termina. È il turno del cliente B, che ha in suo possesso il vecchio valore del contatore e non quello nuovo. Anche lui porta quindi il valore del contatore a 101 e termina. Il valore del contatore delle richieste dell’applicazione è ora errato.
Per illustrare questo problema, riprendiamo l’applicazione precedente e la modifichiamo come segue:
- i file [global.asax], [global.asax.vb] e [main.aspx] rimangono invariati
- il file [main.aspx.vb] diventa il seguente:
Imports System.Threading
Public Class main
Inherits System.Web.UI.Page
Protected nbRequêtesApplication As Integer
Protected nbRequêtesClient As Integer
Protected jeton As String
Private Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.Load
' un'altra richiesta per l'applicazione e la sessione
' lettura dei contatori
nbRequêtesApplication = CType(Application.Item("nbRequêtes"), Integer)
nbRequêtesClient = CType(Session.Item("nbRequêtes"), Integer)
' attesa di 5 s
Thread.Sleep(5000)
' incremento dei contatori
nbRequêtesApplication += 1
nbRequêtesClient += 1
' salvataggio dei contatori
Application.Item("nbRequêtes") = nbRequêtesApplication
Session.Item("nbRequêtes") = nbRequêtesClient
' inizializzazione delle variabili di presentazione
jeton = Session.SessionID
End Sub
End Class
L'aggiornamento dei contatori è stato suddiviso in quattro fasi:
- lettura del contatore
- sospensione del thread di esecuzione
- incremento del contatore
- riscrivita del contatore
Consideriamo nuovamente i nostri due client A e B. Tra la fase di lettura e quella di incremento dei contatori delle richieste, costringiamo il thread di esecuzione a fermarsi per 5 secondi. Ciò avrà come conseguenza immediata la perdita del processore, che verrà quindi assegnato a un’altra attività. Supponiamo che il cliente A sia il primo a eseguire l’operazione. Leggerà il valore N del contatore e verrà interrotto per 5 secondi. Se durante questo intervallo il cliente B dispone del processore, dovrebbe leggere lo stesso valore N del contatore. Alla fine, entrambi i clienti dovrebbero visualizzare lo stesso valore del contatore, il che sarebbe anomalo.
Testiamo l’applicazione inserendo i quattro file precedenti in una cartella che chiamiamo <application-path> e avviamo il server Cassini con i parametri (<application-path>,/aspnet/webapplib). Prepariamo due browser diversi con l’URL [http://localhost/aspnet/webapplib/main.aspx]. Avviamo il primo browser affinché richieda la pagina URL, quindi, senza attendere la risposta che arriverà 5 secondi dopo, avviamo il secondo browser. Dopo poco più di 5 secondi, otteniamo il seguente risultato:

Si nota:
- che ci sono due client diversi (non hanno lo stesso token di sessione)
- che ogni client ha effettuato una richiesta
- che il contatore delle richieste effettuate all’applicazione dovrebbe quindi essere a 2 in uno dei due browser. Ma non è così.
Ora facciamo un altro esperimento. Con lo stesso browser, inviamo cinque richieste all’URL [http://localhost/aspnet/webapplib/main.aspx]. Anche in questo caso, le inviamo una dopo l’altra senza attendere i risultati. Una volta eseguite tutte le richieste, otteniamo il seguente risultato per l’ultima:

Si può notare che:
- che le 5 richieste sono state considerate provenienti dallo stesso client poiché il contatore delle richieste del client è a 5. Anche se non mostrato sopra, si constata che il token di sessione è effettivamente lo stesso per tutte e 5 le richieste.
- che il contatore delle richieste effettuate all’applicazione è corretto.
Cosa si può concludere? Nulla di definitivo. Forse il server web non avvia l’esecuzione di una richiesta di un cliente se quest’ultimo ne ha già una in corso? Non ci sarebbe quindi mai simultaneità nell’esecuzione delle richieste dello stesso cliente. Verrebbero eseguite una dopo l’altra. Questo punto va verificato. Potrebbe infatti dipendere dal tipo di client utilizzato.
4.1.3.6. Sincronizzazione dei client
Il problema evidenziato nell’applicazione precedente è un classico problema (ma non semplice da risolvere) di accesso esclusivo a una risorsa. Nel nostro caso specifico, occorre fare in modo che due client A e B non possano trovarsi contemporaneamente nella sequenza di codice:
- lettura del contatore
- incremento del contatore
- riscrivere il contatore
Una sequenza di codice di questo tipo viene definita sequenza critica. Essa richiede la sincronizzazione dei thread chiamati a eseguirla simultaneamente. La piattaforma .NET offre diversi strumenti per garantirla. In questa sede utilizzeremo la classe [Mutex].

In questa sede utilizzeremo solo i seguenti costruttori e metodi:
crea un oggetto di sincronizzazione M | |
Il thread T1 che esegue l'operazione M.WaitOne() richiede la proprietà dell'oggetto di sincronizzazione M. Se il mutex M non è detenuto da alcun thread (come avviene inizialmente), viene “assegnato” al thread T1 che lo ha richiesto. Se poco dopo un thread T2 esegue la stessa operazione, verrà bloccato. Infatti, un mutex può appartenere a un solo thread. Verrà sbloccato quando il thread T1 rilascerà il mutex M che detiene. Diversi thread possono quindi rimanere bloccati in attesa del mutex M. | |
Il thread T1, che esegue l’operazione M.ReleaseMutex(), rinuncia al possesso del mutex M. Quando il thread T1 perderà il controllo del processore, il sistema potrà assegnarlo a uno dei thread in attesa del mutex M. Solo uno di essi lo otterrà a sua volta, mentre gli altri in attesa di M rimarranno bloccati |
Un mutex M gestisce l’accesso a una risorsa condivisa R. Un thread richiede la risorsa R tramite M.WaitOne() e la restituisce tramite M.ReleaseMutex(). Una sezione critica di codice che deve essere eseguita da un solo thread alla volta è una risorsa condivisa. La sincronizzazione dell’esecuzione della sezione critica può avvenire in questo modo:
dove M è un oggetto Mutex. Ovviamente non bisogna mai dimenticare di liberare un Mutex che non serve più, in modo che un altro thread possa a sua volta entrare nella sezione critica; altrimenti i thread in attesa di un mutex mai liberato non avranno mai accesso al processore. Inoltre, occorre evitare la situazione di interblocco (deadlock) in cui due thread si aspettano a vicenda. Consideriamo le seguenti azioni che si susseguono nel tempo:
- un thread T1 acquisisce il controllo di un mutex M1 per accedere a una risorsa condivisa R1
- un thread T2 acquisisce il controllo di un mutex M2 per accedere a una risorsa condivisa R2
- il thread T1 richiede il mutex M2. Viene bloccato.
- il thread T2 richiede il mutex M1. Viene bloccato.
In questo caso, i thread T1 e T2 si attendono a vicenda. Questo caso si verifica quando i thread necessitano di due risorse condivise: la risorsa R1 controllata dal mutex M1 e la risorsa R2 controllata dal mutex M2. Una possibile soluzione consiste nel richiedere entrambe le risorse contemporaneamente tramite un unico mutex M. Tuttavia, ciò non è sempre possibile, in particolare se comporta un lungo blocco di una risorsa costosa. Un’altra soluzione consiste nel far sì che un thread in possesso di M1, non potendo ottenere M2, rilasci M1 per evitare il deadlock.
Se mettiamo in pratica ciò che abbiamo appena appreso, la nostra applicazione diventa la seguente:
- i file [global.asax] e [main.aspx] rimangono invariati
- il file [global.asax.vb] diventa il seguente:
Imports System
Imports System.Web
Imports System.Web.SessionState
Imports System.Threading
Public Class global
Inherits System.Web.HttpApplication
Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)
' Si attiva all'avvio dell'applicazione
' inizializzazione contatore clienti
Application.Item("nbRequêtes") = 0
' creazione di un blocco di sincronizzazione
Application.Item("verrou") = New Mutex
End Sub
Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
' Si attiva all'avvio della sessione
' Inizializzazione del contatore delle richieste
Session.Item("nbRequêtes") = 0
End Sub
End Class
L'unica novità è la creazione di un [Mutex] che verrà utilizzato dai clienti per la sincronizzazione. Poiché deve essere accessibile a tutti i clienti, viene inserito nell'oggetto [Application].
- Il file [main.aspx.vb] diventa il seguente:
Imports System.Threading
Public Class main
Inherits System.Web.UI.Page
Protected nbRequêtesApplication As Integer
Protected nbRequêtesClient As Integer
Protected jeton As String
Private Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.Load
' Un'ulteriore richiesta per l'applicazione e la sessione
' si entra in una sezione critica - si acquisisce il blocco di sincronizzazione
Dim verrou As Mutex = CType(Application.Item("verrou"), Mutex)
' si richiede di accedere in solitaria alla sezione critica successiva
verrou.WaitOne()
' lettura dei contatori
nbRequêtesApplication = CType(Application.Item("nbRequêtes"), Integer)
nbRequêtesClient = CType(Session.Item("nbRequêtes"), Integer)
' attesa di 5 s
Thread.Sleep(5000)
' incremento dei contatori
nbRequêtesApplication += 1
nbRequêtesClient += 1
' salvataggio dei contatori
Application.Item("nbRequêtes") = nbRequêtesApplication
Session.Item("nbRequêtes") = nbRequêtesClient
' si consente l'accesso alla sezione critica
verrou.ReleaseMutex()
' inizializzazione delle variabili di presentazione
jeton = Session.SessionID
End Sub
End Class
Si nota che il client:
- richiede di accedere da solo alla sezione critica. A tal fine, richiede la proprietà esclusiva del mutex [verrou]
- rilascia il mutex [verrou] al termine della sezione critica, in modo che un altro client possa a sua volta accedere alla sezione critica.
Testiamo l’applicazione inserendo i quattro file precedenti in una cartella che chiamiamo <application-path> e avviamo il server Cassini con i parametri (<application-path>,/aspnet/webapplic). Prepariamo due browser diversi con l’URL [http://localhost/aspnet/webapplic/main.aspx]. Avviamo il primo in modo che richieda URL, poi, senza attendere la risposta che arriverà 5 secondi dopo, avviamo il secondo browser. Dopo poco più di 5 secondi, otteniamo il seguente risultato:

Questa volta, il contatore delle richieste dell’applicazione è corretto.
Da questa lunga dimostrazione si evince l’assoluta necessità di sincronizzare i client di una stessa applicazione web, qualora debbano aggiornare elementi condivisi da tutti i client.
4.1.3.7. Gestione del token di sessione
Abbiamo parlato più volte del token di sessione scambiato tra il client e il server web. Ricordiamone il principio:
- il client effettua una prima richiesta al server. Non invia alcun token di sessione.
- A causa dell’assenza del token di sessione nella richiesta, il server riconosce un nuovo client e gli assegna un token. A questo token è associato anche un oggetto [Session] che verrà utilizzato per memorizzare le informazioni specifiche di quel client. Il token accompagnerà tutte le richieste di questo cliente. Sarà incluso nelle intestazioni HTTP della risposta inviata alla prima richiesta del cliente.
- Il cliente ora conosce il proprio token di sessione. Lo invierà nelle intestazioni HTTP di ciascuna delle successive richieste che invierà al server web. Grazie al token, il server potrà recuperare l’oggetto [Session] associato al cliente.
Per evidenziare questo meccanismo, riprendiamo l’applicazione precedente modificando solo il file [main.aspx.vb]:
Imports System.Threading
Public Class main
Inherits System.Web.UI.Page
Protected nbRequêtesApplication As Integer
Protected nbRequêtesClient As Integer
Protected jeton As String
Private Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.Load
' un'ulteriore richiesta per l'applicazione e la sessione
' si accede a una sezione critica - si acquisisce il blocco di sincronizzazione
Dim verrou As Mutex = CType(Application.Item("verrou"), Mutex)
' si richiede di accedere da soli alla sezione successiva
verrou.WaitOne()
' lettura dei contatori
nbRequêtesApplication = CType(Application.Item("nbRequêtes"), Integer)
nbRequêtesClient = CType(Session.Item("nbRequêtes"), Integer)
' attesa di 5 s
Thread.Sleep(5000)
' incremento dei contatori
nbRequêtesApplication += 1
nbRequêtesClient += 1
' salvataggio dei contatori
Application.Item("nbRequêtes") = nbRequêtesApplication
Session.Item("nbRequêtes") = nbRequêtesClient
' si consente l'accesso alla sezione critica
verrou.ReleaseMutex()
' inizializzazione delle variabili di presentazione
jeton = Session.SessionID
End Sub
Private Sub Page_Init(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.Init
' si memorizza la richiesta del client in request.txt nella cartella dell'applicazione
Dim requestFileName As String = Me.MapPath(Me.TemplateSourceDirectory) + "\request.txt"
Me.Request.SaveAs(requestFileName, True)
End Sub
End Class
Quando si verifica l'evento [Page_Init], salviamo la richiesta del client nella cartella dell'applicazione. Ricordiamo alcuni punti:
- [TemplateSourceDirectory] rappresenta il percorso virtuale della pagina in esecuzione,
- MapPath (TemplateSourceDirectory) rappresenta il percorso fisico corrispondente. Questo ci permette di costruire il percorso fisico del file da generare,
- [Request] è un oggetto che rappresenta la richiesta attualmente in elaborazione. Questo oggetto è stato creato utilizzando la richiesta grezza inviata dal cliente, c.a.d, una sequenza di righe di testo della forma:

- Request.Save([FileName]) salva l’intera richiesta del cliente (le intestazioni HTTP ed eventualmente il documento che segue) in un file il cui percorso viene passato come parametro.
Potremo quindi sapere esattamente quale sia stata la richiesta del client. Testiamo l’applicazione inserendo i quattro file precedenti in una cartella che chiamiamo <application-path> e avviamo il server Cassini con i parametri (<application-path>,/aspnet/session1). Quindi, con un browser, richiediamo il file URL
[http://localhost/aspnet/session1/main.aspx]. Otteniamo il seguente risultato:

Utilizziamo il file [request.txt] salvato da [main.aspx.vb] per accedere alla richiesta del browser:
GET /aspnet/session1/main.aspx HTTP/1.1
Cache-Control: max-age=0
Connection: keep-alive
Keep-Alive: 300
Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0,5
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Accept-Encoding: gzip,deflate
Accept-Language: en-us,en;q=0.5
Host: localhost
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.0; en-US; rv:1.7b) Gecko/20040316
Notiamo che il browser ha effettuato la richiesta per URL e [/aspnet/session1/main.aspx], inviando altre informazioni di cui abbiamo già parlato nel capitolo precedente. Non si vede alcun token di sessione. La pagina ricevuta in risposta mostra invece che il server ha creato un token di sessione. Non sappiamo ancora se il browser lo abbia ricevuto. Effettuiamo ora una seconda richiesta con lo stesso browser (Ricarica). Otteniamo la seguente nuova risposta:

Il tracciamento della sessione è effettivamente presente, poiché il numero di richieste della sessione è stato correttamente incrementato. Vediamo ora il contenuto del file [request.txt]:
GET /aspnet/session1/main.aspx HTTP/1.1
Cache-Control: max-age=0
Connection: keep-alive
Keep-Alive: 300
Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Accept-Encoding: gzip,deflate
Accept-Language: en-us,en;q=0.5
Cookie: ASP.NET_SessionId=y153tk45sise0lrhdzrf22m3
Host: localhost
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.0; en-US; rv:1.7b) Gecko/20040316
Si nota che, per questa seconda richiesta, il browser ha inviato al server una nuova intestazione HTTP [Cookie:] che definisce un'informazione denominata [ASP.NET_SessionId] e il cui valore è il token di sessione che abbiamo visto comparire nella risposta alla prima richiesta. Grazie a questo token, il server web collegherà questa nuova richiesta all’oggetto [Session] identificato dal token [y153tk45sise0lrhdzrf22m3] e recupererà il contatore di richieste associato.
Non sappiamo ancora attraverso quale meccanismo il server abbia inviato il token al client, poiché non abbiamo accesso alla risposta HTTP del server. Ricordiamo che questa ha la stessa struttura della richiesta del client, ovvero un insieme di righe di testo della forma:

Abbiamo avuto modo di utilizzare un client web che ci consentiva di accedere alla risposta HTTP del server web, ovvero il client curl. Lo utilizziamo nuovamente, in una finestra DOS, per interrogare lo stesso URL del browser precedente:
E:\curl>curl --include http://localhost/aspnet/session1/main.aspx
HTTP/1.1 200 OK
Server: Microsoft ASP.NET Web Matrix Server/0.6.0.0
Date: Thu, 01 Apr 2004 07:31:42 GMT
X-AspNet-Version: 1.1.4322
Set-Cookie: ASP.NET_SessionId=qxnxmqmvhde3al55kzsmx445; path=/
Cache-Control: private
Content-Type: text/html; charset=utf-8
Content-Length: 228
Connection: Close
<HTML>
<HEAD>
<title>application-session</title>
</HEAD>
<body>
jeton de session :
qxnxmqmvhde3al55kzsmx445
<br>
requêtes Application :
3
<br>
requêtes Client :
1
<br>
</body>
</HTML>
Abbiamo la risposta alla nostra domanda. Il server web invia il token di sessione sotto forma di un’intestazione HTTP [Set-Cookie:]:
Effettuiamo la stessa richiesta senza inviare il token di sessione. Otteniamo la seguente risposta:
E:\curl>curl --include http://localhost/aspnet/session1/main.aspx
HTTP/1.1 200 OK
Server: Microsoft ASP.NET Web Matrix Server/0.6.0.0
Date: Thu, 01 Apr 2004 07:36:06 GMT
X-AspNet-Version: 1.1.4322
Set-Cookie: ASP.NET_SessionId=cs2p12mehdiz5v55ihev1kaz; path=/
Cache-Control: private
Content-Type: text/html; charset=utf-8
Content-Length: 228
Connection: Close
<HTML>
<HEAD>
<title>application-session</title>
</HEAD>
<body>
jeton de session :
cs2p12mehdiz5v55ihev1kaz
<br>
requêtes Application :
4
<br>
requêtes Client :
1
<br>
</body>
</HTML>
Poiché non abbiamo rinviato il token di sessione, il server non è riuscito a identificarci e ci ha assegnato un nuovo token. Per proseguire una sessione già avviata, il client deve rinviare al server il token di sessione che ha ricevuto. Lo faremo qui utilizzando l’opzione [--cookie clé=valeur] di curl, che genererà l’intestazione HTTP [Cookie: clé=valeur]. Abbiamo visto che il browser aveva inviato questa intestazione HTTP durante la sua seconda richiesta.
E:\curl>curl --include --cookie ASP.NET_SessionId=cs2p12mehdiz5v55ihev1kaz http://localhost/aspnet/session1/main.aspx
HTTP/1.1 200 OK
Server: Microsoft ASP.NET Web Matrix Server/0.6.0.0
Date: Thu, 01 Apr 2004 07:40:20 GMT
X-AspNet-Version: 1.1.4322
Cache-Control: private
Content-Type: text/html; charset=utf-8
Content-Length: 228
Connection: Close
<HTML>
<HEAD>
<title>application-session</title>
</HEAD>
<body>
jeton de session :
cs2p12mehdiz5v55ihev1kaz
<br>
requêtes Application :
5
<br>
requêtes Client :
2
<br>
</body>
</HTML>
Si notano diverse cose:
- il contatore delle richieste del client è stato effettivamente incrementato, a dimostrazione del fatto che il server ha riconosciuto correttamente il nostro token.
- il token di sessione visualizzato dalla pagina è proprio quello che abbiamo inviato
- il token di sessione non è più presente nelle intestazioni HTTP inviate dal server web. Infatti, quest’ultimo lo invia una sola volta: al momento della generazione del token all’avvio di una nuova sessione. Una volta che il client ha ottenuto il proprio token, spetta a lui utilizzarlo quando lo desidera per farsi riconoscere.
Nulla impedisce a un client di utilizzare più token di sessione, come mostra il seguente esempio con [curl], in cui utilizziamo il token ottenuto durante la nostra prima richiesta (richiesta n. 1):
E:\curl>curl --include --cookie ASP.NET_SessionId=qxnxmqmvhde3al55kzsmx445 http://localhost/aspnet/session1/main.aspx
HTTP/1.1 200 OK
Server: Microsoft ASP.NET Web Matrix Server/0.6.0.0
Date: Thu, 01 Apr 2004 07:48:47 GMT
X-AspNet-Version: 1.1.4322
Cache-Control: private
Content-Type: text/html; charset=utf-8
Content-Length: 228
Connection: Close
<HTML>
<HEAD>
<title>application-session</title>
</HEAD>
<body>
jeton de session :
qxnxmqmvhde3al55kzsmx445
<br>
requêtes Application :
6
<br>
requêtes Client :
2
<br>
</body>
</HTML>
Cosa significa questo esempio? Abbiamo inviato un token ottenuto poco prima. Quando il server web crea un token, lo conserva finché il client associato a quel token continua a inviargli richieste. Dopo un certo periodo di inattività (20 minuti per impostazione predefinita con IIS), il token viene eliminato. L'esempio precedente mostra che abbiamo utilizzato un token ancora attivo.
Potremmo essere curiosi di sapere quali siano state le richieste HTTP del client [curl] durante tutte queste operazioni. Sappiamo che sono state registrate nel file [request.txt]. Ecco l’ultima:
GET /aspnet/session1/main.aspx HTTP/1.1
Pragma: no-cache
Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, */*
Cookie: ASP.NET_SessionId=qxnxmqmvhde3al55kzsmx445
Host: localhost
User-Agent: curl/7.10.8 (win32) libcurl/7.10.8 OpenSSL/0.9.7a zlib/1.1.4
Qui si trova effettivamente l'intestazione HTTP che invia il token di sessione.
Le informazioni trasmesse dal server tramite le intestazioni HTTP e [Set-Cookie:] sono denominate cookie. Il server può utilizzare questo meccanismo per trasmettere informazioni diverse dal token di sessione. Quando il server S trasmette un cookie a un client, ne indica anche la durata D e l’U associata. Ciò significa che, quando il client richiede al server S un URL della forma /U/percorso, può inviare nuovamente il cookie se non lo ha ricevuto da un tempo superiore a D. Nulla impedisce a un client di non rispettare questo codice di condotta. I browser, invece, lo rispettano. Alcuni browser consentono di accedere al contenuto dei cookie che ricevono. È il caso del browser Mozilla. Ecco, ad esempio, le informazioni relative al cookie inviato dal server in un esempio precedente:

In esso si trovano:
- il nome del cookie [ASP.NET_SessionId]
- il suo valore [y153...m3]
- il dispositivo a cui è associato: [localhost]
- l'URL a cui è associato: [/]
- la sua durata [at end of session]
Il browser invierà quindi il token di sessione ogni volta che richiederà un URL nella forma [http://localhost/...], c.a.d. ogni volta che richiederà un URL al server web del computer [localhost]. La durata del cookie corrisponde a quella della sessione. Per il browser, ciò significa che il cookie non scade mai. Lo invierà ogni volta che richiederà un URL dal computer [localhost]. Pertanto, se il browser riceve il token di sessione il giorno D, viene chiuso e riutilizzato il giorno successivo, invierà nuovamente il token di sessione (che è stato conservato in un file). Il server riceverà questo token che non possiede più, poiché un token di sessione ha una durata limitata sul server (20 minuti su IIS). Di conseguenza, avvierà una nuova sessione.
È possibile disabilitare l’uso dei cookie su un browser. In questo caso, il client riceve il token di sessione ma non lo rinvia, il che impedisce il tracciamento della sessione. Per dimostrarlo, disabilitiamo l’uso dei cookie sul nostro browser (Mozilla in questo caso):

Inoltre, eliminiamo tutti i cookie esistenti:

Fatto ciò, riavviamo il server Cassini per ripartire da zero e, dal browser, richiediamo nuovamente l’URL [http://localhost/aspnet/session1/main.aspx]:

Verifichiamo se il nostro browser ha memorizzato un cookie:

Notiamo che il browser non ha memorizzato il cookie del token di sessione che il server gli ha inviato. È quindi prevedibile che non vi sia alcun tracciamento della sessione. Richiediamo nuovamente lo stesso URL (Ricarica):

Otteniamo proprio il risultato previsto. Il browser non ha rinviato il token di sessione, che aveva ricevuto ma non memorizzato. Il server ha quindi avviato una nuova sessione con un nuovo token. Da questo esempio si evince che la nostra politica di tracciamento della sessione viene compromessa se l’utente ha disabilitato l’uso dei cookie sul proprio browser. Esiste tuttavia un altro modo, oltre ai cookie, per scambiare il token di sessione tra server e client. È infatti possibile segnalare al server web che l’applicazione funziona senza cookie. Ciò avviene tramite il file di configurazione [web.config]:
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
<system.web>
<sessionState cookieless="true" timeout="10" />
</system.web>
</configuration>
Il file di configurazione sopra riportato indica che l'applicazione funzionerà senza cookie (cookieless="true") e che il tempo massimo di inattività di un token di sessione è di 10 minuti (timeout="10"). Trascorso questo tempo, la sessione associata al token viene interrotta. Il processo di scambio del token di sessione tra il server e il client sarà il seguente:
- il client richiede l'URL [http://machine:port/V/chemin], dove V è una cartella virtuale del server web
- il server genera un token J e risponde al client di reindirizzarsi all’URL [http://machine:port/V/(J)/chemin]. Ha quindi inserito il token nell’URL da interrogare, immediatamente dopo la cartella virtuale V
- il client segue questo reindirizzamento e richiede il nuovo URL URL [http://machine:port/V/(J)/chemin].
- Il server risponde a questa richiesta e invia una pagina di risposta.
Illustriamo questi diversi punti. Inseriamo l’intera applicazione precedente in una nuova cartella <application-path>. In questa stessa cartella inseriamo il file [web.config] precedente. Inoltre, modifichiamo il codice di presentazione [main.aspx] per includervi un link:
<%@ Page src="main.aspx.vb" Language="vb" AutoEventWireup="false" Inherits="main" %>
<HTML>
<HEAD>
<title>application-session</title>
</HEAD>
<body>
jeton de session :
<% =jeton %>
<br>
requêtes Application :
<% =nbRequêtesApplication %>
<br>
requêtes Client :
<% =nbRequêtesClient %>
<br>
<a href="main.aspx">Recharger l'application</a>
</body>
</HTML>
Questo link rimanda alla pagina [main.aspx] ed è quindi equivalente al pulsante (Ricarica) del browser. Il server Cassini viene avviato con i parametri (<application-path>,/session2). In questo caso ci discostiamo dalla nostra prassi abituale, che consisteva nell'annotare la cartella virtuale [/aspnet/XX]. Infatti, a causa dell’inserimento del token di sessione nell’URL, la cartella virtuale deve contenere solo un elemento: /XX. Utilizziamo innanzitutto il client [curl] per richiedere l’URL [http://localhost/session2/main.aspx]:
E:\curl>curl --include http://localhost/session2/main.aspx
HTTP/1.1 302 Found
Server: Microsoft ASP.NET Web Matrix Server/0.6.0.0
Date: Thu, 01 Apr 2004 13:52:36 GMT
X-AspNet-Version: 1.1.4322
Location: /session2/(hinadjag3bt0u155g5hqe245)/main.aspx
Cache-Control: private
Content-Type: text/html; charset=utf-8
Content-Length: 163
Connection: Close
<html><head><title>Object moved</title></head><body>
<h2>Object moved to <a href='/session2/(hinadjag3bt0u155g5hqe245)/main.aspx'>here
</body></html>
Notiamo che il server risponde con l'intestazione HTTP [HTTP/1.1 302 Found] invece di [HTTP/1.1 200 OK]. Si tratta di un'intestazione che richiede al client di reindirizzarsi all'URL indicato dall'intestazione HTTP Location [Location: /session2/(hinadjag3bt0u155g5hqe245)/main.aspx]. Si nota il token di sessione che è stato inserito nell’URL di reindirizzamento. Un browser che riceve questa risposta richiede il nuovo URL in modo trasparente per l’utente, il quale non vede la nuova richiesta. Nel caso in cui il browser non gestisca autonomamente il reindirizzamento, viene inviato un documento HTML dopo il codice HTTP sopra riportato. In esso è presente un link all’URL di reindirizzamento, sul quale l’utente potrà cliccare.
Ora facciamo la stessa cosa con un browser in cui i cookie sono stati disabilitati. Richiediamo nuovamente l’URL [http://localhost/session2/main.aspx]. Otteniamo la seguente risposta dal server:

Innanzitutto, notiamo che l’URL visualizzato dal browser non è quello che abbiamo richiesto. Questo indica che è avvenuto un reindirizzamento. Infatti, il browser visualizza sempre l'URL URL dell'ultimo documento ricevuto. Pertanto, se non visualizza l'URL [http://localhost/session2/main.aspx], significa che gli è stato richiesto di reindirizzarsi verso un altro URL. Possono esserci diversi reindirizzamenti. L'URL visualizzato dal browser è quello dell'ultimo reindirizzamento. Possiamo notare che il token di sessione è presente nell'URL visualizzato dal browser. Lo si può vedere perché questo token viene visualizzato anche dal nostro programma nella pagina.
Ricordiamo il codice del link che è stato inserito nella pagina:
<a href="main.aspx">Recharger l'application</a>
Si tratta di un link relativo poiché non inizia con il segno /, che lo renderebbe un link assoluto. Relativo a cosa? Per comprendere questo punto, occorre tornare all’URL del documento attualmente visualizzato: [http://localhost/session2/(gu5ee455pkpffn554e3b1a32)/main.aspx]. I link relativi presenti in questo documento saranno relativi al percorso [http://localhost/session2/(gu5ee455pkpffn554e3b1a32)]. Pertanto, il nostro link sopra riportato è equivalente al link:
<a href=" http://localhost/session2/(gu5ee455pkpffn554e3b1a32)/main.aspx">Recharger l'application</a>
È ciò che ci mostra il browser se si passa il mouse sul link:

Se clicchiamo sul link [Recharger l'application], viene quindi richiamato l’URL
[http://localhost/session2/(gu5ee455pkpffn554e3b1a32)/main.aspx]. Il server riceverà quindi il token di sessione e potrà recuperare le informazioni ad esso associate. Ecco cosa ci mostra la risposta del server:

Ricordiamo che se abbiamo bisogno di tracciare una sessione in un’applicazione web e non siamo sicuri che i browser dei clienti di tale applicazione consentano l’uso dei cookie, allora
- dobbiamo configurare l’applicazione in modo che funzioni senza cookie
- le pagine dell’applicazione devono contenere link relativi e non assoluti
4.2. Recuperare le informazioni da una richiesta del client
4.2.1. Il ciclo richiesta-risposta del modello client-server web
Ricordiamo qui il contesto client-server di un’applicazione web:

La richiesta di un client per un'applicazione web viene elaborata nel modo seguente:
- il client apre una connessione TCP/IP verso una porta P del servizio web del computer M che ospita l’applicazione web
- invia su questa connessione una sequenza di righe di testo secondo il protocollo HTTP. Questo insieme di righe costituisce quella che viene definita la richiesta del client. Essa ha la seguente forma:

Una volta inviata la richiesta, il client attenderà la risposta.
- La prima riga delle intestazioni HTTP specifica l’azione richiesta al server web. Può assumere diverse forme:
- GET url HTTP/<versione>, dove <versione> è attualmente pari a 1.0 o 1.1. In questo caso, la richiesta non include la parte [Document]
- POST url HTTP/<versione>. In questo caso, la richiesta include una parte [Document], che nella maggior parte dei casi è un elenco di informazioni destinate all’applicazione web
- PUT url HTTP/<versione>. Il client invia un documento nella parte [Document] e desidera memorizzarlo sul server all'indirizzo url
Quando il client desidera trasmettere informazioni all’applicazione web a cui si è connesso, dispone principalmente di due modalità:
- (continua)
- la sua richiesta è [GET url_enrichie HTTP/<version>], dove url_enrichie ha la forma [url?param1=val1¶m2=val2&...]. Il client trasmette, oltre all’URL, una serie di informazioni nella forma [clé=valeur].
- la sua richiesta è [POST url HTTP/<version>]. Nella parte [Document], trasmette informazioni nella stessa forma di prima: [param1=val1¶m2=val2&...].
- Sul server, l’intera catena di elaborazione della richiesta del client ha accesso a quest’ultima tramite un oggetto globale denominato Request. Il server web ha inserito in questo oggetto l’intera richiesta del client in un formato che vedremo tra poco. L’applicazione interpellata elaborerà questo oggetto e costruirà una risposta per il client. Quest’ultima è disponibile in un oggetto globale denominato Response. Il ruolo dell’applicazione web è quello di costruire un oggetto [Response] a partire dall’oggetto [Request] ricevuto. La catena di elaborazione dispone inoltre degli oggetti globali [Application] e [Session] di cui abbiamo già parlato e che le consentiranno di condividere dati tra clienti diversi (Applicazione) o tra richieste successive dello stesso cliente (Sessione).
- L’applicazione invierà la propria risposta al server tramite l’oggetto [Response]. Una volta in rete, tale risposta assumerà la seguente forma: HTTP:

Una volta inviata questa risposta, il server chiuderà la connessione di rete in ricezione (a meno che il cliente non gli abbia indicato di non farlo).
- Il client riceverà la risposta e a sua volta chiuderà la connessione (in fase di trasmissione). Il trattamento di tale risposta dipenderà dal tipo di client. Se si tratta di un browser e il documento ricevuto è un documento HTML, questo verrà visualizzato. Se il client è un programma, la risposta verrà analizzata ed elaborata.
- Il fatto che, al termine del ciclo richiesta-risposta, la connessione che collegava il client al server venga chiusa rende il protocollo HTTP un protocollo senza stato. Alla richiesta successiva, il client stabilirà una nuova connessione di rete allo stesso server. Poiché non si tratta più della stessa connessione di rete, il server non ha alcuna possibilità (a livello TCP/IP e HTTP) di collegare questa nuova connessione a una precedente. Sarà il sistema del token di sessione a consentire tale collegamento.
4.2.2. Recuperare le informazioni trasmesse dal client
Esaminiamo ora alcune proprietà e metodi dell’oggetto [Request] che consentono al codice dell’applicazione di accedere alla richiesta del client e quindi alle informazioni da esso trasmesse. L’oggetto [Request] è di tipo [HttpRequest]:

Questa classe presenta numerose proprietà e metodi. Ci interessano le proprietà HttpMethod, QueryString, Form e Params, che ci consentiranno di accedere agli elementi della stringa di informazioni [param1=val1¶m2=val2&...].
metodi di richiesta del client: GET, POST, HEAD, ... | |
raccolta degli elementi della stringa di richiesta param1=val1¶m2=val2&.. della prima riga HTTP [méthode]?param1=val1¶m2=val2&... dove [méthode] può essere GET, POST, HEAD. | |
raccolta degli elementi della stringa di query param1=val1¶m2=val2&... presenti nella parte [Document] della query (metodo POST). | |
raggruppa diverse raccolte: QueryString, Form, ServerVariables, Cookies in un'unica raccolta. |
4.2.3. Esempio 1
Mettiamo in pratica questi elementi con un primo esempio. L’applicazione avrà un solo elemento [main.aspx]. Il codice di presentazione [main.aspx] sarà il seguente:
<%@ Page src="main.aspx.vb" Language="vb" AutoEventWireup="false" Inherits="main" %>
<html>
<head>
<title>Requête client</title>
</head>
<body>
Requête :
<% = méthode %>
<br />
nom :
<% = nom %>
<br />
âge :
<% = age %>
<br />
</body>
</html>
La pagina mostra tre informazioni [méthode, nom, age] calcolate dalla sua parte di controllo [main.aspx.vb]:
Public Class main
Inherits System.Web.UI.Page
Protected nom As String = "xx"
Protected age As String = "yy"
Protected méthode As String
Private Sub Page_Init(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.Init
' si memorizza la richiesta del client in request.txt nella cartella dell'applicazione
Dim requestFileName As String = Me.MapPath(Me.TemplateSourceDirectory) + "\request.txt"
Me.Request.SaveAs(requestFileName, True)
End Sub
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
' si recuperano i parametri della richiesta
méthode = Request.HttpMethod.ToLower
If Not Request.QueryString("nom") Is Nothing Then nom = Request.QueryString("nom").ToString
If Not Request.QueryString("age") Is Nothing Then age = Request.QueryString("age").ToString
If Not Request.Form("nom") Is Nothing Then nom = Request.Form("nom").ToString
If Not Request.Form("age") Is Nothing Then age = Request.Form("age").ToString
End Sub
End Class
Quando la pagina viene caricata (Form_Load), le informazioni [nom, age] vengono recuperate dalla richiesta del cliente. Vengono cercate nelle due collezioni [QueryString] e [Form]. . Inoltre, in [Page_Init], memorizziamo la richiesta del client per poter verificare ciò che ha inviato. Inseriamo questi due file in una cartella <application-path> e avviamo il server Cassini con i parametri (<application-path>,/request1), quindi con un browser richiediamo l'URL
[http://localhost/request1/main.aspx?nom=tintin&age=27]. Otteniamo la seguente risposta:

Le informazioni trasmesse dal client sono state recuperate correttamente. La richiesta del browser memorizzata nel file [request.txt] è la seguente:
GET /request1/main.aspx?nom=tintin&age=27 HTTP/1.1
Cache-Control: max-age=0
Connection: keep-alive
Keep-Alive: 300
Accept: application/x-shockwave-flash,text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,image/jpeg,image/gif;q=0.2,*/*;q=0.1
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Accept-Encoding: gzip,deflate
Accept-Language: en-us,en;q=0.5
Host: localhost
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.7b) Gecko/20040316
Si nota che il browser ha effettuato una richiesta GET. Per effettuare una richiesta POST, utilizzeremo il client [curl]. In una finestra DOS, digitiamo il seguente comando:
per visualizzare le intestazioni HTTP della risposta | |
per inviare l'informazione param=valore tramite un POST |
La risposta del server è la seguente:
HTTP/1.1 200 OK
Server: Microsoft ASP.NET Web Matrix Server/0.6.0.0
Date: Fri, 02 Apr 2004 09:27:25 GMT
Cache-Control: private
Content-Type: text/html; charset=utf-8
Content-Length: 178
Connection: Close
<html>
<head>
<title>Requête client</title>
</head>
<body>
Requête :
post
<br />
nom :
tintin
<br />
âge :
27
<br />
</body>
</html>
Il server ha correttamente recuperato, anche in questo caso, i parametri inviati questa volta da un POST. Per accertarsi di quest'ultimo punto, è possibile verificare il contenuto del file [request.txt]:
POST /request1/main.aspx HTTP/1.1
Pragma: no-cache
Content-Length: 17
Content-Type: application/x-www-form-urlencoded
Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, */*
Host: localhost
User-Agent: curl/7.10.8 (win32) libcurl/7.10.8 OpenSSL/0.9.7a zlib/1.1.4
nom=tintin&age=27
Il client [curl] ha effettivamente eseguito un POST. Ora, uniamo i due metodi di trasmissione delle informazioni. Inseriamo [age] nell'URL richiesto e [nom] nel documento inviato:
La richiesta inviata da [curl] è la seguente (request.txt):
POST /request1/main.aspx?age=27 HTTP/1.1
Pragma: no-cache
Content-Length: 10
Content-Type: application/x-www-form-urlencoded
Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, */*
Host: localhost
User-Agent: curl/7.10.8 (win32) libcurl/7.10.8 OpenSSL/0.9.7a zlib/1.1.4
nom=tintin
Si nota che l'età è stata inserita nell'URL richiesto. La si otterrà nella raccolta [QueryString]. Il nome, invece, è stato inserito nel documento inviato a tale URL. Lo si otterrà nella raccolta [Form]. La risposta ricevuta dal client [curl]:
<html>
<head>
<title>Requête client</title>
</head>
<body>
Requête :
post
<br />
nom :
tintin
<br />
âge :
27
<br />
</body>
</html>
Infine, non inviamo alcuna informazione al server:
E:\curl>curl --include http://localhost/request1/main.aspx
HTTP/1.1 200 OK
Server: Microsoft ASP.NET Web Matrix Server/0.6.0.0
Date: Fri, 02 Apr 2004 12:43:14 GMT
X-AspNet-Version: 1.1.4322
Cache-Control: private
Content-Type: text/html; charset=utf-8
Content-Length: 173
Connection: Close
<html>
<head>
<title>Requête client</title>
</head>
<body>
Requête :
get
<br />
nom :
xx
<br />
âge :
yy
<br />
</body>
</html>
Si invita il lettore a rileggere il codice del controller [main.aspx.vb] per comprendere questa risposta.
4.2.4. Esempio 2
Il cliente può inviare più valori per una stessa chiave. Cosa succede quindi se nell’esempio precedente si richiede l’URL [http://localhost/request1/main.aspx?nom=tintin&age=27&nom=milou], in cui la chiave [nom] compare due volte? Proviamo con un browser:

La nostra applicazione ha recuperato correttamente entrambi i valori associati alla chiave [nom]. La visualizzazione è un po’ fuorviante. È stata ottenuta tramite l’istruzione
If Not Request.QueryString("nom") Is Nothing Then nom = Request.QueryString("nom").ToString
Il metodo [ToString] ha generato la stringa [tintin,milou] che è stata visualizzata. Essa nasconde il fatto che, in realtà, l’oggetto [Request.QueryString("nom")] è un array di stringhe {"tintin","milou"}. L'esempio seguente illustra questo punto. La pagina di presentazione [main.aspx] sarà la seguente:
<%@ Page src="main.aspx.vb" Language="vb" AutoEventWireup="false" Inherits="main" %>
<HTML>
<HEAD>
<title>Requête client</title>
</HEAD>
<body>
<P>Informations passées par le client :</P>
<form runat="server">
<P>QueryString :</P>
<P><asp:listbox id="lstQueryString" runat="server" EnableViewState="False" Rows="6"></asp:listbox></P>
<P>Form :</P>
<P><asp:listbox id="lstForm" runat="server" EnableViewState="False" Rows="2"></asp:listbox></P>
</form>
</body>
</HTML>
In questa pagina sono presenti alcune novità che utilizzano i cosiddetti controlli server. Sono contrassegnati dall'attributo [runat="server"]. È troppo presto per introdurre il concetto di controllo server. È sufficiente sapere che in questo caso:
- la pagina presenta due elenchi (tag <asp:listbox>)
- che questi elenchi sono oggetti (lstQueryString, lstForm) di tipo [ListBox] che verranno creati dal controller della pagina
- che questi oggetti esistono solo all’interno del server web. Al momento della risposta, saranno trasformati in tag HTML standard che il client potrà interpretare. Un oggetto [listbox] verrà quindi trasformato (si dice anche «renderizzato») nei tag HTML <select> e <option>.
- Il vantaggio principale di questi oggetti è quello di liberare il codice di presentazione da qualsiasi codice VB, che rimane confinato al controller.
Il controller [main.aspx.vb] incaricato di costruire i due oggetti [lstQueryString] e [lstForm] è il seguente:
Imports System.Collections
Imports System
Imports System.Collections.Specialized
Public Class main
Inherits System.Web.UI.Page
Protected infosQueryString As ArrayList
Protected WithEvents lstQueryString As System.Web.UI.WebControls.ListBox
Protected WithEvents lstForm As System.Web.UI.WebControls.ListBox
Protected infosForm As ArrayList
Private Sub Page_Init(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.Init
' si memorizza la richiesta del client in request.txt nella cartella dell'applicazione
Dim requestFileName As String = Me.MapPath(Me.TemplateSourceDirectory) + "\request.txt"
Me.Request.SaveAs(requestFileName, True)
End Sub
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
' si recupera l'intera raccolta di informazioni da QueryString
infosQueryString = getValeurs(Request.QueryString)
lstQueryString.DataSource = infosQueryString
lstQueryString.DataBind()
infosForm = getValeurs(Request.Form)
lstForm.DataSource = infosForm
lstForm.DataBind()
End Sub
Private Function getValeurs(ByRef data As NameValueCollection) As ArrayList
' inizialmente un elenco di informazioni vuoto
Dim infos As New ArrayList
' si recuperano le chiavi della raccolta
Dim clés() As String = data.AllKeys
' si scorre l'array delle chiavi
Dim valeurs() As String
For Each clé As String In clés
' valori associati alla chiave
valeurs = data.GetValues(clé)
' un solo valore?
If valeurs.Length = 1 Then
infos.Add(clé + "=" + valeurs(0))
Else
' più valori
For ivalue As Integer = 0 To valeurs.Length - 1
infos.Add(clé + "(" + ivalue.ToString + ")=" + valeurs(ivalue))
Next
End If
Next
' si restituisce il risultato
Return infos
End Function
End Class
I punti salienti di questo codice sono i seguenti:
- in [Form_Load] la pagina recupera le due collezioni [QueryString] e [Form]. Utilizza una funzione [getValeurs] per inserire il contenuto di queste due collezioni in due oggetti di tipo [ArrayList] che conterranno stringhe di caratteri del tipo [clé=valeur] se la chiave della collezione è associata a un unico valore o [clé(i)=valeur] se la chiave è associata a più valori.
- Ciascuno degli oggetti [ArrayList] viene quindi associato a uno degli oggetti [ListBox] della pagina di presentazione tramite due istruzioni:
- [ListBox.DataSource=ArrayList] e [ListBox.DataBind]. Quest’ultima istruzione trasferisce gli elementi di [DataSource] nella collezione [Items] dell’oggetto [ListBox]
si noti che nessuno dei due oggetti [ListBox] viene creato esplicitamente da un'operazione [New]. Se ne deduce che, in presenza del tag <asp:listbox id="xx">...<asp:listbox/>, il server web crea autonomamente l’oggetto [ListBox] a cui fa riferimento l’attributo [id] del tag.
- La funzione [getValeurs] utilizza l’oggetto di tipo [NameValueCollection] che le viene passato come parametro per produrre un risultato di tipo [ArrayList].
Inseriamo i due file precedenti in una cartella <application-path> e avviamo il server Cassini con i parametri (<application-path>,/request2), quindi richiediamo l’URL
[http://localhost/request2/main.aspx?nom=tintin&age=27]. Otteniamo la seguente risposta:

Ora richiediamo un URL in cui la chiave [nom] è presente due volte:

Notiamo che l’oggetto [Request.QueryString("nome")) era effettivamente un array. In questo caso, le richieste sono state effettuate tramite un metodo GET. Utilizziamo il client [curl] per effettuare una richiesta POST:
E:\curl>curl --data nom=milou --data nom=tintin --data age=14 --data age=27 http://localhost/request2/main.aspx
<HTML>
<HEAD>
<title>Requête client</title>
</HEAD>
<body>
<P>Informations passées par le client :</P>
<form name="_ctl0" method="post" action="main.aspx" id="_ctl0">
<input type="hidden" name="__VIEWSTATE" value="dDwtMTI3MjA1MzUzMTs7PtCDC7NG4riDYIB4YjyGFpVAAviD" />
<P>QueryString :</P>
<P><select name="lstQueryString" size="6" id="lstQueryString">
</select></P>
<P>Form :</P>
<P><select name="lstForm" size="2" id="lstForm">
<option value="nom(0)=milou">nom(0)=milou</option>
<option value="nom(1)=tintin">nom(1)=tintin</option>
<option value="age(0)=14">age(0)=14</option>
<option value="age(1)=27">age(1)=27</option>
</select></P>
</form>
</body>
</HTML>
Si può notare che il client riceve correttamente il codice HTML standard per entrambi gli elenchi della pagina. Compaiono informazioni che non abbiamo inserito noi stessi, come il campo nascosto [_VIEWSTATE]. Queste informazioni sono state generate dai tag <asp:xx runat="server">. Dovremo imparare a gestirli.
4.3. Implementazione di un’architettura MVC
4.3.1. Il concetto
Concludiamo questo lungo capitolo con l’implementazione di un’applicazione costruita secondo il modello MVC (Model-View-Controller). Un’applicazione web strutturata secondo questo modello si presenta così:

- il client invia le sue richieste a un'entità specifica dell'applicazione chiamata controller
- il controller analizza la richiesta del client e ne provvede all’esecuzione. A tal fine si avvale di classi che raggruppano la logica di business dell’applicazione e delle classi di accesso ai dati.
- a seconda del risultato dell’esecuzione della richiesta, il controller decide di inviare una determinata pagina come risposta al client
Nel nostro modello, tutte le richieste passano attraverso un unico controller che funge da direttore d’orchestra dell’intera applicazione web. Il vantaggio di questo modello è che nel controller è possibile raggruppare tutto ciò che deve essere eseguito prima di ogni richiesta. Supponiamo, ad esempio, che l’applicazione richieda un’autenticazione. Questa viene effettuata una sola volta. Una volta completata con successo, l’applicazione inserirà nella sessione le informazioni relative all’utente che si è appena autenticato. Poiché un client può richiamare direttamente una pagina dell’applicazione senza autenticarsi, ogni pagina dovrà quindi verificare nella sessione che l’autenticazione sia stata effettivamente completata. Se tutte le richieste passano attraverso un unico controller, sarà quest’ultimo a svolgere tale compito. Le pagine a cui la richiesta verrà eventualmente inoltrata non dovranno occuparsene.
4.3.2. Gestire un'applicazione MVC senza sessione
Da quanto visto finora, si potrebbe pensare che il file [global.asax] possa svolgere il ruolo di controller. Infatti, sappiamo che tutte le richieste passano attraverso di esso. È quindi nella posizione ideale per controllare tutto. L’applicazione che segue lo utilizza a questo scopo. Il suo percorso virtuale sarà [http://localhost/mvc1/main.aspx]. Per indicare ciò che desidera, il client aggiungerà all’URL un parametro action=valore. A seconda del valore del parametro [action], il controller [global.asax] indirizzerà la richiesta verso una pagina specifica:
- [main.aspx] se il parametro action non è definito o se action=main
- [action1.aspx] se action=action1
- [inconnu.aspx] se `action` non rientra nei casi 1 e 2
Le pagine [main.aspx, action1.aspx, inconnu.aspx] si limitano a visualizzare il valore di [action] che ne ha determinato la visualizzazione. Di seguito elenchiamo gli otto file di questa applicazione e li commentiamo quando necessario:
[global.asax]
[global.asax.vb]
Imports System
Imports System.Web
Imports System.Web.SessionState
Public Class Global
Inherits System.Web.HttpApplication
Sub Application_BeginRequest(ByVal sender As Object, ByVal e As EventArgs)
' si recupera l'azione da eseguire
Dim action As String
If Request.QueryString("action") Is Nothing Then
action = "main"
Else
action = Request.QueryString("action").ToString.ToLower
End If
' si inserisce l'azione nel contesto della richiesta
Context.Items("action") = action
' si esegue l'azione
Select Case action
Case "main"
Server.Transfer("main.aspx", True)
Case "action1"
Server.Transfer("action1.aspx", True)
Case Else
Server.Transfer("inconnu.aspx", True)
End Select
End Sub
End Class
Punti da tenere presenti:
- intercettiamo tutte le richieste del client nella procedura [Application_BeginRequest], che viene eseguita automaticamente all’avvio di ogni nuova richiesta inviata all’applicazione.
- In questa procedura, abbiamo accesso all’oggetto [Request], che rappresenta la richiesta HTTP del client. Poiché ci aspettiamo un URL della forma [http://localhost/mvc1/main.aspx?action=xx], cerchiamo una chiave [action] nella collezione [Request.QueryString]. Se non è presente, impostiamo per impostazione predefinita l’azione pari a «main».
- Il valore del parametro [action] viene inserito nell’oggetto [Context]. Come gli oggetti [Application, Session, Request, Response, Server], questo oggetto è globale e accessibile da qualsiasi codice. Questo oggetto viene passato da una pagina all’altra se la richiesta viene elaborata da più pagine, come avverrà in questo caso. Viene eliminato non appena la risposta è stata inviata al cliente. La sua durata è quindi pari a quella dell’elaborazione della richiesta.
- A seconda del valore del parametro [action], la richiesta viene inoltrata alla pagina appropriata. A tal fine si utilizza l’oggetto globale [Server] che, grazie al suo metodo, consente di trasferire la richiesta corrente a un’altra pagina. Il suo primo parametro è il nome della pagina di destinazione, il secondo è un valore booleano che indica se si devono trasferire o meno alla pagina di destinazione le collezioni [QueryString] e [Form]. In questo caso, la risposta è sì.
I file [main.aspx] e [main.aspx.vb]:
<%@ Page src="main.aspx.vb" Language="vb" AutoEventWireup="false" Inherits="main" %>
<HTML>
<head>
<title>main</title></head>
<body>
<h3>Page [main]</h3>
Action : <% =action %>
</body>
</HTML>
Public Class main
Inherits System.Web.UI.Page
Protected action As String
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
' si recupera l'azione in corso
action = Me.Context.Items("action").ToString
End Sub
End Class
Il controller [main.aspx.vb] si limita a recuperare il valore della chiave [action] dal contesto; tale valore viene visualizzato dal codice di presentazione. In questo caso si intende illustrare il passaggio dell’oggetto [Context] tra diverse pagine che gestiscono la stessa richiesta del cliente. Le pagine [action1.aspx] e [inconnu.aspx] funzionano in modo analogo:
[action1.aspx]
<%@ Page src="action1.aspx.vb" Language="vb" AutoEventWireup="false" Inherits="action1" %>
<HTML>
<head>
<title>action1</title></head>
<body>
<h3>Page [action1]</h3>
Action : <% =action %>
</body>
</HTML>
[action1.aspx.vb]
Public Class action1
Inherits System.Web.UI.Page
Protected action As String
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
' si recupera l'azione in corso
action = Me.Context.Items("action").ToString
End Sub
End Class
[inconnu.aspx]
<%@ Page src="inconnu.aspx.vb" Language="vb" AutoEventWireup="false" Inherits="inconnu" %>
<HTML>
<head>
<title>inconnu</title></head>
<body>
<h3>Page [inconnu]</h3>
Action : <% =action %>
</body>
</HTML>
[inconnu.aspx.vb]
Public Class inconnu
Inherits System.Web.UI.Page
Protected action As String
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
' si recupera l'azione in corso
action = Me.Context.Items("action").ToString
End Sub
End Class
Per effettuare un test, i documenti precedenti vengono inseriti in una cartella <application-path> e Cassini viene avviato con i parametri (<application-path>,/mvc1). Richiediamo l'URL [http://localhost/mvc1/main.aspx]:

La richiesta non ha inviato alcun parametro [action]. Il codice del controller dell'applicazione [global.asax.vb] ha generato la pagina [main.aspx]. Ora richiediamo l'URL [http://localhost/mvc1/main.aspx?action=action1]:

Il codice del controller dell'applicazione [global.asax.vb] ha generato la pagina [action1.aspx]. Ora richiediamo l'URL [http://localhost/mvc1/main.aspx?action=xx]:

L'azione non è stata riconosciuta e il controller [global.asax.vb] ha restituito la pagina [inconnu.aspx].
4.3.3. Gestire un'applicazione MVC con sessione
Nella maggior parte dei casi, le diverse richieste di un client per un'applicazione devono condividere delle informazioni. Abbiamo visto una possibile soluzione a questo problema: memorizzare le informazioni da condividere nell'oggetto [Session] della richiesta. Questo oggetto è infatti condiviso da tutte le richieste ed è in grado di memorizzare informazioni nella forma (chiave, valore), dove la chiave è di tipo [String] e il valore è di qualsiasi tipo derivato da [Object].
Nell’esempio precedente, le diverse pagine associate alle diverse azioni venivano richiamate nella procedura [Application_BeginRequest] del file [global.asax.vb]:
Sub Application_BeginRequest(ByVal sender As Object, ByVal e As EventArgs)
' si recupera l'azione da eseguire
Dim action As String
If Request.QueryString("action") Is Nothing Then
action = "main"
Else
action = Request.QueryString("action").ToString.ToLower
End If
' si inserisce l'azione nel contesto della richiesta
Context.Items("action") = action
' si esegue l'azione
Select Case action
Case "main"
Server.Transfer("main.aspx", True)
Case "action1"
Server.Transfer("action1.aspx", True)
Case Else
Server.Transfer("inconnu.aspx", True)
End Select
End Sub
Si è riscontrato che nella procedura [Application_BeginRequest] l'oggetto [Session] non è accessibile. Lo stesso vale per la pagina a cui viene trasferita l'esecuzione. Pertanto, questo modello non è utilizzabile per un'applicazione con sessione. È possibile assegnare il ruolo di controller a qualsiasi pagina, ad esempio [default.aspx]. I file [global.asax, global.asax.vb] vengono quindi eliminati e sostituiti dai file [default.aspx, default.aspx.vb]:
[default.aspx]
[default.aspx.vb]
Imports System
Imports System.Web
Imports System.Web.SessionState
Public Class controleur
Inherits System.Web.UI.Page
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
' si recupera l'azione da eseguire
Dim action As String
If Request.QueryString("action") Is Nothing Then
action = "main"
Else
action = Request.QueryString("action").ToString.ToLower
End If
' si inserisce l'azione nel contesto della richiesta
Context.Items("action") = action
' si recupera l'azione precedente, se esiste
Context.Items("actionPrec") = Session.Item("actionPrec")
If Context.Items("actionPrec") Is Nothing Then Context.Items("actionPrec") = ""
' si memorizza l'azione corrente nella sessione
Session.Item("actionPrec") = action
' si esegue l'azione
Select Case action
Case "main"
Server.Transfer("main.aspx", True)
Case "action1"
Server.Transfer("action1.aspx", True)
Case Else
Server.Transfer("inconnu.aspx", True)
End Select
End Sub
End Class
Per evidenziare il meccanismo di sessione, le diverse pagine visualizzeranno, oltre all'azione corrente, anche l'azione precedente. Per una sequenza di azioni A1, A2, ..., An, quando si verifica l'azione Ai, il controller sopra indicato:
- inserisce l’azione corrente Ai nel contesto
- recupera dalla sessione l’azione Ai-1 che l’ha preceduta. Nel caso in cui non ce ne sia una (come nel caso dell’azione A1), la stringa vuota indica l’azione precedente.
- inserisce l'azione corrente Ai nella sessione in sostituzione di Ai-1
- trasferisce l'esecuzione alla pagina appropriata
Le tre pagine dell’applicazione sono le seguenti:
[main.aspx]
<%@ Page src="main.aspx.vb" Language="vb" AutoEventWireup="false" Inherits="main" %>
<HTML>
<HEAD>
<title>main</title>
</HEAD>
<body>
<h3>Page [main]</h3>
Action courante :
<% =action %>
<br>
Action précédente :
<% =actionPrec %>
</body>
</HTML>
[action1.aspx]
<%@ Page src="main.aspx.vb" Language="vb" AutoEventWireup="false" Inherits="main" %>
<HTML>
<head>
<title>action1</title></head>
<body>
<h3>Page [action1]</h3>
Action courante :
<% =action %>
<br>
Action précédente :
<% =actionPrec %>
</body>
</HTML>
[inconnu.aspx]
<%@ Page src="main.aspx.vb" Language="vb" AutoEventWireup="false" Inherits="main" %>
<HTML>
<head>
<title>inconnu</title>
</head>
<body>
<h3>Page [inconnu]</h3>
Action courante :
<% =action %>
<br>
Action précédente :
<% =actionPrec %>
</body>
</HTML>
Poiché le tre pagine visualizzano le stesse informazioni [action, actionPrec], possono avere tutte e tre lo stesso controller di pagina. Le abbiamo quindi fatte derivare tutte dalla classe [main] del file [main.aspx.vb]:
Public Class main
Inherits System.Web.UI.Page
Protected action As String
Protected actionPrec As String
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
' si recupera l'azione in corso
action = Me.Context.Items("action").ToString
' e l'azione precedente
actionPrec = Me.Context.Items("actionPrec").ToString
End Sub
End Class
Il codice sopra riportato si limita a recuperare le informazioni inserite nel contesto dal controller dell'applicazione [default.aspx.vb].
Tutti questi file sono collocati in <application-path> e Cassini viene avviato con i parametri (<application-path>,/mvc2). Innanzitutto si richiede l'URL [http://localhost/mvc2]:

L’URL [http://localhost/mvc2] indica una cartella. Sappiamo che in questo caso, se esiste, il server restituisce il documento [default.aspx] presente in tale cartella. In questo caso non era stata specificata alcuna azione. È stata quindi eseguita l’azione [main]. Passiamo all’azione [action1]:

L’azione corrente e quella precedente sono state identificate correttamente. Passiamo all’azione [xx]:

4.4. Conclusion
Ora disponiamo degli elementi di base su cui si fonda qualsiasi applicazione ASP.ET. Resta tuttavia da introdurre un concetto importante: quello di modulo. Questo sarà l’argomento del capitolo seguente.