4. Os fundamentos do desenvolvimento ASP.NET
4.1. O conceito de aplicação web ASP.NET
4.1.1. Introdução
Uma aplicação web é uma aplicação que reúne vários documentos (HTML, código .NET, imagens, sons, etc.). Estes documentos devem estar numa mesma raiz, a que se chama raiz da aplicação web. A esta raiz está associado um caminho virtual do servidor web. Já abordámos o conceito de pasta virtual no servidor web Cassini. Este conceito também existe no servidor web IIS. Uma diferença importante entre os dois servidores é que, num determinado momento, o IIS pode ter um número qualquer de pastas virtuais, enquanto o servidor web Cassini tem apenas uma, aquela que foi especificada no seu arranque. Isto significa que o servidor IIS pode servir várias aplicações web simultaneamente, enquanto o servidor Cassini serve apenas uma de cada vez. Nos exemplos anteriores, o servidor Cassini era sempre iniciado com os parâmetros (<webroot>,/aspnet) que associavam a pasta virtual /aspnet à pasta física <webroot>. O servidor web servia, portanto, sempre a mesma aplicação web. Isso não nos impediu de escrever e testar páginas diferentes e independentes dentro dessa única aplicação web. Cada aplicação web tem os seus próprios recursos, que se encontram na sua raiz física <webroot>:
- uma pasta [bin], na qual é possível colocar classes pré-compiladas
- um ficheiro [global.asax] que permite inicializar a aplicação web na sua totalidade, bem como o ambiente de execução de cada um dos seus utilizadores
- um ficheiro [web.config] que permite configurar o funcionamento da aplicação
- um ficheiro [default.aspx] que funciona como porta de entrada da aplicação
- ...
Assim que uma aplicação utiliza um destes três recursos, necessita de um caminho físico e virtual que lhe seja próprio. De facto, não há qualquer razão para que duas aplicações web diferentes sejam configuradas da mesma forma. Todos os nossos exemplos anteriores puderam ser colocados na mesma aplicação (<webroot>,/aspnet) porque não utilizavam nenhum dos recursos acima referidos.
Voltemos à arquitetura MVC recomendada no início deste capítulo para o desenvolvimento de uma aplicação web:

A aplicação web é composta por ficheiros de classe (controlador, classes de negócio, classes de acesso aos dados) e por ficheiros de apresentação (documentos HTML, imagens, sons, folhas de estilo, etc.). O conjunto destes ficheiros será colocado numa mesma raiz a que, por vezes, nos referiremos como <application-path>. Esta raiz será associada a um caminho virtual <application-vpath>. A associação entre este caminho virtual e o caminho físico é feita através da configuração do servidor web. Vimos que, no caso do servidor Cassini, esta associação é feita no momento do arranque do servidor. Por exemplo, numa janela do DOS, o Cassini seria iniciado com o comando:
Na pasta <application-path>, encontraremos, consoante as nossas necessidades:
- a pasta [bin], para colocar classes pré-compiladas (DLL)
- o ficheiro [global.asax], quando for necessário efetuar inicializações, quer durante a inicialização da aplicação, quer durante a inicialização de uma sessão de utilizador
- o ficheiro [web.config], quando for necessário configurar a aplicação
- o ficheiro [default.aspx] quando precisarmos de uma página predefinida na aplicação
Para respeitar este conceito de aplicação web, os exemplos que se seguem serão todos colocados numa pasta <application-path> específica da aplicação, à qual será associada uma pasta virtual <application-vpath>, sendo o servidor Cassini iniciado de forma a ligar estes dois parâmetros.
4.1.2. Configurar uma aplicação web
Se <application-path> for a raiz de uma aplicação ASP.NET, pode utilizar-se o ficheiro <application-path>\web.config para a configurar. Este ficheiro está no formato XML. Eis um exemplo:
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
<appSettings>
<add key="nom" value="tintin"/>
<add key="age" value="27"/>
</appSettings>
</configuration>
É importante ter em conta que as balizas XML distinguem maiúsculas de minúsculas. Todas as informações de configuração devem estar entre as balizas <configuration> e </configuration>. Existem várias secções de configuração que podem ser utilizadas. Apresentamos aqui apenas uma delas, a secção <appSettings>, que permite inicializar dados com a baliza <add>. A sintaxe desta baliza é a seguinte:
Quando o servidor Web inicia uma aplicação, verifica se em <application-path> existe um ficheiro chamado web.config. Se sim, lê-o e armazena as suas informações num objeto do tipo [ConfigurationSettings], que estará disponível em todas as páginas da aplicação enquanto esta estiver ativa. A classe [ConfigurationSettings] possui um método estático [AppSettings]:

