miércoles, 25 de junio de 2008

Truco para mostrar HTML en el WebBrowser

Este post trata de cómo mostrar HTML en el WebBrowser, haciendo que las URLs que aparezcan en dicho código HTML se procesen correctamente.

El problema surge cuando queremos descargar una página por "fuera" del WebBrowser (generando un GET o un POST, por ejemplo), y luego queremos mostrar el HTML recibido en esta ventana, a modo de registro de lo sucedido.

El WebBrowser provee al menos tres formas de cargar una página:
- .Navigate( url ): carga la URL indicada y la muestra.
- .Url = url: también carga la URL indicada y la muestra.
- .DocumentText = cadena_html: procesa el árbol DOM y lo muestra.

Para nuestro caso, sólo nos sirve la tercera opción. No queremos que se descargue dos veces la misma página, sino que nos interesa descargarla una vez y luego "alimentar" al WebBrowser con su HTML.

Surge un primer problema: debemos esperar a que la página termine de cargar. Podemos bloquearnos de la siguiente manera, siendo _embeddedBrowser nuestro WebBrowser:

do {
System.Threading.Thread.Sleep(50);
Application.DoEvents();
} while (_embeddedBrowser.ReadyState != WebBrowserReadyState.Complete || _embeddedBrowser.IsBusy);

Ahora surge otro problema, que está relacionado con las URLs relativas. Una página web puede mostrar imágenes, cargar hojas de estilo CSS y cargar scripts desde otros archivos del servidor web. El enlace a dichos archivos no necesita estar del todo completo. Así, si nuestra página se encuentra en http://www.blogger.com/china/index.html, podemos utilizar una imagen ubicada en http://www.blogger.com/logo.jpg de las siguientes maneras:

- src="http://www.blogger.com/logo.jpg"
- src="/logo.jpg"
- src="../logo.jpg"

Es complicado corregir en el HTML cada una de estas situaciones (sobre todo cuando el "/index.html" tampoco es obligatorio). Sin embargo, sin algún truco, sólo los contenidos indicados de la primera manera (la "completa") se mostrarán.

Después de experimentar con diferentes soluciones, entre las cuales incluyo modificar el HTML haciendo un parser, encontré que la mejor manera era generar un árbol DOM (simplificando, una estructura XML) con la página, agregar un tag 'BASE href="url de la página"' dentro del tag HEAD y volver a mostrarla.

La forma más directa de obtener el árbol DOM resultó ser a partir del mismo WebBrowser. Si nos quedamos esperando de la siguiente manera (aquí el WebBrowser es wb):

wb.DocumentText = contents;
do
{
System.Threading.Thread.Sleep(50);
Application.DoEvents();
} while ((wb.ReadyState != WebBrowserReadyState.Loaded) && (wb.ReadyState != WebBrowserReadyState.Complete));

Pronto podremos modificar el árbol DOM:

HtmlElement head = wb.Document.GetElementsByTagName("head")[0];
if (head != null)
{
if (wb.Document.GetElementsByTagName("base").Count == 0)
{
HtmlElement b = wb.Document.CreateElement("base");
b.SetAttribute("href",
sourceUrl);
head.AppendChild(b);
}
}

La contra de este procedimiento es que vemos el mismo HTML dos veces: una vez procesado con los links incorrectos, y otra con los links correctos. Si queremos evitar este parpadeo, ninguno de los métodos del WebBrowser parece ayudar, ni siquiera haciendo un .Visible = false. La forma más sencilla es:

- crear un WebBrowser falso
- asignarle el HTML con .DocumentText
- esperar a que se genere el DOM
- modificar el DOM para agregarle un tag 'base href="..."'
- recuperar el HTML de este WebBrowser falso
- asignar este HTML al WebBrowser real
- esperar a que se termine de cargar la página

Otra consideración hace a la interactividad. Probablemente nos interese hacer un .AllowNavigation = false, para que el usuario no pueda hacer click en el navegador. Sin embargo, las páginas no se cargan si esta propiedad no está en true. Así que deberemos habilitarla temporalmente.

El código final es el siguiente, donde contents es el HTML descargado y sourceUrl es la dirección desde donde se lo descargó. Por motivos de espacio, he quitado todas las comprobaciones de error.

public void showHtml(string contents, string sourceUrl)
{
WebBrowser wb = new WebBrowser();
wb.DocumentText = contents;
do
{
System.Threading.Thread.Sleep(50);
Application.DoEvents();
} while ((wb.ReadyState != WebBrowserReadyState.Loaded) && (wb.ReadyState != WebBrowserReadyState.Complete));

HtmlElement head = wb.Document.GetElementsByTagName("head")[0];
HtmlElement b = wb.Document.CreateElement("base");
b.SetAttribute("href", sourceUrl);
head.AppendChild(b);

_embeddedBrowser.AllowNavigation = true;
_embeddedBrowser.DocumentText = wb.Document.GetElementsByTagName("html")[0].OuterHtml;
wb = null;

do
{
System.Threading.Thread.Sleep(50);
Application.DoEvents();
} while (_embeddedBrowser.ReadyState != WebBrowserReadyState.Complete || _embeddedBrowser.IsBusy);

_embeddedBrowser.AllowNavigation = false;
}

En resumen, el WebBrowser está lleno de trucos, por ser amables. En otras palabras, podríamos decir que su interfaz es bastante caprichosa. Si alguien encuentra una solución mejor para estos problemas agradecería que la enviara:
- hacer que un WebBrowser cargue un HTML sin mostrarlo inmediatamente
- generar un árbol DOM completo sin utilizar un WebBrowser
- realizar el proceso sin tener que permitir .AllowNavigation en ningún momento
- modificar todas las URLs partiendo de una cadena HTML y una URL de base sin usar un WebBrowser para nada

Espero que esto le sirva de ayuda a alguien.

Nota posterior:

El tag BASE se inserta al final de la sección HEAD. Si hubiera referencias anteriores, por ejemplo, tags de SCRIPT externos, el navegador no esperará e intentará cargarlos antes de llegar al base path. Esto quiere decir que no encontrará los archivos con los scripts. Hay dos soluciones:
1 - Evitar que aparezcan popups con los errores en los scripts, haciendo .ScriptsErrorsSuppressed = true
2 - En vez de insertar un hijo al HEAD, insertar un hermano al primer hijo, de la siguiente manera:

if (head.Children.Count > 0)
{
head.Children[0].InsertAdjacentElement( HtmlElementInsertionOrientation.BeforeBegin, baseTag);
}
else
{
head.AppendChild(baseTag);
}

No hay comentarios: