martes, 31 de mayo de 2011

Reescritura de urls y webforms de ASP.NET

Existen distintas formas de reescribir una URL, un buen articulo es el el post en el Blog de Scott Guthrie donde se resumen las formas al que habría que añadir la existencia del modulo url-rewrite para iis7.

En el post de Scott Guthrie se recomienda el uso de modulos ya desarrollados, como el url-rewrite, pero mi experiencia es que estos modulos crean muchos problemas con referencias a estilos, imagenes, scripts.

Estos problemas se agravan cuando se usan webforms ASP.NET como adelanta la sección URL Rewriting for ASP.NET Web Forms en las páginas del modulo url-rewite para iis7.

Mi opinión es que el modulo url-rewrite es útil y lo recomiendocuando tienes que trabajar con código ajeno, pero si estas trabajando con un desarrollo propio y tu código está minimamente estructurado será mas eficiente implantar la reescritura de urls en tu codigo. Eficiente tanto en tiempo de ejecución como de desarrollo.

Recordemos que el problema es usar URL del tipo:
http://www.spaintiles.info/dirbeta/0-0-1-1-1-1-15-1-1-E-1/default.aspx
Cuando sabemos que a nuestras páginas les gusta mas del tipo:
http://www.spaintiles.info/dirbeta/default.aspx?P=0-0-1-1-1-1-15-1-1-E-1

El primer paso que equivale la regla de entrada (inbound rule) del Rewrite-module es transformar la URL primera en la segunda.

Para ello usamos el evento de aplicación beginRequest y el metodo rewritePath del contexto.  Dicho en simple, en global.asax editamos el procedimiento Application_BeginRequest con un código similar al que sigue.
Sub Application_BeginRequest(ByVal sender As Object, ByVal e As EventArgs)
        ' Se desencadena al comienzo de cada solicitud
        Dim pathOriginal = Request.Url.ToString
        Dim rg As New System.Text.RegularExpressions.Regex("(?http\://\w*(\.\w*)*(?:\:\d*)?)(?/\w*/)(?:(?\w-\w(?:-\w*)*))/(?default\.aspx)(?.*)", RegexOptions.IgnoreCase)
        Dim captura = rg.Matches(pathOriginal)
        If captura.Count = 1 Then
            With captura(0)
                Dim pagina = .Groups("app").Value + "/default.aspx"
                If .Groups("queryString").Value.Length = 0 Then
                    pagina += "?P="
                Else
                    pagina += .Groups("queryString").Value + "&P="
                End If
                pagina += .Groups("argumentos").Value
                Context.RewritePath(pagina, False)
            End With
        End If
    End Sub

Este código podría ser mas simple como el ejemplo de
void Application_BeginRequest(object sender, EventArgs e) {

        string fullOrigionalpath = Request.Url.ToString();
        
        if (fullOrigionalpath.Contains("/Products/Books.aspx")) {
            Context.RewritePath("/Products.aspx?Category=Books");
        }
        else if (fullOrigionalpath.Contains("/Products/DVDs.aspx")) {
            Context.RewritePath("/Products.aspx?Category=DVDs");
        }
    }  
Pero a mi me gustan las expresiones regulares y tenerlo todo bien identificado.

Importante:
En mi código de ejemplo establezco a false el parametro rebaseClientpath:
  Context.RewritePath(pagina, False)
Con esto se resuelven muchos de los problemas relacionados con el uso del simbolo ~ en las urls de imagenes y otros controles web para referenciar la raiz de la aplicación.

El siguiente paso es reescribir las url para navegación entre páginas.  Outbound rule o regla de salida en un rewrite-module.
Aquí es donde puede depender de como estructuraste tu codigo. En mi  caso todas las url se generaban en un único procedimiento url(). A efectos de depuración introduje un setting de aplicación que me permite controlar el tipo de url que quiero generar.
Un primer paso es disponer es generar un patrón de url:
If tipoUrl = tiposDeUrl.caminoIzquierdaSimple Then
    dim privpagina=My.Request.uri.Absoluteuri
    Dim rg As New Regex( _
             "(?http\://\w*(\.\w*)*(?:\:\d*)?/\w*/)" + _
             "(?:(?\w-\w(?:-\w*)*)/)?" + _
             "(?default\.aspx)(?.*)?", _
                  RegexOptions.IgnoreCase)
    Dim capturas = rg.Matches(privpagina)
    If capturas.Count = 1 Then
        privpagina = capturas(0).Groups("raiz").Value + _
                     "{0}/" + _
                     capturas(0).Groups("pagina").Value
    End If
End If
El resultado es que en privPagina tendremos algo asi como:
http://www.spaintiles.info/dirbeta/{0}/default.aspx
Que nos será muy util para reemplazar con el valor del parametro:
Select Case tipoUrl
    Case tiposDeUrl.parametros
              resultado = String.Format("{0}?P={1}", pagina, camino)
    Case tiposDeUrl.caminoIzquierdaSimple
              resultado = String.Format(pagina, camino)
End Select
(recordar que por pruedencia tengo un setting para controlar el tipo de url que utilizo)

En este momento podemos probar porque igual todo funciona directamente.
Es posible que surgan problemas:
A) Al hacer click en un botón o similar.
El problema es que hacemos un post del form asp.net y el path está mal escrito.
Solución:
En el evento Load de la página añadid:
Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
        form1.Action = Request.RawUrl
.........

B) No se cargan estilos o scripts.
En mi caso opté por cargarlos mediante código:
Public Function RaizApp() As String
     With me.Request
         If .ApplicationPath.EndsWith("/") Then
            Return .ApplicationPath
         Else
            Return .ApplicationPath + "/"
         End If
     End With
End Function
Sub CargaJava(ByVal nombre As String)
     Dim sc As New HtmlGenericControl("script")
     sc.Attributes("type") = "text/javascript"
     sc.Attributes("src") = String.Format("{0}Scripts/{1}.js", raizapp, nombre)
     me.Header.Controls.Add(sc)
End Sub

Sub CargaCSS(ByVal nombre As String)
Dim css As New HtmlLink
    css.Href = String.Format("{0}estilos/{1}.css", RaizApp, nombre)
    css.Attributes("type") = "text/css"
    css.Attributes("rel") = "stylesheet"
    css.Attributes("media") = "all"
    me.Header.Controls.Add(css)
End Sub

Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
'Event load de la página
    form1.Action = Request.RawUrl
    If Request.QueryString.AllKeys.Contains("_TSM_HiddenField") Then Exit Sub
    ToolkitScriptManager1.Scripts.Add(New UI.ScriptReference(RaizApp + "scripts/dfc.js"))
    CargaJava("jquery-1.3.2.min")
    CargaCSS("estilocomun")
    CargaCSS("estiloceramica")
    CargaCSS("findyourCeramic")
..........
Con esto y algun que otro parche ocasional te funcionará la reescritura de URLs.

Puedes encontrar mas información sobre parchear los paths relativos en  Fixing Relative Paths in C# ASP.NET When Using Url Rewriting.
Gracias a Walter Wang que me dió la última idea en http://www.eggheadcafe.com/software/aspnet/30412853/url-rewriting-and-file-paths.aspx (otro post interesante sobre el tema.

No hay comentarios: