Vamos falar sobre o ASP.NET MVC. Este novo framework tornará popular o padrão MVC (Model View Controller) no mundo do desenvolvimento web na plataforma Microsoft. O Framework é realmente impressionante, ainda que esteja, no momento do fechamento desta edição, em versão de Preview 2, ou seja, não é sequer uma versão Beta (o que quer dizer que ainda não tem implementadas todas as funcionalidades previstas).
Você pode baixá-lo no site do ASP.NET MVC. Uma das funcionalidades mais interessantes do novo framework é o seu elegante roteamento, e ele é atribuído erroneamente como parte do componente MVC. Na verdade ele é um componente independente, do qual o MVC depende. Isso quer dizer que podemos utilizá-lo com os Web Forms padrões do ASP.NET, e é justamente o que faremos neste artigo e na sua seqüência.
O Routing é um dos componentes do qual o framework MVC depende, mais especificamente do componente System.Web.Routing. O outro componente do qual o MVC depende é o System.Web.Abstractions, do qual o Routing também depende. Na prática, o MVC depende do Routing, que depende do Web Abstractions. Portanto, para utilizarmos o Routing, teremos também que referenciar o componente Web Abstractions, mas não precisaremos do MVC.
Os outros componentes referenciados são parte do .NET Framework 3.5, ou seja, para utilizar o Routing em um projeto, precisamos apenas de 2 dlls no diretório Bin de uma aplicação Web, mais nada. Após a instalação do MVC, essas dlls ficam no diretório C:\Program Files\Microsoft ASP.NET MVC Preview 2\Assemblies. Para ajudar, as dlls são pequenas (menos de 100k) e assinadas. É possível baixar o código-fonte do MVC no Codeplex, o site de projetos open source da Microsoft. O código-fonte do Routing também vai ser aberto, e por enquanto pode-se gerar o código-fonte do Routing com auxílio do Reflector.
O que é o ASP.NET Routing
Atualmente, a única forma disponível out-of-the-box para entregar conteúdo ASP.NET é com a utilização de arquivos aspx (e ashx, asmx, axd etc.) dispostos fisicamente no disco. Sendo assim, quando chamamos no navegador o caminho http://servidorweb/app/Categorias.aspx, estamos, de forma simplificada, solicitando à máquina servidorweb, que, através da aplicação app, nos entregue a página dinâmica Categorias.aspx, que está no disco do servidor, provavelmente no caminho c:\inetpub\wwwroot\app\Categorias.aspx (assumindo o caminho padrão do IIS). Esta página de categoria poderia entregar todas as categorias do banco de dados Northwind, listadas de alguma forma. Para editar uma categoria específica, chamaríamos algo como http://servidorweb/app/CategoriaEditar.aspx?ID=3, assumindo que queremos a categoria de número 3.
O Routing é um mecanismo criado para facilitar o roteamento de uma aplicação Web. O exemplo citado anteriormente funciona perfeitamente, mas seria mais interessante se, em vez de utilizarmos aqueles endereços, com o identificador número 3, utilizássemos endereços que dissessem mais sobre o que estão exibindo. Neste caso, como a categoria número 3 do banco Northwind se refere à categoria Confections, a seguinte URL deixaria muito mais claro o que está sendo editado: http://servidorweb/app/Categoria/Confections/Editar. Para exibir a categoria, poderíamos utilizar simplesmente http://servidorweb/app/Categoria/Confections. Esse tipo de construção deixa a URL mais fácil de ser lida e também indexada por buscadores.
Como o Routing funciona?
Visto de fora, como uma caixa preta, o Routing tem um funcionamento muito claro. Vamos analisar sua interação com o MVC, e a partir dela derivar como faríamos para utilizá-lo com Web Forms.
O Routing utiliza um módulo HTTP .NET para receber as requisições. Dessa forma, a seguinte linha é acrescentada ao Web.config, como filha da tag:
Com isso, todos os requests passam pelo módulo UrlRoutingModule, que direciona as chamadas ao engine de roteamento.
A partir daí, a rota é configurada. Isso é feito no arquivo global.asax, na função Application_Start. O template padrão do MVC traz uma sugestão de registro de rotas no arquivo global.asax, conforme a Listagem 1.
Public Class GlobalApplication
Inherits System.Web.HttpApplication
Shared Sub RegisterRoutes(ByVal routes As RouteCollection)
routes.Add(New Route("{controller}/{action}/{id}", New MvcRouteHandler) With { _
.Defaults = New RouteValueDictionary(New With {.action = "Index", .id = ""}) _
})
routes.Add(New Route("default.aspx", New MvcRouteHandler) With { _
.Defaults = New RouteValueDictionary(New With {.controller = "Home", .action = "Index", .id = ""}) _
})
end sub
sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)
RegisterRoutes(RouteTable.Routes)
end sub
End Class
O registro é feito adicionando novas rotas à coleção de rotas localizada na propriedade estática Routes da classe RouteTables. Cada rota recebe como parâmetro um padrão de URL, como uma string, e um endereçador de rotas, que é uma classe que implementa System.Web.Routing.IRouteHandler. O padrão de URL do MVC está dividido em três sub-parâmetros, identificados entre chaves, conforme visto no global.asax da Listagem 1: controller, action e id. É com estes parâmetros que informamos ao engine de roteamento do MVC que queremos que ele chame uma função (action) de uma classe controladora (controller), e passe um parâmetro (id). Dessa forma, uma chamada à http://servidorweb/app/categorias/editar/3 atribuirá a palavra categorias ao parâmetro controller, editar ao parâmetro action e 3 ao parâmetro id, chamando então na classe CategoriasController a função Editar, passando o valor 3 ao parâmetro id desta função.
O responsável por fazer a ligação entre a URL da rota à classe e o método a serem chamados, além de qual parâmetro passar, é o endereçador de rotas, ou seja, a classe MvcRouteHandler, e ele faz isso com Reflection.
Também é possível especificar valores padrão. Ou seja, valores para os três parâmetros especificados que, caso o parâmetro não seja passado via URL, será passado então em seu lugar. No código padrão, vemos na linha 5 da Listagem 1 que a ação padrão é Index, e id vazio (note o uso de tipos anônimos). Assim, para chamar a função Index, em uma classe controladora CategoriasController, sem o parâmetro id (ou com ele vazio), podemos chamar simplesmente http://servidorweb/app/categorias. Podemos mudar a ordem dos parâmetros na rota, simplesmente mudando o parâmetro de URL da rota. Se, em vez do valor passado na linha 4 da Listagem 1 para a URL, passássemos {action}/{controller}/{id}, a URL de edição de uma categoria poderia ser chamada assim: http://servidorweb/app/editar/categorias/3.
As rotas são buscadas na ordem em que são registradas, dessa forma, se você registrar as duas opções de rota, a original, e em seguida a com URL {action}/{controller}/{id}, você nunca verá a segunda rota executar, já que elas atendem ao mesmo formato de URL (três variáveis separadas por barra). Para isso, você deve criar uma rota singular, algo como acoes/{action}/{controller}/{id}, e poderia então chamar o caminho de edição assim: http://servidorweb/app/acoes/editar/categorias/3 e ainda chamar o caminho padrão.
Agora que entendemos como o Routing funciona com o MVC fica mais fácil traduzir esta implementação para a do Web Forms. Como vimos, quem traduz a URL para ações, controles e parâmetros é o endereçador de Rotas MvcRouteHandler. Esta classe, passada no construtor da Rota, implementa a interface IRouteHandler, do namespace System.Web.Routing, que tem, como única operação o método GetHttpHandler, que retorna um objeto que implemente System.Web.IHttpHandler: uma página (System.Web.Page), por exemplo.
A Figura 1 mostra claramente como isso acontece. Criei um endereçador de Rotas para Web Forms (ele será visto em seguida), e coloquei um breakpoint na declaração da chamada de GetHttpHandler. Ao solicitar uma rota (no meu caso, http://localhost:51408/Categories) este ponto foi atingido. Note na pilha de chamadas (Call Stack) a progressão de chamadas. Coloquei 2 pontos importantes nesta pilha, além da própria parada do breakpoint. A pilha deve ser lida de baixo para cima, dessa forma, no ponto 1 temos a passagem da chamada do Web Server para o runtime do ASP.NET. No ponto 2 temos a passagem do runtime ao módulo UrlRoutingModule, que registramos no web.config. E finalmente temos o breakpoint atingido. Ele será responsável por resolver a rota e entregar uma página. No caso do MVC, isso é feito com controllers e ações. No nosso caso, precisamos simplesmente criar uma página Web comum e devolvê-la. Vamos ver como fazer isso em um caso bastante simples.
Entregando páginas simples com o Routing
Existe uma maneira muito simples de entregar Web Forms via rotas alternativas com o Routing. Basta criar um gestor de rotas que saiba exatamente qual o caminho da página que deve entregar. O código deste gestor está disponível na Listagem 2. Note que ele solicita este caminho no construtor e o armazena em uma variável. Quando o método GetHttpHandler é solicitado, ele cria uma instância da página com auxílio da classe BuildManager e seu método estático CreateInstanceFromVirtualPath, que recebe como parâmetro o caminho virtual da página a ser criada. O objeto criado (a página) é então retornado. O caminho virtual passado poder ser o caminho de qualquer página aspx, como por exemplo ~/default.aspx.
Imports System.Web.Routing
Imports System.Web
Public Class GestorDeWebFormsRoteadosMuitoSimples
Implements IRouteHandler
Public Sub New(ByVal caminhoVirtual As String)
'armazenamos o caminho real da rota
_CaminhoVirtual = caminhoVirtual
End Sub
'variável modal para armazenar o caminho real da rota
Private ReadOnly _CaminhoVirtual As String
Public Function GetHttpHandler(ByVal requestContext As RequestContext) _
As IHttpHandler Implements IRouteHandler.GetHttpHandler
'criamos uma página a partir do caminho e retornamos
Dim pagina = DirectCast( _
System.Web.Compilation.BuildManager.CreateInstanceFromVirtualPath( _
_CaminhoVirtual, _
GetType(Page)), _
Page)
Return pagina
End Function
End Class
Crie um diretório chamado “Roteado”. Vamos colocar todas as páginas aspx roteadas lá. Então crie neste diretório a página Pedidos.aspx, e escreva algo para identificá-la. No global.asax, inclua a rota, conforme mostra a Listagem 3, nomeando-a de RotaDePedido. Ao rodar, chame a rota, ou seja, o nome do servidor, mais /pedidos. O resultado deve ser semelhante à Figura 2.
Imports System.Web.SessionState
Public Class Global_asax
Inherits System.Web.HttpApplication
Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)
RegistraRotas(Routing.RouteTable.Routes)
End Sub
Private Sub RegistraRotas(ByVal rotas As Routing.RouteCollection)
rotas.Add("RotaDePedido", _
New Routing.Route("pedidos", _
New GestorDeWebFormsRoteadosMuitoSimples("~/roteado/pedidos.aspx")))
End Sub
End Class
Entregando rotas parametrizadas
Da forma com que a rota foi configurada apenas a chamada à URL ~/pedidos será atendida. Note que não usamos parâmetros, como é feito no MVC: não há em nossa rota nada entre chaves, sendo ela apenas pedidos (no MVC o padrão é {controller}/{action}/{id}). Por esse motivo, se quisermos que alguma rota atenda ~/clientes, temos que criar uma rota para ela. Vamos fazer isso, passando um id como parâmetro. No entanto, o endereçador de rotas criado, como dizia seu nome, era muito simples. Para passarmos um parâmetro utilizando a estrutura do framework de Routing, seria interessante se pudéssemos fazer algo como chamar http://localhost:51408/Cliente/3. Para isso, basta adicionar uma rota que atenda esse requisito, que seria cliente/{id}.
Este id não seria recuperado da forma usual da URL, que é via query string. A maneira mais fácil é colocá-lo no contexto da requisição. É isso que está sendo feito no próximo endereçador de rotas, que tem seu método GetHttpHandler disponível na Listagem 4, o GestorDeWebFormsRoteadosSimples (todo o resto é igual à classe GestorDeWebFormsRoteadosMuitoSimples). Note que a única diferença com o endereçador anterior é que estamos agora adicionando os dados de rotas ao contexto (linhas 16 a 19), além de armazenar o contexto do Routing também no contexto HTTP.
É bom notar que esse contexto é do tipo System.Web.Routing.RequestContext, não sendo o mesmo tipo que System.Web.HttpContext (acessado em WebForms pela propriedade Context), ou mesmo o do tipo System.Web.HttpContextBase, que é um novo tipo abstrato, implementado concretamente no tipo System.Web.HttpContextWrapper2, e que envolve (é um wrapper) um objeto do tipo System.Web.HttpContext. Esses dois tipos System.Web.HttpContextBase e System.Web.HttpContextWrapper2 fazem parte do novo componente System.Web.Abstractions. É por esse motivo que armazenamos esse contexto de Routing ao contexto HTTP: ele não estaria disponível nos Web Forms se não o fizéssemos. No artigo seguinte veremos onde utilizá-lo.
Public Function GetHttpHandler(ByVal requestContext As RequestContext) _
As IHttpHandler Implements IRouteHandler.GetHttpHandler
'adicionamos as variáveis de rotas nas chaves de contexto:
For Each key In requestContext.RouteData.Values.Keys
requestContext.HttpContext.Items.Add(key, _
requestContext.RouteData.Values(key))
Next
'adicionamos ao contexto também o requestContext do Routing:
requestContext.HttpContext.Items.Add("contexto", requestContext)
'criamos a página a partir do caminho:
Dim pagina = DirectCast( _
System.Web.Compilation.BuildManager.CreateInstanceFromVirtualPath( _
_CaminhoVirtual, _
GetType(Page)), _
Page)
Return pagina
End Function
Esse novo endereçador de rotas precisa agora ser configurado no global.asax. Inclua o conteúdo da Listagem 5 ao método RegistraRotas. Note que agora temos um parâmetro variável na rota, que é o {id}. Para utilizá-lo, crie a página cliente.aspx no diretório roteado. O código deve ficar como o da Listagem 6, onde um label exibirá o número passado ao id. Como vimos no endereçador de rotas, o id será armazenado no contexto. Para recuperá-lo, basta buscar a coleção de itens, e exibí-lo no label. O código da Listagem 7, que exibe o code-behind da página cliente.aspx, faz exatamente isso. A Figura 3 mostra o resultado. Troque o valor da URL para ver como ele será refletido na página.
rotas.Add("RotaDeCliente", _
New Routing.Route("cliente/{id}", _
New GestorDeWebFormsRoteadosSimples("~/roteado/clientes.aspx")))
<%@ Page Language="vb" AutoEventWireup="false" CodeBehind="clientes.aspx.vb" Inherits="RoutingWebForms2.clientes" %>
Essa é a página de pedidos
Ela está localizada no diretório: Roteado/Clientes.aspx
Você solicitou o cliente número:
Partial Public Class clientes
Inherits System.Web.UI.Page
Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
If Not Page.IsPostBack Then
lblIDCliente.Text = DirectCast(Context.Items("id"), String)
End If
End Sub
End Class
Uma página de soma roteada
Para fazer uma página de soma, fica bastante fácil. Utilizando o mesmo endereçador de rotas simples, adicione a nova rota de soma ao global.asax, crie a página de soma e está pronto. A Listagem 8 mostra esses códigos, e a Figura 4 mostra o resultado.
rotas.Add("RotaDeSoma", _
New Routing.Route("soma/{num1}/{num2}", _
New GestorDeWebFormsRoteadosSimples("~/roteado/soma.aspx")))
<%@ Page Language="vb" AutoEventWireup="false" CodeBehind="soma.aspx.vb" Inherits="RoutingWebForms2.Soma" %>
Resultado da soma:
Public Partial Class Soma
Inherits System.Web.UI.Page
Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) _
Handles Me.Load
If Not Page.IsPostBack Then
lblResultado.Text = CStr(CInt(Context.Items("num1")) + _
CInt(Context.Items("num2")))
End If
End Sub
End Class
Existe ainda a possibilidade de fazer a página de soma pegar todos os parâmetros que se seguirem à rota básica. Pode-se criar um parâmetro pega-tudo (catch all, em inglês). Para isso, o nome do parâmetro deve começar com um asterísco, como soma/{*números}. Nesse caso, todos os parâmetros são passados a uma variável do tipo string, com o caminho completo. Uma chamada à http://localhost:56119/soma/5/4/7/3 traria no parâmetro numeros o valor 5/4/7/3. A partir daí, basta separar os valores e somá-los. Veja essas alterações na Listagem 9.
rotas.Add("RotaDeSoma", _
New Routing.Route("soma/{*numeros}", _
New GestorDeWebFormsRoteadosSimples("~/roteado/soma.aspx")))
Public Partial Class Soma
Inherits System.Web.UI.Page
Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) _
Handles Me.Load
If Not Page.IsPostBack Then
Dim res As Integer
Dim numeros = TryCast(Context.Items("numeros"), String)
If numeros IsNot Nothing Then
For Each num In numeros.Split("/"c)
res += CInt(num)
Next
End If
lblResultado.Text = res.ToString()
End If
End Sub
End Class
Uma aplicação com dados
Vamos criar uma aplicação com dados e Roteamento e que vai utilizar o banco padrão Northwind e sua tabela Categories. Utilizaremos rotas parametrizadas, e por padrão o nome das páginas derivará do nome da rota e da ação. Ao chamar http://servidorweb/app/categories, a chamada deve ser roteada para a página CategoriesListar.aspx, ou seja, como não foi especificada uma categoria específica, será exibibida uma lista com todas as categorias. Ao chamar http://servidorweb/app/categories/Beverages, a categoria Beverages deverá ser exibida, com utilização da página CategoriesEditar.aspx, e ao chamar http://servidorweb/app/categories/Beverages/editar, deve-se ser capaz de editar o registro na mesma página CategoriesEditar.aspx. A Tabela 1 resume esse padrão.
Caminho | Página | Ação |
---|---|---|
http://servidorweb/app/tabela | TabelaListar.aspx | Listar os registros |
http://servidorweb/app/tabela/registro | TabelaEditar.aspx | Consultar um registro |
http://servidorweb/app/tabela/registro/editar | TabelaEditar.aspx | Editar um registro |
Duas rotas podem ser abstraídas desta tabela. A URL da primeira seria somente de consulta: {tabela}, e a URL da segunda permitiria a edição e a consulta: {tabela}/{nome}/{operacao}, sendo que, se a operação não for informada, deve ser consultar.
O endereçador de rotas precisa agora ser mais inteligente. Ele precisa entender esse padrão, da mesma forma que o endereçador do MVC entende os parâmetros {controller} e {action}. Este gestor de Web Forms está na Listagem 10. Note que ele é muito parecido com o gestor que utilizamos nos exemplos anteriores, tendo apenas o código adicional para trabalhar os parâmetros de tabela e nome (o parâmetro operação deverá ser tratado pela própria página).
Imports System.Web.Routing
Imports System.Web
Public Class GestorDeWebFormsRoteados
Implements IRouteHandler
Public Function GetHttpHandler(ByVal requestContext As RequestContext) _
As IHttpHandler Implements IRouteHandler.GetHttpHandler
'adicionamos as chaves de roteamento às chaves de contexto web:
For Each key In requestContext.RouteData.Values.Keys
requestContext.HttpContext.Items.Add(key, _
requestContext.RouteData.Values(key))
Next
'adicionamos o contexto de roteamento ao contexto web:
requestContext.HttpContext.Items.Add("contexto", requestContext)
'cria o caminho virtual, no nosso caso, as chaves "tabela" e "sufixo"
'vão permitir criar um endereço web:
Dim strTabela = TryCast(requestContext.RouteData.Values("tabela"), String)
Dim strSufixoPagina = TryCast(requestContext.RouteData.Values("sufixo"), String)
If String.IsNullOrEmpty(strTabela) Then
Throw New ArgumentException("Tabela não foi passada.")
End If
'As webs roteadas vão ficar no sub diretório "Roteado", e o nome da página aspx
'será o nome da tabela mais o sufixo. Assim, uma chamada de listagem
'de categorias ficaria assim:
' "~/Roteado/CategoriesListar.aspx"
Dim strCaminhoVirtual = "~/Roteado/" & strTabela & strSufixoPagina & ".aspx"
'guardamos o nome da página chamada em outra variável de contexto:
requestContext.HttpContext.Items.Add("caminhovirtual", strCaminhoVirtual)
'criamos a página com auxílio do BuildManager do ASP.NET e retornamos:
Dim pagina = DirectCast( _
System.Web.Compilation.BuildManager.CreateInstanceFromVirtualPath( _
strCaminhoVirtual, _
GetType(Page)), _
Page)
Return pagina
End Function
End Class
Para registrar as rotas no global.asax o procedimento é exatamente o mesmo. Utilizaremos agora um recurso adicional ao registro de rotas: valores padrão. Vimos que o novo endereçador de rotas está esperando uma variável sufixo, que utilizará para compor o nome da página (é ele quem fará a diferença entre a página CategoriesListar, e CategoriesExibir). Esse valor não vem da URL, de forma que precisamos ser capazes de passá-lo de alguma forma ao gestor de roteamento. Isso é feito criando valores padrão para esse tipo de variável, que são passados na criação da rota, com o auxílio de tipos anônimos. Veja as 2 rotas adicionais na Listagem 11. Para a segunda rota estamos ainda adicionando um padrão para a operação, o valor consultar, para que o usuário não precise digitar http://servidorweb/app/categories/Beverages/consultar, podendo digitar somente http://servidorweb/app/categories/Beverages, sendo o valor consultar assumido como padrão.
rotas.Add("Listagem", _
New Routing.Route("{tabela}", _
New GestorDeWebFormsRoteados()) _
With {.Defaults = New Routing.RouteValueDictionary( _
New With {.sufixo = "listar"})})
rotas.Add("Consulta", _
New Routing.Route("{tabela}/{nome}/{operacao}", _
New GestorDeWebFormsRoteados()) _
With {.Defaults = New Routing.RouteValueDictionary( _
New With {.sufixo = "editar", .operacao = "consultar"})})
Para fazer a ligação com os dados utilizaremos LINQ to SQL. Precisamos, antes de mais nada, criar uma fonte de dados. Adicione um novo arquivo DBML do LINQ to SQL e nomeie-o Northwind.dbml (Figura 5). Arraste do Server Explorer a tabela de Categories, e salve. O resultado deve ser conforme a Figura 6.
Vamos então criar a página de listagem de categorias. Seguindo o padrão, crie a página CategoriesListar.aspx no diretório roteado. Adicione a ela um LinqDataSource e aponte para o DataContext criado pelo arquivo dbml (não esqueça de compilar o projeto antes, ou o LinqDataSource não vai ser capaz de encontrá-lo), seu nome deve ser NorthwindDataContext. Selecione a tabela Categories e clique em Finish.
Adicione então um GridView e aponte para o LinqDataSource1 recém criado. Isso já deve ser suficiente para rodar o teste de listagem de categorias. Adicione na default.aspx um link para categories. Veja na Listagem 12 o código das 2 páginas. Rode a solução e selecione o link Exibir Categorias. A página http://localhost:56119/Categories (com outro número de porta) deve ser solicitada, e a página CategoriesListar.aspx deve ser exibida.
<%@ Page Language="vb" AutoEventWireup="false" CodeBehind="CategoriesListar.aspx.vb"
Inherits="RoutingWebForms2.CategoriesListar" %>
ContextTypeName="RoutingWebForms2.NorthwindDataContext" TableName="Categories">
DataKeyNames="CategoryID" DataSourceID="LinqDataSource1">
InsertVisible="False" ReadOnly="True" SortExpression="CategoryID" />
SortExpression="CategoryName" />
SortExpression="Description" />
<%@ Page Language="vb" AutoEventWireup="false" CodeBehind="Default.aspx.vb" Inherits="RoutingWebForms2._Default" %>
Exibir Categorias
Conclusão
Vimos como criar uma aplicação com roteamento customizado baseado no novo componente de Routing do ASP.Net. Vimos como ele pode nos auxiliar a criar rotas dinâmicas e demos os primeiros passos na criação de uma aplicação baseada em dados. No próximo artigo concluiremos a aplicação, veremos como solucionar os problemas que o roteamento traz ao lidarmos com links gerados dinamicamente, como implementar segurança com o Routing e Web Forms, como criar links automaticamente, e como alterar as rotas sem quebrar os links da aplicação.