Para obter o valor de uma chave C do ficheiro de configuração, escreve-se ConfigurationSettings.AppSettings("C"). Obtém-se uma cadeia de caracteres. Para utilizar o ficheiro de configuração anterior, criemos uma página [default.aspx]. O código VB do ficheiro [default.aspx.vb] será o seguinte:
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
'recuperam-se as informações de configuração
nom = ConfigurationSettings.AppSettings("nom")
age = ConfigurationSettings.AppSettings("age")
End Sub
End Class
Vê-se que, ao carregar a página, os valores dos parâmetros de configuração [nom] e [age] são recuperados. Estes serão apresentados pelo código de apresentação de [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>
Para o teste, colocamos os ficheiros [web.config], [default.aspx] e [default.aspx.vb] na mesma pasta:
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
Seja <application-path> a pasta onde se encontram os três ficheiros da aplicação. O servidor Cassini é iniciado com os parâmetros (<application-path>,/aspnet/config1). Solicitamos os ficheiros URL e [http://localhost/aspnet/config1]. Como [config1] é uma pasta, o servidor web irá procurar um ficheiro [default.aspx] dentro dela e exibi-lo se o encontrar. Neste caso, irá encontrá-lo:

4.1.3. Aplicação, Sessão, Contexto
4.1.3.1. O ficheiro global.asax
O código do ficheiro [global.asax] é sempre executado antes de a página solicitada pela requisição atual ser carregada. Deve estar localizado na raiz <application-path> da aplicação. Se existir, o ficheiro [global.asax] é utilizado em vários momentos pelo servidor web:
- quando a aplicação web é iniciada ou encerrada
- quando uma sessão de utilizador é iniciada ou encerrada
- quando uma solicitação do utilizador é iniciada
Tal como acontece com as páginas .aspx, o ficheiro [global.asax] pode ser escrito de diferentes formas e, em particular, separando o código VB numa classe controladora e no código de apresentação. Esta é a escolha predefinida pela ferramenta Visual Studio e faremos aqui o mesmo. Normalmente, não há qualquer apresentação a fazer, uma vez que essa função cabe às páginas .aspx. O conteúdo do ficheiro [global.asax] fica, assim, reduzido a uma diretiva que faz referência ao ficheiro que contém o código do controlador:
<%@ Application src="Global.asax.vb" Inherits="Global" %>
Note-se que a diretiva já não é [Page], mas sim [Application]. O código do controlador [global.asax.vb] associado e gerado pela ferramenta Visual Studio é o seguinte:
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)
' É acionado quando a aplicação é iniciada
End Sub
Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
' É acionado quando a sessão é iniciada
End Sub
Sub Application_BeginRequest(ByVal sender As Object, ByVal e As EventArgs)
' É acionado no início de cada pedido
End Sub
Sub Application_AuthenticateRequest(ByVal sender As Object, ByVal e As EventArgs)
' É acionado durante uma tentativa de autenticação do utilizador
End Sub
Sub Application_Error(ByVal sender As Object, ByVal e As EventArgs)
' É acionado quando ocorre um erro
End Sub
Sub Session_End(ByVal sender As Object, ByVal e As EventArgs)
' É acionado quando a sessão termina
End Sub
Sub Application_End(ByVal sender As Object, ByVal e As EventArgs)
' É acionado quando a aplicação é encerrada
End Sub
End Class
Note-se que a classe do controlador deriva da classe [HttpApplication]. Ao longo do ciclo de vida de uma aplicação, ocorrem vários eventos importantes. Estes são geridos por procedimentos cuja estrutura básica é apresentada acima.
- [Application_Start]: recorde-se que uma aplicação web está «encerrada» num caminho virtual. A aplicação inicia-se assim que uma página localizada nesse caminho virtual é solicitada por um cliente. O procedimento [Application_Start] é então executado. Esta será a única vez. Neste procedimento, realizar-se-á toda a inicialização necessária para a aplicação, como, por exemplo, a criação de objetos cuja duração corresponde à da aplicação.
- [Application-End]: é executado quando a aplicação é encerrada. A cada aplicação está associado um período de inatividade, configurável em [web.config], ao fim do qual a aplicação é considerada encerrada. É, portanto, o servidor web que toma esta decisão com base na configuração da aplicação. O período de inatividade de uma aplicação é definido como o tempo durante o qual nenhum cliente efetuou um pedido de um recurso da aplicação.
- [Session-Start]/[Session_End]: A cada cliente está associada uma sessão, a menos que a aplicação esteja configurada para não utilizar sessões. Um cliente não é um utilizador em frente ao seu ecrã. Se este tiver aberto dois navegadores para aceder à aplicação, representa dois clientes. Um cliente é identificado por um token de sessão que deve anexar a cada uma das suas solicitações. Este token de sessão é uma sequência de caracteres gerada aleatoriamente pelo servidor web e é única. Dois clientes não podem ter o mesmo token de sessão. Este token acompanhará o cliente da seguinte forma:
- o cliente que efetua a sua primeira solicitação não envia um token de sessão. O servidor web reconhece este facto e atribui-lhe um. Este é o início da sessão e o procedimento [Session_Start] é executado. Esta será a única vez.
- o cliente efetua as suas solicitações seguintes enviando o token que o identifica. Isto permitirá ao servidor web recuperar informações associadas a esse token. Isto permitirá o acompanhamento entre as diferentes solicitações do cliente.
- A aplicação pode disponibilizar ao cliente um formulário para encerrar a sessão. Neste caso, é o próprio cliente que solicita o encerramento da sua sessão. O procedimento [Session_End] será executado. Esta será a única vez.
- O cliente pode nunca solicitar ele próprio o encerramento da sua sessão. Neste caso, após um determinado período de inatividade da sessão, também configurável através do procedimento [web.config], a sessão será encerrada pelo servidor web. Será então executado o procedimento [Session_End].
- [Application_BeginRequest]: este procedimento é executado assim que chega um novo pedido. É, portanto, executado em cada pedido de qualquer cliente. É um bom momento para analisar o pedido antes de o encaminhar para a página que foi solicitada. É até possível decidir redirecioná-lo para outra página.
- [Application_Error]: é executado sempre que ocorre um erro não tratado explicitamente pelo código do controlador [global.asax.vb]. Aqui, é possível redirecionar o pedido do cliente para uma página que explique a causa do erro.
Se nenhum destes eventos tiver de ser tratado, então o ficheiro [global.asax] pode ser ignorado. Foi isso que se fez nos primeiros exemplos deste capítulo.
4.1.3.2. Exemplo 1
Vamos desenvolver uma aplicação para compreender melhor os três momentos que são: o arranque da aplicação, da sessão e de um pedido do cliente. O ficheiro [global.asax] será o seguinte:
O ficheiro [global.asax.vb] associado será o seguinte:
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)
' É acionado quando a aplicação é iniciada
' regista-se a hora
Dim startApplication As String = Date.Now.ToString("T")
' É guardada no contexto da aplicação
Application.Item("startApplication") = startApplication
End Sub
Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
' É acionado quando a sessão é iniciada
' regista-se a hora
Dim startSession As String = Date.Now.ToString("T")
' coloca-se na sessão
Session.Item("startSession") = startSession
End Sub
Sub Application_BeginRequest(ByVal sender As Object, ByVal e As EventArgs)
' regista-se a hora
Dim startRequest As String = Date.Now.ToString("T")
' Insere-se na sessão
Context.Items("startRequest") = startRequest
End Sub
End Class
Os pontos importantes do código são os seguintes:
- o servidor web disponibiliza à classe [HttpApplication], a partir de [global.asax.vb], um determinado número de objetos:
- Aplicação do tipo [HttpApplicationState] — representa a aplicação web — dá acesso a um dicionário de objetos [Application.Item] acessível a todos os clientes da aplicação — permite a partilha de informações entre diferentes clientes — o acesso simultâneo de vários clientes a um mesmo dado em modo de leitura/escrita requer a sincronização dos clientes.
- Sessão do tipo [HttpSessionState] — representa um cliente específico — dá acesso a um dicionário de objetos [Session.Item] acessível a todas as solicitações desse cliente — permitirá memorizar informações sobre um cliente que poderão ser recuperadas ao longo das suas solicitações.
- Pedido do tipo [HttpRequest] — representa a solicitação HTTP atual do cliente
- Resposta do tipo [HttpResponse] — representa a resposta HTTP que está a ser construída pelo servidor para o cliente
- Servidor do tipo [HttpServerUtility] — oferece métodos utilitários, nomeadamente para redirecionar o pedido para uma página diferente daquela inicialmente prevista.
- Contexto do tipo [HttpContext] — este objeto é recriado a cada nova solicitação, mas é partilhado por todas as páginas que participam no processamento da solicitação — permite transmitir informações de página para página durante o processamento de uma solicitação, graças ao seu dicionário «Items».
- O procedimento [Application_Start] regista o início da aplicação numa variável armazenada num dicionário acessível ao nível da aplicação
- o procedimento [Session_Start] regista o início da sessão numa variável armazenada num dicionário acessível ao nível da sessão
- o procedimento [Application_BeginRequest] regista o início da consulta numa variável armazenada num dicionário acessível ao nível da consulta (c.a.d disponível durante todo o tempo do seu processamento, mas perdida no final do mesmo)
A página de destino será a seguinte página [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>
Esta página de apresentação apresenta valores calculados pelo seu controlador [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
' recuperam-se as informações da aplicação e da sessão
jeton=Session.SessionId
startApplication = Application.Item("startApplication").ToString
startSession = Session.Item("startSession").ToString
startRequest = Context.Items("startRequest").ToString
End Sub
End Class
O controlador limita-se a recuperar as três informações colocadas, respetivamente, na aplicação, na sessão e no contexto por [global.asax.vb].
Testamos a aplicação da seguinte forma:
- os ficheiros são reunidos numa mesma pasta <application-path>

- o servidor Cassini é iniciado com os parâmetros (<application-path>,/aspnet/globalasax1)
- um primeiro cliente solicita o URL [http://localhost/aspnet/globalasax1/main.aspx] e obtém o seguinte resultado:

- o mesmo cliente faz uma nova solicitação (opção «Reload» do navegador):

Pode-se verificar que apenas a hora da solicitação mudou. Isto demonstra duas coisas:
- os procedimentos [Application_Start] e [Session_Start] de [global.asax] não foram executados na segunda solicitação.
- os objetos [Application] e [Session], onde estavam armazenadas as horas de início da aplicação e da sessão, continuam disponíveis para a segunda solicitação.
- abrimos um segundo navegador para criar um segundo cliente e solicitamos novamente a mesma URL:

Desta vez, verificamos que a hora da sessão mudou. O segundo navegador, apesar de estar na mesma máquina, foi considerado um segundo cliente e foi criada uma nova sessão para ele. É possível constatar que os dois clientes não têm o mesmo token de sessão. A hora de início da aplicação não mudou, o que significa que:
- o procedimento [Application_Start] de [global.asax.vb] não foi executado
- o objeto [Application], onde foi armazenada a hora de início da aplicação, está acessível ao segundo cliente. É, portanto, neste objeto que devem ser armazenadas as informações que os diferentes clientes da aplicação devem partilhar, servindo o objeto [Session] para armazenar informações que as consultas de um mesmo cliente devem partilhar.
4.1.3.3. Uma visão geral
Com o que aprendemos até agora, estamos em condições de elaborar um primeiro esquema explicativo do funcionamento de um servidor web e das aplicações web que este serve:

O esquema anterior mostra-nos um servidor a servir duas aplicações, designadas por A e B, cada uma com dois clientes. Um servidor web é capaz de servir várias aplicações web em simultâneo. Estas são totalmente independentes umas das outras. Vamos centrar-nos na aplicação A. O processamento de uma solicitação do cliente-1A à aplicação A decorrerá da seguinte forma:
- o cliente 1A solicita ao servidor web um recurso que pertence ao domínio da aplicação A. Isto significa que solicita um URL com o formato [http://machine:port/VA/ressource], em que VA é o caminho virtual da aplicação A.
- Se o servidor web detetar que se trata da primeira solicitação de um recurso da aplicação A, aciona o evento [Application_Start] do ficheiro [global.asax] da aplicação A. Será criado um objeto [ApplicationA] do tipo [HttpApplicationState]. Os diferentes códigos da aplicação irão armazenar neste objeto dados com o âmbito [Application], c.a.d, relativos a todos os utilizadores. O objeto [ApplicationA] permanecerá ativo até que o servidor web encerre a aplicação A.
- Se, além disso, o servidor web detetar que está a lidar com um novo cliente da aplicação A, irá desencadear o evento [Session_Start] do ficheiro [global.asax] da aplicação A. Será criado um objeto [Session-1A] do tipo [HttpSessionState]. Este objeto permitirá que a aplicação A armazene objetos de âmbito [Session] e c.a.d, pertencentes a um cliente específico. O objeto [Session-1A] existirá enquanto o cliente 1A efetuar pedidos. Permitirá o acompanhamento desse cliente. O servidor web deteta que está a lidar com um novo cliente em dois casos:
- o cliente não lhe enviou um token de sessão nos cabeçalhos HTTP da sua solicitação
- o cliente enviou-lhe um token de sessão que não existe (mau funcionamento do cliente ou tentativa de pirataria) ou que já não existe. Um token de sessão expira, de facto, após um determinado período de inatividade do cliente (20 minutos por predefinição com IIS). Este período é programável.
- Em todos os casos, o servidor web irá desencadear o evento [Application_BeginRequest] do ficheiro [global.asax]. Este evento inicia o processamento de uma solicitação do cliente. É comum não processar este evento e passar o controlo para a página solicitada pelo cliente, que, por sua vez, processará a solicitação. Também é possível utilizar este evento para analisar a solicitação, processá-la e decidir qual a página que deve ser enviada como resposta. Utilizaremos esta técnica para implementar uma aplicação que respeite a arquitetura MVC de que falámos.
- Depois de passar pelo filtro [global.asax], a solicitação do cliente é encaminhada para uma página .aspx que irá processá-la. Veremos mais adiante que é possível fazer a solicitação passar por um filtro composto por várias páginas. A última página ficará encarregada de enviar a resposta ao cliente. As páginas podem adicionar à solicitação inicial do cliente informações que tenham calculado. Podem armazenar essas informações na coleção Context.Items. Com efeito, todas as páginas envolvidas no processamento da solicitação de um cliente têm acesso a este repositório de dados.
- O código das diferentes páginas tem acesso aos reservatórios de dados que são os objetos [ApplicationA], [Session-1A], ... É importante lembrar que o servidor web processa simultaneamente vários clientes para a aplicação A. Todos estes clientes têm acesso ao objeto [Application A]. Caso precisem de alterar dados neste objeto, é necessário efetuar uma sincronização entre os clientes. Além disso, cada cliente XA tem acesso ao repositório de dados [Session-XA]. Uma vez que este lhe está reservado, não é necessária qualquer sincronização neste caso.
- O servidor web serve várias aplicações web em simultâneo. Não há qualquer interferência entre os clientes destas diferentes aplicações.
Destas explicações, retemos os seguintes pontos:
- Num determinado momento, um servidor web atende a vários clientes simultaneamente. Isto significa que não aguarda o fim de um pedido para processar outro. Num instante T, existem, portanto, vários pedidos em curso de processamento, pertencentes a clientes diferentes e para aplicações diferentes. Chama-se por vezes «threads de execução» aos códigos de processamento que decorrem em simultâneo no interior do servidor web.
- Os threads de execução dos clientes de diferentes aplicações web não interferem entre si. Existe isolamento.
- Os threads de execução dos clientes de uma mesma aplicação podem ter de partilhar dados:
- os threads de execução das solicitações de dois clientes diferentes (que não partilham o mesmo token de sessão) podem partilhar dados através do objeto [Application].
- Os threads de execução de pedidos sucessivos de um mesmo cliente podem partilhar dados através do objeto [Session].
- os threads de execução das páginas sucessivas que processam uma mesma solicitação de um determinado cliente podem partilhar dados através do objeto [Context].
4.1.3.4. Exemplo 2
Vamos desenvolver um novo exemplo que ilustre o que acabámos de ver. Reunimos na mesma pasta os seguintes ficheiros:
[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)
' É acionado quando a aplicação é iniciada
' inicializar o contador de clientes
Application.Item("nbRequêtes") = 0
End Sub
Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
' É acionado quando a sessão é iniciada
' inicializa o contador de pedidos
Session.Item("nbRequêtes") = 0
End Sub
End Class
O princípio da aplicação consiste em contabilizar o número total de pedidos efetuados à aplicação e o número de pedidos por cliente. Quando a aplicação é iniciada ([Application_Start]), o contador de solicitações feitas à aplicação é zerado. Este contador é colocado no âmbito [Application], uma vez que deve ser incrementado por todos os clientes. Quando um cliente acede pela primeira vez a [Session_Start], o contador de solicitações feitas por esse cliente é zerado. Este contador é colocado no âmbito [Session], uma vez que diz respeito apenas a um determinado cliente.
Assim que o [global.asax] for executado, será executado o ficheiro seguinte, [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>
Apresenta três informações calculadas pelo seu controlador:
- a identidade do cliente através do seu token de sessão: [jeton]
- o número total de pedidos enviados à aplicação: [nbRequêtesApplication]
- o número total de pedidos efetuados pelo cliente identificado em 1: [nbRequêtesClient]
As três informações são calculadas em [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
' Mais uma solicitação para a aplicação
Application.Item("nbRequêtes") = CType(Application.Item("nbRequêtes"), Integer) + 1
' mais uma solicitação na sessão
Session.Item("nbRequêtes") = CType(Session.Item("nbRequêtes"), Integer) + 1
' inicialização das variáveis de apresentação
nbRequêtesApplication = Application.Item("nbRequêtes").ToString
jeton = Session.SessionID
nbRequêtesClient = Session.Item("nbRequêtes").ToString
End Sub
End Class
Quando o [main.aspx.vb] é executado, estamos a processar um pedido de um determinado cliente. Utilizamos o objeto [Application] para incrementar o número de pedidos da aplicação e o objeto [Session] para incrementar o número de pedidos do cliente cujo pedido está a ser processado. Recorde-se que, embora todos os clientes de uma mesma aplicação partilhem o mesmo objeto [Application], cada um deles possui um objeto [Session] que lhe é próprio.
Testamos a aplicação colocando os quatro ficheiros anteriores numa pasta a que chamamos <application-path> e iniciamos o servidor Cassini com os parâmetros (<application-path>,/aspnet/webapplia). Abrimos um primeiro navegador e acedemos à URL [http://localhost/aspnet/webapplia/main.aspx]:

Fazemos uma segunda solicitação com o botão [Reload]:

Abrimos um segundo navegador para aceder à mesma URL. Para o servidor web, trata-se de um novo cliente:

É possível verificar que o token de sessão mudou e que, por isso, temos um novo cliente. Isto reflete-se no número de pedidos do cliente. Voltemos agora ao primeiro navegador e solicitemos novamente a mesma URL:

O número de pedidos feitos à aplicação é, de facto, contabilizado na totalidade.
4.1.3.5. Sobre a necessidade de sincronizar os clientes de uma aplicação
Na aplicação anterior, o contador de pedidos feitos à aplicação é incrementado no procedimento [Form_Load] da página [main.aspx] da seguinte forma:
' mais uma solicitação para a aplicação
Application.Item("nbRequêtes") = CType(Application.Item("nbRequêtes"), Integer) + 1
Esta instrução, embora simples, requer várias instruções do processador para ser executada. Suponhamos que sejam necessárias três:
- leitura do contador
- incremento do contador
- regravação do contador
O servidor web é executado numa máquina multitarefa, o que significa que cada tarefa tem acesso ao processador durante alguns milissegundos antes de o perder e de o recuperar depois de todas as outras tarefas terem também tido o seu tempo de execução. Suponhamos que dois clientes, A e B, enviam um pedido ao servidor web ao mesmo tempo. Admitamos que o cliente A seja atendido primeiro, que chegue à rotina [Form_Load] a partir de [main.aspx.vb], leia o contador (=100) e seja interrompido porque o seu tempo de execução se esgotou. Suponhamos agora que seja a vez do cliente B e que este tenha o mesmo destino: consegue ler o valor do contador (=100), mas não tem tempo para o incrementar. Os clientes A e B têm ambos um contador igual a 100. Suponhamos que volta a ser a vez do cliente A: este incrementa o seu contador, passa-o para 101 e, em seguida, termina. É a vez do cliente B, que tem na sua posse o valor antigo do contador e não o novo. Por isso, também ele passa o valor do contador para 101 e termina. O valor do contador de pedidos da aplicação está agora errado.
Para ilustrar este problema, retomamos a aplicação anterior e modificamo-la da seguinte forma:
- os ficheiros [global.asax], [global.asax.vb] e [main.aspx] não sofrem alterações
- o ficheiro [main.aspx.vb] passa a ter o seguinte conteúdo:
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
' mais uma solicitação para a aplicação e a sessão
' leitura de contadores
nbRequêtesApplication = CType(Application.Item("nbRequêtes"), Integer)
nbRequêtesClient = CType(Session.Item("nbRequêtes"), Integer)
' espera de 5 s
Thread.Sleep(5000)
' incremento dos contadores
nbRequêtesApplication += 1
nbRequêtesClient += 1
' registo dos contadores
Application.Item("nbRequêtes") = nbRequêtesApplication
Session.Item("nbRequêtes") = nbRequêtesClient
' inicialização das variáveis de apresentação
jeton = Session.SessionID
End Sub
End Class
A atualização dos contadores foi dividida em quatro fases:
- leitura do contador
- suspensão do thread de execução
- incrementação do contador
- regravação do contador
Consideremos novamente os nossos dois clientes, A e B. Entre a fase de leitura e a de incremento dos contadores de pedidos, forçamos o thread de execução a parar durante 5 segundos. Isto terá como consequência imediata que ele perca o processador, que será então atribuído a outra tarefa. Suponhamos que o cliente A seja o primeiro a passar. Ele irá ler o valor N do contador e será interrompido durante 5 segundos. Se, durante esse período, o cliente B tiver o processador à sua disposição, deverá ler o mesmo valor N do contador. No final, os dois clientes deveriam apresentar o mesmo valor do contador, o que seria anormal.
Testamos a aplicação colocando os quatro ficheiros anteriores numa pasta a que chamamos <application-path> e iniciamos o servidor Cassini com os parâmetros (<application-path>,/aspnet/webapplib). Preparamos dois navegadores diferentes com a URL [http://localhost/aspnet/webapplib/main.aspx]. Iniciamos o primeiro para que solicite o URL e, sem esperar pela resposta que chegará 5 segundos mais tarde, iniciamos o segundo navegador. Passados pouco mais de 5 segundos, obtemos o seguinte resultado:

Percebe-se:
- que temos dois clientes diferentes (não têm o mesmo token de sessão)
- que cada cliente efetuou uma solicitação
- que o contador de pedidos feitos à aplicação deveria, portanto, estar em 2 num dos dois navegadores. Mas não é esse o caso.
Agora, vamos fazer outra experiência. Com o mesmo navegador, enviamos cinco pedidos para a URL [http://localhost/aspnet/webapplib/main.aspx]. Mais uma vez, enviamos os pedidos um após o outro, sem esperar pelos resultados. Quando todos os pedidos foram executados, obtemos o seguinte resultado para o último:

É possível observar que:
- que as 5 solicitações foram consideradas como provenientes do mesmo cliente, uma vez que o contador de solicitações do cliente está em 5. Embora não seja mostrado acima, verifica-se que o token de sessão é efetivamente o mesmo para as 5 solicitações.
- que o contador de pedidos feitos à aplicação está correto.
Que conclusão se pode tirar? Nada definitivo. Talvez o servidor web não comece a executar uma solicitação de um cliente se este já tiver uma em execução? Assim, nunca haveria simultaneidade na execução das solicitações de um mesmo cliente. Seriam executadas uma a seguir à outra. Este ponto deve ser verificado. Pode, de facto, depender do tipo de cliente utilizado.
4.1.3.6. Sincronização dos clientes
O problema evidenciado na aplicação anterior é um problema clássico (mas não simples de resolver) de acesso exclusivo a um recurso. No nosso caso específico, é necessário garantir que dois clientes, A e B, não possam estar simultaneamente na sequência de código:
- leitura do contador
- incremento do contador
- reescrita do contador
A uma sequência de código como esta chama-se sequência crítica. Ela requer a sincronização dos threads que a executam simultaneamente. A plataforma .NET oferece várias ferramentas para garantir essa sincronização. Aqui, vamos utilizar a classe [Mutex].

Aqui, utilizaremos apenas os seguintes construtores e métodos:
cria um objeto de sincronização M | |
O thread T1, que executa a operação M.WaitOne(), solicita a posse do objeto de sincronização M. Se o mutex M não estiver na posse de nenhum thread (o que acontece inicialmente), este é «atribuído» ao thread T1 que o solicitou. Se, pouco tempo depois, um thread T2 realizar a mesma operação, ficará bloqueado. Com efeito, um mutex só pode pertencer a um único thread. Será desbloqueado quando o thread T1 libertar o mutex M que detém. Assim, vários threads podem ficar bloqueados à espera do mutex M. | |
O thread T1, que executa a operação M.ReleaseMutex(), abdica da posse do mutex M. Quando o thread T1 perder o processador, o sistema poderá atribuí-lo a um dos threads que aguardam o mutex M. Apenas um deles o obterá por sua vez, ficando os outros que aguardam o M bloqueados |
Um mutex M gere o acesso a um recurso partilhado R. Uma thread solicita o recurso R através de M.WaitOne() e devolve-o através de M.ReleaseMutex(). Uma secção crítica de código que só deve ser executada por uma única thread de cada vez é um recurso partilhado. A sincronização da execução da secção crítica pode ser feita da seguinte forma:
onde M é um objeto Mutex. É claro que nunca se deve esquecer de libertar um Mutex que já não seja necessário, para que outro thread possa, por sua vez, entrar na secção crítica; caso contrário, os threads que aguardam um mutex que nunca foi libertado nunca terão acesso ao processador. Além disso, é necessário evitar a situação de interbloqueio (deadlock) em que duas threads esperam uma pela outra. Consideremos as seguintes ações que se sucedem no tempo:
- um thread T1 obtém a posse de um mutex M1 para aceder a um recurso partilhado R1
- um thread T2 obtém a posse de um mutex M2 para aceder a um recurso partilhado R2
- o thread T1 solicita o mutex M2. Fica bloqueado.
- O thread T2 solicita o mutex M1. Fica bloqueado.
Neste caso, os threads T1 e T2 estão à espera um do outro. Esta situação ocorre quando os threads necessitam de dois recursos partilhados: o recurso R1, controlado pelo mutex M1, e o recurso R2, controlado pelo mutex M2. Uma solução possível consiste em solicitar ambas as recursos simultaneamente através de um único mutex M. No entanto, isso nem sempre é possível, especialmente se implicar uma ocupação prolongada de um recurso dispendioso. Outra solução consiste em que um thread que possua M1 e não consiga obter M2 liberte então M1 para evitar o interbloqueio.
Se pusermos em prática o que acabámos de aprender, a nossa aplicação fica da seguinte forma:
- os ficheiros [global.asax] e [main.aspx] não se alteram
- o ficheiro [global.asax.vb] passa a ser o seguinte:
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)
' É acionado quando a aplicação é iniciada
' inicialização do contador de clientes
Application.Item("nbRequêtes") = 0
' criação de um bloqueio de sincronização
Application.Item("verrou") = New Mutex
End Sub
Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
' É acionado quando a sessão é iniciada
' inicialização do contador de pedidos
Session.Item("nbRequêtes") = 0
End Sub
End Class
A única novidade é a criação de um [Mutex], que será utilizado pelos clientes para se sincronizarem. Como tem de estar acessível a todos os clientes, é colocado no objeto [Application].
- O ficheiro [main.aspx.vb] passa a ter o seguinte conteúdo:
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
' mais uma solicitação para a aplicação e a sessão
' entra-se numa secção crítica — recupera-se o bloqueio de sincronização
Dim verrou As Mutex = CType(Application.Item("verrou"), Mutex)
' solicita-se a entrada exclusiva na secção crítica seguinte
verrou.WaitOne()
' leitura dos contadores
nbRequêtesApplication = CType(Application.Item("nbRequêtes"), Integer)
nbRequêtesClient = CType(Session.Item("nbRequêtes"), Integer)
' espera de 5 s
Thread.Sleep(5000)
' incremento dos contadores
nbRequêtesApplication += 1
nbRequêtesClient += 1
' registo dos contadores
Application.Item("nbRequêtes") = nbRequêtesApplication
Session.Item("nbRequêtes") = nbRequêtesClient
' permite-se o acesso à secção crítica
verrou.ReleaseMutex()
' inicialização das variáveis de apresentação
jeton = Session.SessionID
End Sub
End Class
Vê-se que o cliente:
- solicita entrar sozinho na secção crítica. Para tal, solicita a posse exclusiva do mutex [verrou]
- libera o mutex [verrou] no final da secção crítica, para que outro cliente possa, por sua vez, entrar na secção crítica.
Testamos a aplicação colocando os quatro ficheiros anteriores numa pasta a que chamamos <application-path> e iniciamos o servidor Cassini com os parâmetros (<application-path>,/aspnet/webapplic). Preparamos dois navegadores diferentes com a URL [http://localhost/aspnet/webapplic/main.aspx]. Iniciamos o primeiro para que solicite o URL e, sem esperar pela resposta que chegará 5 segundos mais tarde, iniciamos o segundo navegador. Passados pouco mais de 5 segundos, obtemos o seguinte resultado:

Desta vez, o contador de pedidos da aplicação está correto.
O que se retira desta longa demonstração é a necessidade absoluta de sincronizar os clientes de uma mesma aplicação web, caso estes tenham de atualizar elementos partilhados por todos os clientes.
4.1.3.7. Gestão do token de sessão
Já falámos várias vezes do token de sessão que o cliente e o servidor web trocam entre si. Recorde-se o seu princípio:
- o cliente faz uma primeira solicitação ao servidor. Não envia nenhum token de sessão.
- Devido à ausência do token de sessão na solicitação, o servidor reconhece um novo cliente e atribui-lhe um token. A este token está também associado um objeto [Session], que será utilizado para armazenar informações específicas desse cliente. O token acompanhará todas as solicitações desse cliente. Será incluído nos cabeçalhos HTTP da resposta à primeira solicitação do cliente.
- O cliente conhece agora o seu token de sessão. Irá reenviá-lo nos cabeçalhos HTTP de cada uma das solicitações seguintes que enviar ao servidor web. Graças ao token, o servidor poderá recuperar o objeto [Session] associado ao cliente.
Para ilustrar este mecanismo, retomamos a aplicação anterior, alterando apenas o ficheiro [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
' mais uma solicitação para a aplicação e a sessão
' entra-se numa secção crítica — recupera-se o bloqueio de sincronização
Dim verrou As Mutex = CType(Application.Item("verrou"), Mutex)
' solicita-se o acesso exclusivo à secção seguinte
verrou.WaitOne()
' leitura de contadores
nbRequêtesApplication = CType(Application.Item("nbRequêtes"), Integer)
nbRequêtesClient = CType(Session.Item("nbRequêtes"), Integer)
' espera de 5 s
Thread.Sleep(5000)
' incremento dos contadores
nbRequêtesApplication += 1
nbRequêtesClient += 1
' registo dos contadores
Application.Item("nbRequêtes") = nbRequêtesApplication
Session.Item("nbRequêtes") = nbRequêtesClient
' permite-se o acesso à secção crítica
verrou.ReleaseMutex()
' inicialização das variáveis de apresentação
jeton = Session.SessionID
End Sub
Private Sub Page_Init(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.Init
' armazenar o pedido do cliente em request.txt na pasta da aplicação
Dim requestFileName As String = Me.MapPath(Me.TemplateSourceDirectory) + "\request.txt"
Me.Request.SaveAs(requestFileName, True)
End Sub
End Class
Quando ocorre o evento [Page_Init], guardamos o pedido do cliente na pasta da aplicação. Recorde-se alguns pontos:
- [TemplateSourceDirectory] representa o caminho virtual da página em execução,
- MapPath (TemplateSourceDirectory) representa o caminho físico correspondente. Isto permite-nos construir o caminho físico do ficheiro a criar,
- [Request] é um objeto que representa a solicitação atualmente em processamento. Este objeto foi construído a partir da solicitação bruta enviada pelo cliente, c.a.d, uma sequência de linhas de texto com o seguinte formato:

- Request.Save([FileName]) guarda a totalidade da solicitação do cliente (cabeçalhos HTTP e, eventualmente, o documento que se segue) num ficheiro cujo caminho é passado como parâmetro.
Assim, poderemos saber exatamente qual foi a solicitação do cliente. Testamos a aplicação colocando os quatro ficheiros anteriores numa pasta a que chamamos <application-path> e iniciamos o servidor Cassini com os parâmetros (<application-path>,/aspnet/session1). Em seguida, utilizando um navegador, solicitamos o URL
[http://localhost/aspnet/session1/main.aspx]. Obtemos o seguinte resultado:

Utilizamos o ficheiro [request.txt] guardado por [main.aspx.vb] para aceder ao pedido do navegador:
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
Constatamos que o navegador efetuou o pedido de URL [/aspnet/session1/main.aspx], tendo enviado outras informações de que já falámos no capítulo anterior. Não se observa aqui nenhum token de sessão. A página recebida em resposta mostra, por sua vez, que o servidor criou um token de sessão. Ainda não sabemos se o navegador o recebeu. Vamos agora efetuar uma segunda solicitação com o mesmo navegador (Atualizar). Obtemos a seguinte nova resposta:

Existe, de facto, um acompanhamento da sessão, uma vez que o número de pedidos da sessão foi corretamente incrementado. Vejamos agora o conteúdo do ficheiro [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
Verifica-se que, para esta segunda solicitação, o navegador enviou ao servidor um novo cabeçalho HTTP [Cookie:] que define uma informação denominada [ASP.NET_SessionId] e cujo valor é o token de sessão que apareceu na resposta à primeira solicitação. Graças a este token, o servidor web irá associar esta nova solicitação ao objeto [Session], identificado pelo token [y153tk45sise0lrhdzrf22m3], e recuperar o contador de solicitações associado.
Ainda não sabemos através de que mecanismo o servidor enviou o token ao cliente, uma vez que não temos acesso à resposta HTTP do servidor. Recorde-se que esta tem a mesma estrutura que o pedido do cliente, ou seja, um conjunto de linhas de texto com o seguinte formato:

Tivemos a oportunidade de utilizar um cliente web que nos dava acesso à resposta HTTP do servidor web, o cliente curl. Utilizamo-lo novamente, numa janela do DOS, para consultar a mesma URL que o navegador anterior:
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>
Já temos a resposta à nossa pergunta. O servidor web envia o token de sessão sob a forma de um cabeçalho HTTP [Set-Cookie:]:
Vamos fazer a mesma solicitação sem reenviar o token de sessão. Obtemos a seguinte resposta:
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>
Como não reenviámos o token de sessão, o servidor não conseguiu identificar-nos e atribuiu-nos um novo token. Para dar continuidade a uma sessão já iniciada, o cliente deve reenviar ao servidor o token de sessão que recebeu. Vamos fazê-lo aqui utilizando a opção [--cookie clé=valeur] do curl, que irá gerar o cabeçalho HTTP [Cookie: clé=valeur]. Vimos que o navegador enviou este cabeçalho HTTP na sua segunda solicitação.
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>
É possível observar vários aspetos:
- o contador de pedidos do cliente foi efetivamente incrementado, o que demonstra que o servidor reconheceu o nosso token.
- o token de sessão apresentado pela página é, de facto, aquele que enviámos
- o token de sessão já não consta dos cabeçalhos HTTP enviados pelo servidor web. Com efeito, este apenas o envia uma vez: durante a geração do token no início de uma nova sessão. Assim que o cliente obtém o seu token, cabe-lhe a ele utilizá-lo quando quiser para ser reconhecido.
Nada impede um cliente de utilizar vários tokens de sessão, como mostra o exemplo seguinte com [curl], em que utilizamos o token obtido na nossa primeira solicitação (solicitação 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>
O que significa este exemplo? Enviámos um token obtido pouco antes. Quando o servidor web cria um token, este é mantido enquanto o cliente associado a esse token continuar a enviar-lhe pedidos. Após um certo período de inatividade (20 minutos por predefinição com IIS), o token é eliminado. O exemplo anterior mostra que utilizámos um token ainda ativo.
Podemos ficar curiosos em saber quais foram as solicitações HTTP do cliente [curl] durante todas estas operações. Sabemos que foram registadas no ficheiro [request.txt]. Aqui está a última:
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
É possível ver aqui o cabeçalho HTTP que envia o token de sessão.
As informações transmitidas pelo servidor através dos cabeçalhos HTTP e [Set-Cookie:] são denominadas cookies. O servidor pode utilizar este mecanismo para transmitir outras informações além do token de sessão. Quando o servidor S transmite um cookie a um cliente, indica também o tempo de vida D do mesmo e o valor associado URL U. Isto significa que, para o cliente, quando solicita ao servidor S um URL do tipo /U/caminho, pode reenviar o cookie se não o tiver recebido há um período superior a D. Nada impede um cliente de não respeitar este código de conduta. Os navegadores, por sua vez, respeitam-no. Alguns navegadores permitem aceder ao conteúdo dos cookies que recebem. É o caso do navegador Mozilla. Aqui estão, por exemplo, as informações relacionadas com o cookie enviado pelo servidor num exemplo anterior:

Nele encontram-se:
- o nome do cookie [ASP.NET_SessionId]
- o seu valor: [y153...m3]
- o dispositivo ao qual está associado: [localhost]
- o URL ao qual está associado: [/]
- a sua duração: [at end of session]
O navegador enviará, portanto, o token de sessão sempre que solicitar um URL na forma [http://localhost/...], c.a.d. sempre que solicitar uma URL ao servidor web da máquina [localhost]. A duração do cookie corresponde à da sessão. Para o navegador, isto significa que o cookie nunca expira. O navegador enviá-lo-á sempre que solicitar uma URL da máquina [localhost]. Assim, se o navegador receber o token de sessão no dia D, for fechado e voltar a ser utilizado no dia seguinte, reenviará o token de sessão (que foi guardado num ficheiro). O servidor receberá esse token que já não possui, uma vez que um token de sessão tem uma duração limitada no servidor (20 minutos no IIS). Por isso, iniciará uma nova sessão.
É possível desativar a utilização de cookies num navegador. Nesse caso, o cliente recebe o token de sessão, mas não o reenvia, o que impede o acompanhamento da sessão. Para demonstrar isso, desativamos a utilização de cookies no nosso navegador (Mozilla, neste caso):

Além disso, eliminamos todos os cookies existentes:

Feito isto, reiniciamos o servidor Cassini para recomeçar do zero e, com o navegador, solicitamos novamente a URL [http://localhost/aspnet/session1/main.aspx]:

Vamos verificar se o nosso navegador armazenou um cookie:

Constatamos que o navegador não armazenou o cookie do token de sessão que o servidor lhe enviou. Podemos, portanto, esperar que não haja acompanhamento da sessão. Solicitamos novamente a mesma URL (Atualizar):

O resultado é exatamente o esperado. O navegador não reenviou o token de sessão, que, no entanto, tinha recebido mas não armazenado. O servidor iniciou, portanto, uma nova sessão com um novo token. Retemos deste exemplo que a nossa política de acompanhamento de sessões fica comprometida se o utilizador tiver desativado a utilização de cookies no seu navegador. Existe, no entanto, outra forma, além dos cookies, de trocar o token de sessão entre o servidor e o cliente. É, de facto, possível indicar ao servidor web que a aplicação funciona sem cookies. Isto é feito através do ficheiro de configuração [web.config]:
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
<system.web>
<sessionState cookieless="true" timeout="10" />
</system.web>
</configuration>
O ficheiro de configuração acima indica que a aplicação irá funcionar sem cookies (cookieless="true") e que o tempo máximo de inatividade de um token de sessão é de 10 minutos (timeout="10"). Após este período, a sessão associada ao token é eliminada. O processo de troca do token de sessão entre o servidor e o cliente será o seguinte:
- o cliente solicita o URL [http://machine:port/V/chemin], em que V é uma pasta virtual do servidor web
- o servidor gera um token J e responde ao cliente para que este seja redirecionado para a URL [http://machine:port/V/(J)/chemin]. Assim, inseriu o token na URL a ser consultada, imediatamente a seguir à pasta virtual V
- o cliente obedece a esta redireção e solicita a nova URL URL [http://machine:port/V/(J)/chemin].
- O servidor responde a este pedido e envia uma página de resposta.
Vamos ilustrar estes diferentes pontos. Colocamos toda a aplicação anterior numa nova pasta <application-path>. Colocamos nessa mesma pasta o ficheiro [web.config] anterior. Além disso, alteramos o código de apresentação [main.aspx] para incluir um 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>
Este link aponta para a página [main.aspx] e é, portanto, equivalente ao botão (Reload) do navegador. O servidor Cassini é iniciado com os parâmetros (<application-path>,/session2). Estamos a desviar-nos da nossa prática habitual, que consistia em registar a pasta virtual [/aspnet/XX]. Com efeito, devido à inserção do token de sessão no URL, a pasta virtual deve conter apenas um elemento: /XX. Começamos por utilizar o cliente [curl] para solicitar o 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>
Verificamos que o servidor responde com o cabeçalho HTTP [HTTP/1.1 302 Found] em vez de [HTTP/1.1 200 OK]. Trata-se de um cabeçalho que solicita ao cliente que se redirecione para o URL indicado pelo cabeçalho HTTP Location [Location: /session2/(hinadjag3bt0u155g5hqe245)/main.aspx]. É possível ver o token de sessão que foi inserido na URL de redirecionamento. Um navegador que receba esta resposta solicita a nova URL de forma transparente para o utilizador, que não vê a nova solicitação. Caso o navegador não consiga gerir a redireção por si próprio, é enviado um documento HTML a seguir ao código HTTP acima referido. Nele encontra-se um link para a URL de redireção, no qual o utilizador poderá clicar.
Agora, vamos fazer o mesmo com um navegador em que os cookies foram desativados. Solicitamos, mais uma vez, a URL [http://localhost/session2/main.aspx]. Obtemos a seguinte resposta do servidor:

Em primeiro lugar, constatamos que a URL apresentada pelo navegador não é a que solicitámos. Isso indica que ocorreu um redirecionamento. De facto, o navegador exibe sempre a URL URL do último documento recebido. Portanto, se não exibir a URL [http://localhost/session2/main.aspx], é porque lhe foi solicitado que redirecionasse para outra URL. Podem ocorrer vários redirecionamentos. A URL apresentada pelo navegador é a URL do último redirecionamento. Podemos constatar que o token de sessão está presente na URL apresentada pelo navegador. É possível vê-lo porque esse token também é apresentado pelo nosso programa na página.
Recordemos o código do link que foi colocado na página:
<a href="main.aspx">Recharger l'application</a>
Trata-se de um link relativo, uma vez que não começa com o sinal /, o que o tornaria um link absoluto. Relativo a quê? Para compreender este ponto, é necessário voltar à URL do documento atualmente exibido: [http://localhost/session2/(gu5ee455pkpffn554e3b1a32)/main.aspx]. Os links relativos que forem encontrados neste documento serão relativos ao caminho [http://localhost/session2/(gu5ee455pkpffn554e3b1a32)]. Assim, o nosso link acima é equivalente ao link:
<a href=" http://localhost/session2/(gu5ee455pkpffn554e3b1a32)/main.aspx">Recharger l'application</a>
É isso que o navegador nos mostra quando passamos o rato sobre o link:

Se clicarmos no link [Recharger l'application], é então a URL
[http://localhost/session2/(gu5ee455pkpffn554e3b1a32)/main.aspx] que é chamada. O servidor irá, assim, receber o token de sessão e poderá recuperar as informações que lhe estão associadas. É isso que a resposta do navegador nos mostra:

Concluímos que, se precisarmos de acompanhar uma sessão numa aplicação web e não tivermos a certeza de que os navegadores dos clientes dessa aplicação irão autorizar a utilização de cookies, então
- devemos configurar a aplicação para que funcione sem cookies
- as páginas da aplicação devem incluir links relativos e não absolutos
4.2. Recuperar as informações de um pedido do cliente
4.2.1. O ciclo de pedido-resposta do cliente-servidor web
Recordemos aqui o contexto cliente-servidor de uma aplicação web:

A solicitação de um cliente para uma aplicação web é processada da seguinte forma:
- o cliente abre uma ligação TCP/IP para uma porta P do serviço web da máquina M que aloja a aplicação web
- envia, através dessa ligação, uma sequência de linhas de texto de acordo com o protocolo HTTP. Este conjunto de linhas constitui o que se denomina «pedido do cliente». Tem o seguinte formato:

Depois de enviada a solicitação, o cliente aguarda a resposta.
- A primeira linha dos cabeçalhos HTTP especifica a ação solicitada ao servidor web. Pode assumir várias formas:
- GET url HTTP/<versão>, sendo que <versão> é atualmente igual a 1.0 ou 1.1. Neste caso, a solicitação não inclui a parte [Document]
- POST url HTTP/<versão>. Neste caso, a solicitação inclui uma parte [Document], na maioria das vezes uma lista de informações destinadas à aplicação web
- PUT url HTTP/<versão>. O cliente envia um documento na parte [Document] e pretende armazená-lo no servidor na morada url
Quando o cliente pretende transmitir informações à aplicação web à qual se ligou, dispõe principalmente de dois meios:
- (continuação)
- a sua solicitação é [GET url_enrichie HTTP/<version>], em que url_enrichie tem o formato [url?param1=val1¶m2=val2&...]. O cliente transmite, além do URL, uma série de informações no formato [clé=valeur].
- A sua solicitação é [POST url HTTP/<version>]. Na parte [Document], transmite informações no mesmo formato que anteriormente: [param1=val1¶m2=val2&...].
- No servidor, toda a cadeia de processamento da solicitação do cliente tem acesso a esta através de um objeto global denominado Request. O servidor web colocou neste objeto a totalidade da solicitação do cliente num formato que iremos descobrir. A aplicação solicitada irá processar este objeto e construir uma resposta para o cliente. Esta resposta está disponível num objeto global denominado Response. O papel da aplicação web é construir um objeto [Response] a partir do objeto [Request] recebido. A cadeia de processamento dispõe também dos objetos globais [Application] e [Session], dos quais já falámos e que lhe permitirão partilhar dados entre diferentes clientes (Aplicação) ou entre pedidos sucessivos de um mesmo cliente (Sessão).
- A aplicação enviará a sua resposta ao servidor através do objeto [Response]. Esta resposta, uma vez na rede, assumirá a seguinte forma: HTTP:

Assim que esta resposta for enviada, o servidor encerrará a ligação de rede de receção (a menos que o cliente lhe tenha indicado para não o fazer).
- O cliente irá receber a resposta e, por sua vez, encerrará a ligação (em transmissão). O que será feito com essa resposta depende do tipo de cliente. Se for um navegador e o documento recebido for um documento HTML, este será apresentado. Se o cliente for um programa, a resposta será analisada e processada.
- O facto de, após o ciclo de pedido-resposta, a ligação que ligava o cliente ao servidor ser encerrada faz do protocolo HTTP um protocolo sem estado. Na solicitação seguinte, o cliente estabelecerá uma nova ligação de rede ao mesmo servidor. Como já não se trata da mesma ligação de rede, o servidor não tem qualquer possibilidade (ao nível do TCP/IP e do HTTP) de associar esta nova ligação a uma anterior. É o sistema de token de sessão que permitirá essa associação.
4.2.2. Recuperar as informações transmitidas pelo cliente
Analisamos agora algumas propriedades e métodos do objeto [Request] que permitem ao código da aplicação aceder à solicitação do cliente e, consequentemente, às informações que este transmitiu. O objeto [Request] é do tipo [HttpRequest]:

Esta classe possui várias propriedades e métodos. Estamos interessados nas propriedades HttpMethod, QueryString, Form e Params, que nos permitirão aceder aos elementos da cadeia de informações [param1=val1¶m2=val2&...].
método de consulta do cliente: GET, POST, HEAD, ... | |
recolha dos elementos da cadeia de consulta param1=val1¶m2=val2&.. da 1.ª linha HTTP [méthode]?param1=val1¶m2=val2&... onde [méthode] pode ser GET, POST, HEAD. | |
recolha dos elementos da cadeia de consulta param1=val1¶m2=val2&... que se encontram na parte [Document] da consulta (método POST). | |
reúne várias coleções: QueryString, Form, ServerVariables, Cookies, numa única coleção. |
4.2.3. Exemplo 1
Vamos implementar estes elementos num primeiro exemplo. A aplicação terá apenas um elemento [main.aspx]. O código de apresentação [main.aspx] será o seguinte:
<%@ 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>
A página apresenta três informações [méthode, nom, age] calculadas pela sua parte controladora [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
' a solicitação do cliente é armazenada em request.txt na pasta da aplicação
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
' recuperam-se os parâmetros da solicitação
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 a página é carregada (Form_Load), as informações [nom, age] são recuperadas a partir do pedido do cliente. Estas são pesquisadas nas duas coleções [QueryString] e [Form]. . Além disso, em [Page_Init], guardamos a solicitação do cliente para podermos verificar o que este enviou. Colocamos estes dois ficheiros numa pasta <application-path> e iniciamos o servidor Cassini com os parâmetros (<application-path>,/request1); em seguida, utilizando um navegador, acedemos à URL
[http://localhost/request1/main.aspx?nom=tintin&age=27]. Obtemos a seguinte resposta:

As informações transmitidas pelo cliente foram recuperadas corretamente. A solicitação do navegador guardada no ficheiro [request.txt] é a seguinte:
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
Vemos que o navegador efetuou uma solicitação GET. Para efetuar uma solicitação POST, vamos utilizar o cliente [curl]. Numa janela do DOS, digitamos o seguinte comando:
para apresentar os cabeçalhos HTTP da resposta | |
para enviar a informação «param=valor» através de um POST |
A resposta do servidor é a seguinte:
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>
O servidor recuperou, mais uma vez, os parâmetros enviados, desta vez por um POST. Para confirmar este último ponto, pode-se verificar o conteúdo do ficheiro [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
O cliente [curl] executou corretamente um POST. Agora, vamos combinar os dois métodos de transmissão de informação. Colocamos [age] no URL solicitado e [nom] no documento enviado:
A solicitação enviada por [curl] é a seguinte (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
Vê-se que a idade foi incluída no URL solicitado. Obter-se-á essa informação na coleção [QueryString]. O nome, por sua vez, foi incluído no documento enviado para esse URL. Obter-se-á essa informação na coleção [Form]. A resposta obtida pelo cliente [curl]:
<html>
<head>
<title>Requête client</title>
</head>
<body>
Requête :
post
<br />
nom :
tintin
<br />
âge :
27
<br />
</body>
</html>
Por fim, não enviemos nenhuma informação para o servidor:
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>
Sugere-se ao leitor que releia o código do controlador [main.aspx.vb] para compreender esta resposta.
4.2.4. Exemplo 2
É possível que o cliente envie vários valores para uma mesma chave. Então, o que acontece se, no exemplo anterior, solicitarmos a URL [http://localhost/request1/main.aspx?nom=tintin&age=27&nom=milou], onde a chave [nom] aparece duas vezes? Vamos experimentar com um navegador:

A nossa aplicação recuperou corretamente os dois valores associados à chave [nom]. A apresentação é um pouco enganadora. Foi obtida através da instrução
If Not Request.QueryString("nom") Is Nothing Then nom = Request.QueryString("nom").ToString
O método [ToString] produziu a cadeia [tintin,milou] que foi apresentada. Esta esconde o facto de que, na realidade, o objeto [Request.QueryString("nom")] é um array de cadeias de caracteres {"tintin","milou"}. O exemplo seguinte ilustra este ponto. A página de apresentação [main.aspx] será a seguinte:
<%@ 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>
Há novidades nesta página que utiliza o que se denomina «controlos de servidor». Estes caracterizam-se pelo atributo [runat="server"]. Ainda é cedo para introduzir o conceito de controlo de servidor. Basta saber que, neste caso:
- a página tem duas listas (etiquetas <asp:listbox>)
- que estas listas são objetos (lstQueryString, lstForm) do tipo [ListBox], que serão criados pelo controlador da página
- que estes objetos só existem no servidor web. No momento da resposta, serão transformados em tags HTML clássicas que o cliente poderá compreender. Um objeto [listbox] será assim transformado (também se diz «renderizado») em tags HTML <select> e <option>.
- O principal objetivo destes objetos é livrar o código de apresentação de todo o código VB, ficando este confinado ao controlador.
O controlador [main.aspx.vb] responsável pela construção dos dois objetos [lstQueryString] e [lstForm] é o seguinte:
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
' a solicitação do cliente é armazenada em request.txt na pasta da aplicação
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
' recupera-se todo o conjunto de informações do 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
' inicialmente, uma lista de informações vazia
Dim infos As New ArrayList
' recuperam-se as chaves da coleção
Dim clés() As String = data.AllKeys
' percorre-se a tabela de chaves
Dim valeurs() As String
For Each clé As String In clés
' valores associados à chave
valeurs = data.GetValues(clé)
' apenas um valor?
If valeurs.Length = 1 Then
infos.Add(clé + "=" + valeurs(0))
Else
' vários valores
For ivalue As Integer = 0 To valeurs.Length - 1
infos.Add(clé + "(" + ivalue.ToString + ")=" + valeurs(ivalue))
Next
End If
Next
' retorna o resultado
Return infos
End Function
End Class
Os pontos importantes deste código são os seguintes:
- no [Form_Load], a página recupera as duas coleções [QueryString] e [Form]. Utiliza uma função [getValeurs] para colocar o conteúdo destas duas coleções em dois objetos do tipo [ArrayList], que conterão cadeias de caracteres do tipo [clé=valeur] se a chave da coleção estiver associada a um único valor, ou [clé(i)=valeur], caso a chave esteja associada a vários valores.
- Cada um dos objetos [ArrayList] é, em seguida, associado a um dos objetos [ListBox] da página de apresentação por meio de duas instruções:
- [ListBox.DataSource=ArrayList] e [ListBox.DataBind]. Esta última instrução transfere os elementos de [DataSource] para a coleção [Items] do objeto [ListBox]
Note-se que nenhum dos dois objetos [ListBox] é criado explicitamente por uma operação [New]. Deduz-se, portanto, que, na presença da baliza <asp:listbox id="xx">...<asp:listbox/>, o servidor web cria ele próprio o objeto [ListBox] referenciado pelo atributo [id] da baliza.
- A função [getValeurs] utiliza o objeto do tipo [NameValueCollection] que lhe é passado como parâmetro para produzir um resultado do tipo [ArrayList].
Colocamos os dois ficheiros anteriores numa pasta <application-path> e iniciamos o servidor Cassini com os parâmetros (<application-path>,/request2); em seguida, solicitamos a URL
[http://localhost/request2/main.aspx?nom=tintin&age=27]. Obtemos a seguinte resposta:

Solicitamos agora uma URL em que a chave [nom] aparece duas vezes:

Constatamos que o objeto [Request.QueryString("nome")) era, de facto, um array. Aqui, as solicitações foram feitas através de um método GET. Utilizamos o cliente [curl] para efetuar uma consulta 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>
Pode-se verificar que o cliente recebe, de facto, o código HTML clássico para as duas listas da página. Aparecem informações que não inserimos nós próprios, como o campo oculto [_VIEWSTATE]. Estas informações foram geradas pelas tags <asp:xx runat="server>. Teremos de aprender a dominá-las.
4.3. Implementação de uma arquitetura MVC
4.3.1. O conceito
Vamos concluir este longo capítulo com a implementação de uma aplicação construída de acordo com o modelo MVC (Model-View-Controller). Uma aplicação web arquitetada de acordo com este modelo tem o seguinte aspeto:

- o cliente envia as suas solicitações a uma entidade específica da aplicação denominada «controlador»
- o controlador analisa a solicitação do cliente e executa-a. Para tal, conta com a ajuda de classes que agrupam a lógica de negócio da aplicação e de classes de acesso aos dados.
- Dependendo do resultado da execução da solicitação, o controlador opta por enviar uma determinada página como resposta ao cliente
No nosso modelo, todas as solicitações passam por um único controlador, que é o maestro de toda a aplicação web. A vantagem deste modelo é que é possível agrupar no controlador tudo o que deve ser feito antes de cada solicitação. Suponhamos, por exemplo, que a aplicação exija uma autenticação. Esta é realizada uma única vez. Uma vez bem-sucedida, a aplicação irá armazenar na sessão informações relacionadas com o utilizador que acabou de se autenticar. Como um cliente pode aceder diretamente a uma página da aplicação sem se autenticar, cada página terá, portanto, de verificar na sessão se a autenticação foi efetivamente realizada. Se todas as solicitações passarem por um único controlador, é este que pode realizar essa tarefa. As páginas para as quais a solicitação venha a ser encaminhada não terão de o fazer.
4.3.2. Controlar uma aplicação MVC sem sessão
Pelo que vimos até agora, podemos pensar que o ficheiro [global.asax] poderia desempenhar o papel de controlador. Com efeito, sabemos que todas as solicitações passam por ele. Está, portanto, bem posicionado para controlar tudo. A aplicação que se segue utiliza-o para esse fim. O seu caminho virtual será [http://localhost/mvc1/main.aspx]. Para indicar o que pretende, o cliente irá adicionar à URL um parâmetro action=valor. Dependendo do valor do parâmetro [action], o controlador [global.asax] encaminhará a solicitação para uma página específica:
- [main.aspx] se o parâmetro «action» não estiver definido ou se «action=main»
- [action1.aspx] se action=action1
- [inconnu.aspx] se «action» não se enquadrar nos casos 1 e 2
As páginas [main.aspx, action1.aspx, inconnu.aspx] limitam-se a apresentar o valor de [action] que provocou a sua exibição. Apresentamos abaixo os oito ficheiros desta aplicação e comentamo-los sempre que necessário:
[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)
' recuperamos a ação a realizar
Dim action As String
If Request.QueryString("action") Is Nothing Then
action = "main"
Else
action = Request.QueryString("action").ToString.ToLower
End If
' coloca-se a ação no contexto do pedido
Context.Items("action") = action
' executa-se a ação
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
Pontos a ter em conta:
- interceptamos todos os pedidos do cliente no procedimento [Application_BeginRequest], que é executado automaticamente no início de cada novo pedido feito à aplicação.
- Nesta rotina, temos acesso ao objeto [Request], que é a representação da solicitação HTTP do cliente. Como esperamos uma URL do tipo [http://localhost/mvc1/main.aspx?action=xx], procuramos uma chave [action] na coleção [Request.QueryString]. Se não a encontrarmos, definimos por predefinição a ação igual a «main».
- O valor do parâmetro [action] é colocado no objeto [Context]. Tal como os objetos [Application, Session, Request, Response, Server], este objeto é global e acessível em qualquer código. Este objeto é passado de página em página se o pedido for processado por várias páginas, como será o caso aqui. É eliminado assim que a resposta for enviada ao cliente. A sua duração corresponde, portanto, à duração do processamento do pedido.
- De acordo com o valor do parâmetro [action], a solicitação é encaminhada para a página apropriada. Para tal, utiliza-se o objeto global [Server], que, graças ao seu método, permite transferir a solicitação atual para outra página. O seu primeiro parâmetro é o nome da página de destino; o segundo é um valor booleano que indica se se deve ou não transferir para a página de destino as coleções [QueryString] e [Form]. Neste caso, a resposta é sim.
Os ficheiros [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
' recupera-se a ação em curso
action = Me.Context.Items("action").ToString
End Sub
End Class
O controlador [main.aspx.vb] limita-se a recuperar o valor da chave [action] no contexto, sendo esse valor apresentado pelo código de apresentação. O objetivo aqui é mostrar a passagem do objeto [Context] entre diferentes páginas que processam a mesma solicitação do cliente. As páginas [action1.aspx] e [inconnu.aspx] funcionam de forma semelhante:
[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
' recupera-se a ação em curso
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
' recupera-se a ação em curso
action = Me.Context.Items("action").ToString
End Sub
End Class
Para testar, os documentos anteriores são colocados numa pasta <application-path> e o Cassini é iniciado com os parâmetros (<application-path>,/mvc1). Acedemos à URL [http://localhost/mvc1/main.aspx]:

A solicitação não enviou nenhum parâmetro [action]. O código do controlador da aplicação [global.asax.vb] fez com que fosse apresentada a página [main.aspx]. Agora solicitamos a URL [http://localhost/mvc1/main.aspx?action=action1]:

O código do controlador da aplicação [global.asax.vb] gerou a página [action1.aspx]. Agora solicitamos a URL [http://localhost/mvc1/main.aspx?action=xx]:

A ação não foi reconhecida e o controlador [global.asax.vb] apresentou a página [inconnu.aspx].
4.3.3. Controlar uma aplicação MVC com sessão
Na maioria das vezes, as diferentes solicitações de um cliente para uma aplicação têm de partilhar informações. Vimos uma solução possível para este problema: armazenar as informações a partilhar no objeto [Session] da solicitação. Este objeto é, de facto, partilhado por todas as solicitações e é capaz de armazenar informações na forma (chave, valor), em que a chave é do tipo [String] e o valor é de qualquer tipo derivado de [Object].
No exemplo anterior, as diferentes páginas associadas às diferentes ações eram chamadas no procedimento [Application_BeginRequest] do ficheiro [global.asax.vb]:
Sub Application_BeginRequest(ByVal sender As Object, ByVal e As EventArgs)
' recupera-se a ação a realizar
Dim action As String
If Request.QueryString("action") Is Nothing Then
action = "main"
Else
action = Request.QueryString("action").ToString.ToLower
End If
' coloca-se a ação no contexto do pedido
Context.Items("action") = action
' executa-se a ação
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
Verifica-se que, no procedimento [Application_BeginRequest], o objeto [Session] não está acessível. O mesmo se verifica na página para a qual a execução é transferida. Por conseguinte, este modelo não pode ser utilizado numa aplicação com sessão. Podemos atribuir a função de controlador a qualquer página, por exemplo, [default.aspx]. Os ficheiros [global.asax, global.asax.vb] desaparecem então para serem substituídos pelos ficheiros [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
' recupera-se a ação a realizar
Dim action As String
If Request.QueryString("action") Is Nothing Then
action = "main"
Else
action = Request.QueryString("action").ToString.ToLower
End If
' coloca-se a ação no contexto da solicitação
Context.Items("action") = action
' recupera-se a ação anterior, caso exista
Context.Items("actionPrec") = Session.Item("actionPrec")
If Context.Items("actionPrec") Is Nothing Then Context.Items("actionPrec") = ""
' a ação atual é guardada na sessão
Session.Item("actionPrec") = action
' executa-se a ação
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
Para destacar o mecanismo de sessão, as diferentes páginas irão apresentar, além da ação atual, a ação que a precedeu. Para uma sequência de ações A1, A2, ..., An, quando a ação Ai ocorre, o controlador acima:
- coloca a ação atual Ai no contexto
- recupera na sessão a ação Ai-1 que a precedeu. Caso não exista nenhuma (como no caso da ação A1), a cadeia da ação anterior fica vazia.
- coloca a ação atual Ai na sessão, substituindo Ai-1
- transfere a execução para a página adequada
As três páginas da aplicação são as seguintes:
[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>
Como as três páginas apresentam as mesmas informações [action, actionPrec], todas elas podem ter o mesmo controlador de página. Por isso, todas elas foram derivadas da classe [main] do ficheiro [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
' recupera-se a ação em curso
action = Me.Context.Items("action").ToString
' e a ação anterior
actionPrec = Me.Context.Items("actionPrec").ToString
End Sub
End Class
O código acima limita-se a recuperar as informações inseridas no contexto pelo controlador da aplicação [default.aspx.vb].
Todos estes ficheiros estão localizados em <application-path> e o Cassini é iniciado com os parâmetros (<application-path>,/mvc2). Primeiro, solicita-se a URL [http://localhost/mvc2]:

A URL [http://localhost/mvc2] remete para uma pasta. Sabemos que, neste caso, é o documento [default.aspx] dessa pasta que é devolvido pelo servidor, caso exista. Neste caso, não foi especificada nenhuma ação. Por isso, foi executada a ação [main]. Passemos agora à ação [action1]:

A ação atual e a ação anterior foram identificadas corretamente. Passemos à ação [xx]:

4.4. Conclusion
Temos agora os elementos básicos a partir dos quais qualquer aplicação ASP.ET é construída. Resta-nos, no entanto, introduzir um conceito importante: o de formulário. É esse o tema do capítulo seguinte.