ASP.NET Routing - Parte 1

Continuamos neste artigo o assunto ASP.NET Routing com Web Forms. Esse importante componente, que ainda está em construção, tem sido visto acompanhado do ASP.NET MVC, mas é independente dele. No último artigo vimos o que é o ASP.NET Routing, como utilizá-lo com Web Forms, criamos alguns gestores de rotas customizados e iniciamos uma aplicação com dados. Neste artigo iremos concluir a aplicação, exibir informações com base nas informações de rota (URL), vamos ver como aplicar segurança, como criar os links de roteamento automaticamente e como alterar rotas sem criar problemas à aplicação.

Relembrando a aplicação

Se você se lembra bem da aplicação do artigo anterior pode pular esta seção, onde mostrarei resumidamente o que foi feito para criar uma aplicação com roteamento baseado no ASP.NET Routing.

Baixe o Routing, instale-o. Crie um projeto web e referencie os componentes do Routing, que são as dlls System.Web.Routing e System.Web.Abstractions. Elas devem estar no diretório C:\Program Files\Microsoft ASP.NET MVC Preview 2\Assemblies. Modifique o web.config para incluir a seguinte tag () como filha da tag.

Crie o endereçador de rotas, que é uma classe comum, com o nome GestorDeWebFormsRoteados. Esta classe será responsável por traduzir as solicitações de rotas para Web Forms. O código está na Listagem 1, que está comentado para explicar o que faz cada chamada.

Adicione o arquivo global.asax e configure suas rotas, conforme a Listagem 2. Você utilizará o padrão das rotas, assim como o gestor de roteamento criado no passo anterior.

Adicione ao projeto uma fonte de dados LINQ to SQL com o nome de Northwind.dbml e adicione a ela, a partir do Server Explorer, a tabela Categories.

Crie na aplicação um novo diretório chamado “roteado”, e debaixo dele crie um novo Web Form chamado CategoriesListar.aspx. Seu código está na Listagem 3, depois adicione um link ao arquivo default.aspx para o diretório Categories “Exibir Categorias ”. As listagens estão comentadas para o melhor entendimento.

Listagem 1. Gestor de web forms roteado.

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 subdiretó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
Listagem 2. Arquivo global.asax.

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 GestorDeWebFormsRoteadosSimples("~/roteado/pedidos.aspx")))

        rotas.Add("RotaDeCliente", _

            New Routing.Route("cliente/{id}", _

                New GestorDeWebFormsRoteadosSimples("~/roteado/clientes.aspx")))

        rotas.Add("RotaDeSoma", _

            New Routing.Route("soma/{*numeros}", _

                New GestorDeWebFormsRoteadosSimples("~/roteado/soma.aspx")))

        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"})})

    End Sub

End Class
Listagem 3. Página roteada/CategoriesListar.aspx.

<%@ Page Language="vb" AutoEventWireup="false" CodeBehind="CategoriesListar.aspx.vb" Inherits="RoutingWebForms2.CategoriesListar" %>
    </www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
    </www.w3.org/1999/xhtml" >
   
      <ContextTypeName="RoutingWebForms2.NorthwindDataContext" TableName="Categories">
      <DataKeyNames="CategoryID" DataSourceID="LinqDataSource1">

          <InsertVisible="False" ReadOnly="True" SortExpression="CategoryID" />
          <SortExpression="CategoryName" />
          <SortExpression="Description" />

Utilizaremos também um gestor de roteamento com Web Forms simples, que não permitirá configuração, apenas entregará uma página pré-configurada, chamada GestorDeWebFormsRoteadosSimples. O código desta classe está na Listagem 4.

Listagem 4. Gestor de roteamento simples.

Imports System.Web.Routing

Imports System.Web

Public Class GestorDeWebFormsRoteadosSimples

    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

        '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

End Class

Você já deve ser capaz de rodar o projeto. Clique no link Exibir Categorias da página default.aspx e deverá ver a página CategoriesListar.aspx sendo exibida. O padrão de roteamento está exposto na Tabela 1.

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
Tabela 1. Padrão de roteamento.

Exibindo dados com base na rota

Vamos criar então a página de consulta e alteração de categorias. Mais uma vez seguindo o padrão de roteamento, crie a página CategoriesEditar.aspx. Adicione um LinqDataSource, configure-o, mas desta vez inclua um parâmetro no where, filtrando por CategoryName, com operador do tipo igual, e fonte como none e clique em Add (veja Figura 1). Isso permitirá selecionar o registro de acordo com o nome. Adicione então um detailsview e aponte sua fonte de dados para o LinqDataSource1.

Configuração do LinqDataSource para receber um parâmetro
Figura 1. Configuração do LinqDataSource para receber um parâmetro.

Configure o LinqDataSource para trabalhar com atualizações, habilitando o checkbox Enable Update. Permita também que o detailsview faça a atualização habilitando o seu checkbox Enable Editing. A página deve ficar conforme a Listagem 5.

Listagem 5. Página CategoriesEditar.aspx.

<%@ Page Language="vb" AutoEventWireup="false" CodeBehind="CategoriesEditar.aspx.vb" Inherits="RoutingWebForms3.CategoriesEditar" %>
    <"/www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
    <"/www.w3.org/1999/xhtml" >
   


          <ContextTypeName="RoutingWebForms3.NorthwindDataContext" TableName="Categories"
              Where="CategoryName == @CategoryName" EnableUpdate="True">  
             
                
          <DataKeyNames="CategoryID" DataSourceID="LinqDataSource1" Height="50px"
              Width="125px">
             
                    <InsertVisible="False" ReadOnly="True" SortExpression="CategoryID" />
                    <SortExpression="CategoryName" />
                    <SortExpression="Description" />

Precisamos agora recuperar o valor do parâmetro a ser passado ao where do LinqDataSource. Isso será recuperado das chaves de rota que foram guardadas no contexto pelo gestor de rotas já criado. A página também está configurada para exibir sempre o detailsview no modo padrão, que é o modo de visualização, mas ela deverá lidar também com edição. Para resolver isso, recuperaremos o valor da operação do contexto e se seu valor for editar, devemos configurar o detailsview para modo de edição.

A recuperação dos itens do contexto é algo usual no ASP.NET clássico, e está demonstrada na Listagem 6. Note que o valor do where do LinqDataSource é passado a uma variável modal, que é então utilizado no evento Selecting para que o filtro seja aplicado. O evento Updated também é endereçado porque, caso o nome de uma categoria mude, a URL não será mais válida, e a página será então redirecionada ao novo nome, retirando-a do modo de edição. A página de edição já deve funcionar. Crie dois novos links na página default.aspx “Consultar Categoria Beverage ” e “Editar Categoria Beverage”.

Listagem 6. Code-behind da Página CategoriesEditar.aspx.

Partial Public Class CategoriesEditar1

    Inherits System.Web.UI.Page

    Private _strNome As String

    Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs)     Handles Me.Load

        If Not Page.IsPostBack Then

            'recuperação dos valores de contexto, colocados aí pelo gestor de web forms roteados

            If DirectCast(Context.Items("operacao"), String).ToLower() = "editar" Then

                DetailsView1.ChangeMode(DetailsViewMode.Edit)

            Else

                DetailsView1.ChangeMode(DetailsViewMode.ReadOnly)

            End If

        End If

        _strNome = TryCast(Context.Items("nome"), String)

        If _strNome Is Nothing Then

            Throw New ArgumentException("Nome não foi passado")

        End If

    End Sub

    Protected Sub LinqDataSource1_Selecting(ByVal sender As Object, ByVal e As System.Web.UI.WebControls.LinqDataSourceSelectEventArgs) Handles LinqDataSource1.Selecting

        'o valor é passado ao LinqDataSource pela sua coleção

        'de parâmetros "where"

        e.WhereParameters("CategoryName") = _strNome

    End Sub

    Private Sub LinqDataSource1_Updated(ByVal sender As Object, ByVal e As System.Web.UI.WebControls.LinqDataSourceStatusEventArgs) Handles LinqDataSource1.Updated

        Dim strNovoNome As String = DirectCast(e.Result, RoutingWebForms1.Category).CategoryName

        'em caso de mudança de nome, redirecionamos a chamada ao nome correto:

        If strNovoNome <> _strNome Then

            Response.Redirect(Request.RawUrl.Replace(_strNome, strNovoNome).Replace("/Editar", ""))

        End If

    End Sub

End Class

Teste a aplicação clicando nos links Consultar Categoria Beverage e Editar Categoria Beverage da página default.aspx. O primeiro deve exibir a página CategoriesEditar.aspx em modo de consulta, e o segundo em modo de edição. Testes as atualizações que também devem funcionar.

Iremos agora ligar a página de listagem (CategoriesListar.aspx) à página de edição que acabamos de criar. Vamos editar o GridView da página CategoriesListar.aspx para incluir links para exibição e para edição.

Vamos incluir um HyperLinkField ao GridView. Selecione-o e clique em Edit Columns, adicione dois HyperLinkFields e configure-os conforme a Tabela 2. O GridView deve ficar conforme a Listagem 7. Ao rodar a aplicação, você já será capaz de navegar livremente entre as rotas de listagem e de alteração e consulta.

Controle Propriedade Valor
Consulta e Edição DataNavigateUrlFields CategoryName
Consulta DataNavigateUrlFormatString ~/Categories/{0}
Edição DataNavigateUrlFormatString ~/Categories/{0}/Editar
Consulta HeaderText e Text Consultar
Edição HeaderText e Text Editar
Tabela 2. Propriedades dos controles HyperLinkField da página CategoriesListar.aspx.
Listagem 6. Code-behind da Página CategoriesEditar.aspx.

<DataKeyNames="CategoryID" DataSourceID="LinqDataSource1">
       
    <InsertVisible="False" ReadOnly="True" SortExpression="CategoryID" />


    <SortExpression="CategoryName" />


    <SortExpression="Description" />


    <DataNavigateUrlFormatString="~/Categories/{0}" HeaderText="Consultar"
    Text="Consultar" />


    <DataNavigateUrlFormatString="~/Categories/{0}/Editar" HeaderText="Editar"
    Text="Editar" />

Criando links de rota automaticamente

É possível solicitar ao Framework de roteamento que nos entregue os links de roteamento automaticamente. Precisamos apenas indicar a rota e os valores para cada dado da rota. Em nossa aplicação, os valores que precisam ser especificados são os valores de tabela, nome, sufixo e operação. A função responsável por entregar esta funcionalidade é a GetVirtualPath, da classe RouteCollection. A classe RouteCollection é, como diz o nome, uma coleção de rotas. Temos acesso a uma coleção de rotas na propriedade Routes da classe RouteTable e essa é uma propriedade Shared (static), ou seja, podemos acessá-la diretamente, e ela sempre trará a coleção de rotas de nossa aplicação (foi o que fizemos para adicionar rotas no global.asax).

Para que essa função retorne uma rota, precisamos passar a ela o contexto de roteamento, o nome da rota e os valores configurados (tabela, nome, sufixo e operação). Ela retornará uma string com a URL já montada de acordo com a rota selecionada. Para facilitar o trabalho, coloquei o método em uma função de extensão (nova funcionalidade do VB9, veja artigo da edição 50), que recebe, além dos parâmetros solicitados pelo método GetVirtualPath, também o nome do link e o contexto HTTP (em vez do contexto de roteamento). O contexto de roteamento será obtido a partir de um item do contexto HTTP (veja o método GetHttpHandler do GestorDeWebFormsRoteados na Listagem 1). Como se trata de um método de extensão, ele pode ser chamado diretamente a partir de um objeto de contexto HTTP. Confira esse módulo de extensão na Listagem 8.

Listagem 8. Módulo de extensão.

Imports System.Runtime.CompilerServices

  _

Public Module UtilitariosRoteamento

   _

    Public Function CriarLinkRoteado(ByVal Contexto As System.Web.HttpContext, _

                                       ByVal strNomeLink As String, _

                                       ByVal strNomeRota As String, _

                                       ByVal values As Object) As String

            Dim ContextoRoteamento = DirectCast(Contexto.Items("contexto"),  _

              Routing.RequestContext)

            Dim pathInfo = Routing.RouteTable.Routes.GetVirtualPath( _

              ContextoRoteamento, _

              strNomeRota, _

              New Routing.RouteValueDictionary(values))

            Return String.Format("{1}", _

                             pathInfo.VirtualPath, strNomeLink)

    End Function

End Module

Na Listagem 9 está o código da página default.aspx. Para utilizar o método CriarLinkRoteado da Listagem 8 adicione, logo abaixo dos links já criados na página default.aspx, as chamadas de criação de links. Precisamos adicionar também uma diretiva de Imports na página default.aspx, logo abaixo da diretiva de página. O código assume que seremos capazes de extrair o contexto de roteamento do contexto HTTP, algo feito pelos gestores de Web Forms roteados.

Como a página default.aspx não está roteada, ela vai nos causar problemas ao rodar com esse método de extensão. Para resolver isso, basta rotear esta página também. Vamos utilizar o gestor de rotas simples para realizar essa tarefa. Adicione ao global.asax o código da Listagem 10, e a página default.aspx passará a ser roteada, e a ter guardado em seu contexto HTTP o contexto de roteamento, o que permitirá criar automaticamente o link.

Listagem 9. Página default.aspx se utilizando do método de extensão do contexto HTTP para criar os links.

 <%@ Page Language="vb" AutoEventWireup="false" CodeBehind="Default.aspx.vb" Inherits="RoutingWebForms3._Default" %>
    <%@ Import Namespace="RoutingWebForms3" %>
    <"/www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
    <"/www.w3.org/1999/xhtml" >
   
       

            <Links Hard-coded:>

            <Exibir Categorias>

            <Consultar Categoria Beverage>

            <Editar Categoria Beverage>

            <Links criados pela infra-estrutura de roteamento:>
     
            <% =Context.CriarLinkRoteado("Exibir Categorias", "Listagem", New With {.tabela = "Categories"})%>

            <% =Context.CriarLinkRoteado("Consultar Categoria Beverage", "Consulta", New With {.tabela = "Categories", .nome = "Beverages"})%>

            <% =Context.CriarLinkRoteado("Editar Categoria Beverage", "Consulta", New With {.tabela = "Categories", .nome = "Beverages", .operacao = "editar"})%>
Listagem 10. Novo roteamento da página default.aspx no global.asax.

rotas.Add("PaginaDefault", _

    New Routing.Route("default.aspx", _

        New GestorDeWebFormsRoteadosSimples("~/default.aspx")))

Rode o aplicativo e verá os dois grupos de links: Os criados “hard-coded” e os criados pela infra-estrutura de roteamento, que devem ser idênticos. Veremos em seguida que os links “hard-coded” serão um problema caso alteremos o formato das rotas.

Alterando rotas

Já sabemos que o Framework de Routing permite estruturar as rotas como bem quisermos: elas podem ser fixas, como foi a rota default.aspx (que direciona para a própria default.aspx), ou com parâmetros, como as rotas de Web Forms, que recebem o nome da tabela, da operação e do nome do item.

No global.asax estruturamos as rotas parametrizadas da seguinte forma:

  1. {tabela}, que roteia para uma página em ~/roteado/tabelaListar.aspx, e tem o objetivo de listar itens de uma tabela;
  2. {tabela}/{nome}/{operacao}, que roteia para uma página em ~/roteado/tabelaeditar.aspx, e tem por objetivo exibir ou editar uma determinada linha de uma tabela.

E se quiséssemos, no segundo caso, passar a tratar a rota no formato {tabela}/{operacao}/{nome}? A URL de edição na tabela categories ficaria então assim: /categories/editar/beverages. Fazer isso é muito fácil. Altere a linha do global.asax que contém a rota Consulta (ver Listagem 2) para refletir justamente esse novo formato: {tabela}/{operacao}/{nome}.

