ASP.NET MVC: Mostrar datos en HTML o PDF (pero en el fondo vamos a hablar de la tabla de rutas).

El otro día, uno de los grandes de Webforms (al que, aún que a veces despotrique un poco, MVC le está empezando a gustar :P), publico un excelente artículo sobre como generar PDFs usando MVC. En el artículo Marc mostraba como mostrar datos de un PDF físico en disco o bien usando una vista parcial para generar PDFs al vuelo.

Su artículo me sirve de excusa perfecta para escribir un artículo sobre como podríamos aplicar su solución a un caso más general: vamos a ver como podemos mostrar una misma vista ya sea en formato HTML o bien en PDF. De nuevo, antes que nada leeros el artículo de Marc, ya que este está basado en aquel.

El objetivo que pretendemos es que dada una url, p.ej. http://host/geeks/list nos muestre la información en formato HTML o bien en formato PDF. La primera cosa a resolver es como indicar si queremos que el formato de salida sea pdf o html… Las dos maneras que quizá se os ocurran primero son:

  1. Añadiendo un parámetro de ruta, de forma que tendremos una URL tipo /geeks/list/pdf o /geeks/list/html. No me convence porque no queda claro que este parámetro no es de “negocio”. Me explico, una URL que para ver mis datos podría ser /geeks/view/pdf/edu. El parámetro ‘edu’ és claramente de negocio, pero el parámetro pdf, no…
  2. Añadiendo un parámetro en querystring, de forma que tendremos una URL tipo /geeks/list?format=pdf. No me convence porque aunque MVC se defiende bien con parámetros en querystring, realmente son totalmente innecesarios (y rompen la “amigabilidad” de las URLs que promulga MVC).

Un dia navegando por la MSDN vi otra opción que me pareció interesante. P.ej. si vais a http://msdn.microsoft.com/library/system.string(v=VS.90).aspx obteneis la información de string para el framework 3.5, mientras que si vais a http://msdn.microsoft.com/library/system.string(v=VS.80).aspx la que obteneis es la información para el framework 2.0. El parámetro (v=xxx) indica la versión para el que mostrar información.

Podemos conseguir fácilmente algo parecido en MVC? Pues… por supuesto! 😉

1. La tabla de rutas

La tabla de rutas es uno de los aspectos más desconocidos de ASP.NET MVC. Mucha gente asume que la convención de URLs /controlador/accion/id es una obligación, pero ni mucho menos: las URLs en ASP.NET MVC pueden tener cualquier forma y se definen usando la tabla de rutas. La forma /controlador/accion/id es sólamente la configuración estándard de la tabla de rutas (y digo estándard y no por defecto porque la tabla de rutas inicialmente está vacía, pero el wizard de VS nos genera el código para rellenarla usando dicha convención).

El código para configurar la tabla de rutas está en Global.asax.cs y el código que genera VS es tal y como sigue:

routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);

La primera sentencia es para que MVC ignore las rutas de tipo algo.axd/algomás (estas URLs pertenecen al sistema de trace de ASP.NET) y la segunda sentencia es la interesante. En ella se mapean las URLs con el formato /controlador/accion/id, y no sólo eso, sinó que se asignan valores por defecto (contrrolador vale Home, accion vale index y id es opcional y no tiene valor). Es por eso que la URL / en MVC equivale a /Home que equivale a /Home/Index.

Lo que ponemos entre llaves ({}) en la definición de la URLs son las variables de la tabla de rutas. El resultado de procesar una URL mediante la tabla de rutas es un conjunto de valores, que conocemos con el nombre de Route Values. Hay dos Route Values que el sistema MVC debe ser capaz de extraer de cada URL: controller y action. El resto de route values son pasadas como parámetros al controlador. Cuando usamos MapRoute para añadir una ruta a la tabla de rutas, le pasamos la mayoría de veces tres parámetros:

  1. Un nombre de ruta
  2. El formato de las URLs que acepta dicha ruta. Lo que esté entre llaves es variable y se mapeará a una route value
  3. El valor de las route values por defecto cuando no se puedan mapear a partir de la URL.

Las URLs que queremos obtener son URLs del siguiente tipo: /geeks/list(pdf) o /geeks/list(html). Obviamente quiero poder añadir parámetros después, es decir /geeks/list(pdf)/edu debe funcionar. Y también quiero que las URL clásicas (/geeks/list o bien /geeks/list/edu) funcionen bien (generando la salida en HTML).

La tabla de rutas que hace posible esas URLs es tal y como sigue:

routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"WithDevice", // Route name
"{controller}/{action}({device})/{id}", // URL with parameters
new { controller = "Home", action = "Index", device = "html", id = UrlParameter.Optional }
);

routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", device = "html", id = UrlParameter.Optional }
);

Fijaos en los dos MapRoute:

  1. El primero mapea URLs del tipo controlador/accion(formato)/param. Los paréntesis deben aparecer. Es decir una url del tipo /geeks/list(pdf)/edu será procesada por esa ruta y asignará los route values:
    1. controller = geeks
    2. action=list
    3. device=pdf
    4. id=edu
  2. Si una URL no tiene este formato (no tiene los paréntesis) no se procesará por la primera ruta y entrará por la segunda. Esta segunda ruta es la clásica de ASP.NET MVC. La única diferencia es que asigna el valor del route value a html. Así pues una url del tipo /geeks/list/edu será procesada por esta segunda ruta (y obtendremos los mismos route values que el caso anterior).