Teste agora a aplicação, navegando até a página default.aspx. Veja que, o grupo em que os links foram escritos hard-coded continuam os mesmos, e por isso eles serão um problema. O que aconteceu foi o seguinte:

  1. O primeiro link, Exibir categorias, ainda funciona, já que se refere à rota Listagem;
  2. O segundo link Consultar Categoria Beverage, está apontando para Categories/Beverages e ao clicá-lo receberemos um erro 404, já que ele não é atendido mais por nenhuma rota. Isso aconteceu porque não há uma rota que recupere dois grupos de strings, algo como {tabela}/{operacao}, ou {tabela}/{nome}. Antes funcionava porque a rota era {tabela}/{nome}/{operacao} e o parâmetro {operacao} possui um valor default de consultar, o que o tornava opcional;
  3. O terceiro link, Editar Categoria Beverage, está apontando para Categories/Beverages/editar. Ao clicá-lo não recebemos um erro 404. Quem atendeu essa requisição foi a página CategoriesEditar.aspx. Isso aconteceu porque a rota {tabela}/{operacao}/{nome} funcionou para esse formato de URL, mas como bateu Beverages com o parâmetro {operacao} e editar com o parâmetro {nome}, a página tentou fazer uma exibição da categoria editar, que não existe na base de dados.

O mesmo não aconteceu com o grupo de links gerados dinamicamente no passo anterior, criados com uso da função de extensão CriarLinkRoteado da Listagem 8. Estes links estão refletindo a mudança nas rotas. O primeiro link não teve a rota alterada, já que é atendido por outra rota, mas o segundo e o terceiro agora apontam para /Categories/consultar/Beverages e /Categories/editar/Beverages, respectivamente. Clique-os e confirme que as rotas funcionam perfeitamente.

Temos ainda outro problema a corrigir. A página CategoriesListar.aspx também está escrevendo os links sem consultar o Framework de roteamento. Isso quer dizer que ao clicar em Exibir Categorias, teremos a listagem conforme o esperado (veja Figura 2), mas os links Consultar e Editar apontam para rotas que não existem mais, ou Categories/Beverages e Categories/Beverages/Editar, respectivamente. Temos que resolver isso, já que esses links têm o mesmo problema apontado nos links hard-coded da página default.aspx: apontam para rotas que não existem mais.

Para isso vamos transformar os HyperLinkFields em Template Fields. Para isso clique sobre o GridView e selecione Edit Columns no menu de contexto, selecione o HyperLinkField Consultar, responsável por um dos links, e clique em Convert this field into a TemplateField, conforme exibido pela Figura 3. Faça o mesmo para o campo Editar. Essa ação cria um campo do tipo TemplateField, com todas as características do HyperLinkField, sem perda de funcionalidades. É gerado um HyperLink como controle do campo TemplateField, conforme exibido na Listagem 11, que exibe como ficou o código do GridView.

Página de listagem de categorias
Figura 2. Página de listagem de categorias.
Transformando o HyperLinkField em TemplateField
Figura 3. Transformando o HyperLinkField em TemplateField.
Listagem 11. GridView após a transformação do HyperLinkField em TemplateField.

<DataKeyNames="CategoryID" DataSourceID="LinqDataSource1">
       
    <InsertVisible="False" ReadOnly="True" SortExpression="CategoryID" />
    <SortExpression="CategoryName" />
    <SortExpression="Description" />

        <NavigateUrl='<%# Eval("CategoryName", "~/Categories/{0}") %>' Text="Consultar">

        <NavigateUrl='<%# Eval("CategoryName", "~/Categories/{0}/Editar") %>'

<Text="Editar">

Faremos a substituição dos HyperLinks gerados por chamadas ao método CriarLinkRoteado, da mesma forma com que foi feito com a página default.aspx. Da mesma forma com que fizemos nesta página, não deixe de adicionar a diretiva de Imports logo abaixo da diretiva de página (veja Listagem 9 na página default.aspx). O nome da tabela será passado no parâmetro {nome} da montagem do link. A operação vai variar de acordo com o link, se for o link de consulta, a operação será consulta, e se for de edição, será editar. Veja na Listagem 12 como estes dois Template Fields devem ficar.

Listagem 12. TemplateFields com chamada ao método CriarLinkRoteado.

<%#Context.CriarLinkRoteado("Consultar", "Consulta", New With {.tabela = "Categories", .nome = Eval("CategoryName", "{0}"), .operacao = "consultar", .sufixo = "editar"})%>
   
       
<%#Context.CriarLinkRoteado("Editar", "Consulta", New With {.tabela = "Categories", .nome = Eval("CategoryName", "{0}"), .operacao = "editar", .sufixo = "editar"})%>

Note que o parâmetro sufixo também é passado, mesmo que seja um valor editar, algo que não foi feito na página default.aspx. Isso acontece porque, mesmo que o valor editar seja o padrão, a rota que está atendendo a requisição é uma rota de listagem (a rota Listagem que possui URL {tabela}, atendendo uma chamada de /Categories), que já possui o valor listar para o campo sufixo, então temos que configurá-lo novamente, ou o valor do contexto atual listar será passado ao parâmetro de sufixo.

Teste a aplicação novamente, e perceba que os links consultar e editar de cada item da listagem de categorias apontam corretamente para as rotas de consulta e edição e que ao serem clicados funcionam perfeitamente. Volte em seguida a rota ao padrão {tabela}/{nome}/{operacao} para prosseguirmos no assunto segurança.

Segurança com Routing

As configurações padrão de autorização não se aplicam conforme esperaríamos quando usamos o routing. Isso quer dizer que, caso exista uma restrição de segurança a uma página, a restrição não será aplicada à rota derivada. Se, por exemplo, impusermos uma restrição de acesso que proíba todos os usuários de acessar a página ~/roteado/categorieslistar.aspx, ainda assim será possível chamar a rota ~/categories sem problemas, já que o bloqueio é baseado na URL, e elas são diferentes. Para resolver o problema poderíamos simplesmente adicionar o bloqueio também à URL ~/categories. Isso teria o efeito, talvez não esperado, de bloquear também qualquer rota derivada, como ~/categories/beverages.

Apesar do bloqueio duplo à URL (a página e a rota, veja exemplo simplificado na Listagem 13), este traz o problema de duplicidade, algo complicado e caro de manter. O ideal seria bloquear somente a página e esperar que a rota fosse automaticamente bloqueada. Para resolver esse problema vamos criar um novo gestor de rotas.

Listagem 12. TemplateFields com chamada ao método CriarLinkRoteado.

 <CONFIGURATION>      
 	<LOCATION path="Categories">        
 		<SYSTEM.WEB>          
 		<AUTHORIZATION>            
 			<DENY users="*" />          
 		</AUTHORIZATION>        
 	</SYSTEM.WEB>      
 </LOCATION>      
 <LOCATION path="roteado/CategoriesListar.aspx">        
 	<SYSTEM.WEB>          
 	<AUTHORIZATION>            
 		<DENY users="*" />          
 	</AUTHORIZATION>        
 </SYSTEM.WEB>      
</LOCATION>   
</CONFIGURATION>

Desta vez não precisaremos criar o gestor de rotas do zero. Vamos utilizar o padrão Decorator (veja a edição 48 onde falei sobre este padrão) para basear a criação de um novo gestor de rotas capaz de decorar a classe GestorDeWebFormsRoteados. Como diz o padrão, o tipo de base deve ser o mesmo, e é justamente o que buscamos: como ele será um gestor de rotas precisa implementar IRouteHandler, e esse será o tipo de base.

Ele receberá no construtor uma referência ao tipo decorado, ou seja, outro gestor de rotas que implemente IRouteHandler assim como ele. E como implementará IRouteHandler, seu único trabalho será trabalhar o método de entrega de um IHttpHandler (uma página, essencialmente) através do método GetHttpHandler, chamar este mesmo método no tipo decorado passado no construtor e adicionar a segurança.

A adição da segurança será implementada com auxílio do método estático CheckUrlAccessForPrincipal da classe System.Web.Security.UrlAuthorizationModule, que recebe o caminho avaliado e retorna um booleano que informa se a página está liberada ou não. Para obter o usuário utilizaremos o contexto HTTP e sua propriedade user. Já o contexto HTTP é obtido através da propriedade HttpContext do objeto requestContext (do tipo System.Web.Routing.RequestContext) passado como parâmetro do método. O caminho será obtido a partir do contexto HTTP, uma vez que ele foi salvo no contexto pela classe GestorDeWebFormsRoteados. Isso traz mais acoplamento do que o padrão exigiria, mas como estamos avaliando o roteamento e não o padrão Decorator vamos ignorar o fato da classe decoradora conhecer a classe decorada assim tão bem (na teoria, tenha em mente que tudo o que e a classe decoradora precisa saber sobre a decorada é que elas compartilham a mesma interface). Esse código todo fica bem simples com a utilização do padrão Decorator, e está disponível na Listagem 14.

Listagem 14. Classe gestora de rotas segura baseada no padrão decorator.

Imports System.Web.Routing

   Imports System.Web

   Public Class GestorDeWebFormsRoteadosSeguro

       Implements Routing.IRouteHandler

    

        Public Sub New(ByVal innerRouteHandler As GestorDeWebFormsRoteados)

            'vamos usar o padrão decorator para decorar classes gestoras de rotas

            'para isso guardamos o objeto decorado em uma variável de escopo modal:

            _innerGestorDeWebFormsRoteados = innerRouteHandler

       End Sub

    

        Private _innerGestorDeWebFormsRoteados As GestorDeWebFormsRoteados

    

        Public Function GetHttpHandler(ByVal requestContext As RequestContext) As IHttpHandler _

        Implements IRouteHandler.GetHttpHandler

            'obtemos a página a partir da variável de escopo modal

            'passada no construtor:

            Dim pagina As IHttpHandler = _innerGestorDeWebFormsRoteados.GetHttpHandler(requestContext)

            'o objeto retornado é um IHttpHandler, e por isso, não temos como saber sua URL

            If pagina IsNot Nothing Then

                Dim strCaminhoVirtual = DirectCast(requestContext.HttpContext.Items("caminhovirtual"), String)

                If UrlAuthorizationModule.CheckUrlAccessForPrincipal(strCaminhoVirtual, _

                        requestContext.HttpContext.User, _

                        requestContext.HttpContext.Request.HttpMethod) = False Then

                    Throw New System.Web.HttpException(401, "Não possui acesso à url.")

                End If

            End If

            Return pagina

    

        End Function

    End Class

Criaremos novas rotas seguras que utilizarão este novo gestor de rotas. As rotas devem ser colocadas obrigatoriamente antes das rotas padrão, conforme visto na Listagem 15. As novas rotas aparecem logo após a rota RotaDeSoma e antes da rota Listagem, e se chamarão conforme as rotas não segura, adicionando-se apenas a palavra segura ao final, sendo então ListagemSegura e ConsultaSegura.

Listagem 15. Novas rotas seguras no arquivo global.asax.


rotas.Add("ListagemSegura", _

    New Routing.Route("seguro/{tabela}", _

        New GestorDeWebFormsRoteadosSeguro(New GestorDeWebFormsRoteados())) _

        With {.Defaults = New Routing.RouteValueDictionary( _

            New With {.sufixo = "listar"})})

    rotas.Add("ConsultaSegura", _

        New Routing.Route("seguro/{tabela}/{nome}/{operacao}", _

            New GestorDeWebFormsRoteadosSeguro(New GestorDeWebFormsRoteados())) _

            With {.Defaults = New Routing.RouteValueDictionary( _

                New With {.sufixo = "editar", .operacao = "consultar"})})

O motivo das rotas estarem antes das rotas não seguras é o formato. Uma chamada à URL ~/seguro/categories bateria com a rota {tabela}/{nome}/{operacao}, sendo o parâmetro {tabela} a palavra seguro, o parâmetro {nome} a palavra categories e o parâmetro {operacao} seria obtido do valor padrão, ou seja, consultar. Para evitar isso cadastramos as rotas seguras antes das rotas não seguras.

Esse é um conceito importante no roteamento: a primeira rota que bater com uma URL é a que vai atendê-la, na ordem em que foram cadastradas. Na prática, isso quer dizer que não podemos utilizar nenhuma tabela com o nome “seguro”, já que ela bateria com a rota segura. Para evitar isso você poderia usar outro padrão, como {seguro}-{tabela}/{nome}/{operacao}, com o hífen no lugar da barra, o que é perfeitamente legal.

Falta apenas criar os links na página default.aspx. Para isso, copie os links não seguros e cole-os logo abaixo, trocando somente o nome do tipo da rota de Listagem para ListagemSegura e Consulta para ConsultaSegura. Isso pode ser visto na Listagem 16.

Listagem 16. Novos links seguros na página default.aspx. Links seguros criados pela infra-estrutura de roteamento:


<% =Context.CriarLinkRoteado("Exibir Categorias", "ListagemSegura", New With {.tabela = "Categories"})%>

            <% =Context.CriarLinkRoteado("Consultar Categoria Beverage", "ConsultaSegura", New With {.tabela = "Categories", .nome = "Beverages"})%>

            <% =Context.CriarLinkRoteado("Editar Categoria Beverage", "ConsultaSegura", New With {.tabela = "Categories", .nome = "Beverages", .operacao = "editar"})%>
 

Você pode retirar a restrição de autorização do web.config (Listagem 13), já que agora a regra será refletida também na rota. Com isso o aplicativo já deve funcionar. Rode a aplicação e acesse a rota default.aspx e clique no link seguro Exibir Categorias. Você será então roteado para ~/seguro/Categories e terá um erro de segurança. Perceba no Stack Trace do erro que a exceção vem do método GetHttpHandler do gestor de rotas GestorDeWebFormsRoteadosSeguro, que conferiu que a página roteado/CategoriesListar.aspx estava bloqueada para o usuário (para todos na verdade) e bloqueou também a rota. Veja o Stack Trace do erro na Listagem 17.

Listagem 17. Erro gerado pelo erro de acesso na rota


[HttpException (0x80004005): Não possui acesso à url.]

   RoutingWebForms3.GestorDeWebFormsRoteadosSeguro.GetHttpHandler(RequestContext requestContext) in GestorDeWebFormsRoteadosSeguro.vb:25

   System.Web.Routing.UrlRoutingModule.PostResolveRequestCache(HttpContextBase context) +188

   System.Web.Routing.UrlRoutingModule.OnApplicationPostResolveRequestCache(Object sender, EventArgs e) +123   System.Web.SyncEventExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute() +92

   System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously) +64

Um efeito colateral desta mudança é que os links do GridView da rota ~/seguro/categories não aparecem seguros. Recorde-se que os links chamam a rota Consulta (veja a Listagem 12) e não a rota ConsultaSegura. Por esse motivo os links de consulta e edição aparecem sem segurança. Uma forma de remediar esse problema seria associar a segurança a um parâmetro de página, como foi feito com {operacao} e ele seria automaticamente repassado para os próximos links, fica a sugestão de extensão deste trabalho. Existem outras soluções para esse problema, e publicarei no meu blog as idéias interessantes que surgirem e vocês me enviarem.

O novo Framework de roteamento se prova bastante extensível e reutilizável além do contexto do MVC. Vimos neste artigo e no anterior que trabalhar com ele fica bastante fácil uma vez criado algum código de infra-estrutura (os gestores de rotas), que deverá ser desenhado de acordo com cada solução construída. Sem dúvida é uma alternativa interessante às deselegantes URLs com que nos deparamos no dia-a-dia em aplicações Web construídas com ASP.NET atualmente

Confira também