Ahora que ya podemos procesar las URLs que nos interesan, ya podemos generar la salida en formato PDF o HTML, según el valor del route value device.

2.El controlador

El controlador es muy simple. En el ejemplo sólo tengo una acción (List) que devuelve un listado de geeks. No me interesa el parámetro id, pero si el parámetro device (para saber si debo generar la salida en  pdf o html) así pues, declaro el controlador:

public ActionResult List(string device)
{
IEnumerable<GeeksViewModel> geeks = new GeeksModel().GetAllGeeks();
return "pdf".Equals(device, StringComparison.InvariantCultureIgnoreCase) ?
this.Pdf(geeks) : View(geeks);
}

Fijaos como la acción recibe el parámetro device, cuyo valor será el valor del route value device… Os he dicho que me encanta MVC? 🙂

Lo que hace el controlador es poca cosa: obtiene una lista de objetos GeeksViewModel y luego o bien llama a View() para pasar dichos datos a la vista, o bien llama a Pdf que es un método extensor que he hecho, que devuelve los datos en pdf.

El código del método extensor (totalmente basado en lo que publicó Marc) es el siguiente:

public static class ControllerExtensions
{
public static ActionResult Pdf(this ControllerBase @this)
{
return Pdf(@this, null);
}

public static ActionResult Pdf(this ControllerBase @this, object model)
{
byte[] buf = null;
MemoryStream pdfTemp = new MemoryStream();
ViewEngineResult ver = ViewEngines.Engines.FindView(@this.ControllerContext,
@this.ControllerContext.RouteData.Values["action"].ToString(), null);
if (ver.View != null)
{
if (model != null)
{
@this.ViewData.Model = model;
}
string htmlTextView = GetViewToString(@this.ControllerContext, ver);
iTextSharp.text.Document doc = new iTextSharp.text.Document();
iTextSharp.text.pdf.PdfWriter writer = iTextSharp.text.pdf.PdfWriter.GetInstance(doc, pdfTemp);
writer.CloseStream = false;
doc.Open();
AddHTMLText(doc, htmlTextView);
doc.Close();
buf = new byte[pdfTemp.Position];
pdfTemp.Position = 0;
pdfTemp.Read(buf, 0, buf.Length);
}
return new System.Web.Mvc.FileContentResult(buf, "application/pdf");
}

private static void AddHTMLText(iTextSharp.text.Document doc, string html)
{

List<iTextSharp.text.IElement> htmlarraylist = HTMLWorker.ParseToList(new StringReader(html), null);
foreach (var item in htmlarraylist)
{
doc.Add(item);
}

}

private static string GetViewToString(ControllerContext context, ViewEngineResult result)
{
string viewResult = "";
TempDataDictionary tempData = new TempDataDictionary();
StringBuilder sb = new StringBuilder();
using (StringWriter sw = new StringWriter(sb))
{
using (HtmlTextWriter output = new HtmlTextWriter(sw))
{
ViewContext viewContext = new ViewContext(context, result.View, context.Controller.ViewData, context.Controller.TempData, output);
result.View.Render(viewContext, output);
}
viewResult = sb.ToString();
}
return viewResult;
}
}

Las únicas diferencias respecto el código que puso Marc son:

  1. El uso de vistas completas (en lugar de una vista parcial)
  2. El soporte para pasar a la vista un modelo
  3. El uso del ViewData y del TempData que tenga el controlador (en lugar de crear un ViewData y un TempData vacío)
  4. Que la vista se selecciona a través del nombre de acción en lugar de ser una vista concreta.

El resto es tal y como estaba el código de Marc (porque ya os digo que yo iTextSharp no es que lo domine mucho :P).

Y el resultado? Pues si llamamos a /Geeks/List o /Geeks/List(html) tenemos:

image

Mientras que si llamamos a /Geeks/List(pdf) el resultado es:

image

3. Conclusiones

Lo interesante del post no es ver como generar PDFs (de eso ya se encarga el post de Marc), lo interesante es ver como gracias a la tabla de rutas podemos crearnos nuestras propias URLs de forma muy sencilla!

Un saludo a todos y gracias a Marc por el post que me ha dado la excusa para escribir este! 😉

4 comentarios sobre “ASP.NET MVC: Mostrar datos en HTML o PDF (pero en el fondo vamos a hablar de la tabla de rutas).”

  1. Eduardo,

    La tercera opcion, que lo hace más transparente al usuario y además está en acorde a ReST, es la de usar las el ContentType en la petición, que es lo que yo normalmente hago.

  2. @Hadi
    Muchas gracias por la sugerencia! Tomo nota!
    Entiendo que es sobretodo para «servicios» pensados para ser consumidos por clientes (no browser, donde el cliente puede especificar el ‘accept’), no?

    @Marc
    Gracias Marc… la verdad es que iba a hacer un post muy parecido a este pero combinando salida xml/json en un «servicio», pero al ver tu post cambié el ejemplo!

    Muchas gracias por vuestros comentarios!!!!

Responder a etomas Cancelar respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *