MSDN Magazine > Inicio > Todos los números > 2008 > March >  ASP.NET MVC: Creación de aplicaciones web ...
ASP.NET MVC
Creación de aplicaciones web sin formularios Web Forms
Chris Tavares

Este artículo se basa en una versión preliminar de ASP.NET MVC Framework. Los detalles de este documento están sujetos a cambios.
En este artículo se analizan los siguientes temas:
  • Patrón Controlador de vista de modelo (MVC, Model View Controller)
  • Creación de controladores y vistas
  • Creación de formularios y devolución
  • Fábricas de controladores y otros puntos de extensibilidad
En este artículo se utilizan las siguientes tecnologías:
ASP.NET
Descarga de código disponible en: MVCFramework2008_03.exe (189 KB)
Browse the Code Online
Soy desarrollador profesional desde hace unos 15 años. En los 10 años anteriores a eso, me lo tomaba como un pasatiempo. Como la mayor parte de mi generación, empecé con equipos de 8 bits y después me pasé al PC. Empecé a trabajar con equipos cada vez más complejos, escribí aplicaciones que hacían de todo, desde pequeños juegos hasta la administración de datos personales pasando por el control del hardware externo.
Durante la primera mitad de mi carrera, sin embargo, todo el software que escribí tenía algo en común: siempre eran aplicaciones locales que se ejecutaban en un escritorio de usuario. A principios de la década de los noventa, empecé a oír hablar de algo llamado "World Wide Web". Ahí se me presentó una oportunidad para crear una aplicación web que me permitiera escribir la información de mi tarjeta de horas trabajadas sin tener que volver a mi oficina desde el sitio del trabajo.
La experiencia fue, en una palabra, desconcertante. Tener que enfrentarme con una Web sin estado no encajaba con mi mentalidad orientada al escritorio. A eso había que agregar una depuración pésima, un servidor UNIX al que no tenía acceso principal y encima todo aquello de los corchetes angulares. Así que no me quedó más remedio que agachar la cabeza y volver al desarrollo de escritorio durante algunos años más.
Me aparté del desarrollo web. Obviamente era importante pero realmente no entendí el modelo de programación. Fue entonces cuando aparecieron Microsoft® .NET Framework y ASP.NET. Finalmente, llegaba un marco que me permitía trabajar en aplicaciones web, aunque era casi como programar aplicaciones de escritorio. Podía crear ventanas (páginas), enlazar controles a eventos y el diseñador me evitaba tener que tratar con aquellos corchetes angulares tan horribles. Lo mejor de todo fue que ASP.NET administraba automáticamente la naturaleza sin estado de la Web por mí con un estado de vista. Volví a ser un programador feliz..., al menos durante un tiempo.
A medida que aumentaba mi experiencia, también lo hacían mis elecciones en el diseño. Había aprendido varias procedimientos recomendados que aplicaba cuando trabajaba en mis aplicaciones de escritorio. Dos de ellos fueron:
  • Separación de preocupaciones: no mezclar la lógica de IU con el comportamiento subyacente.
  • Pruebas unitarias automatizadas: escribir pruebas automatizadas que comprueben si el código hace lo que se espera de él.
En este caso, los principios subyacentes se aplican sin tener en cuenta la tecnología. La separación de preocupaciones es un principio fundamental para abordar la complejidad con garantías. Mezclar responsabilidades diferentes dentro del mismo objeto, como calcular las horas de trabajo que quedan, dar formato a los datos o dibujar un gráfico, supone abrir la puerta a los problemas de mantenimiento. La prueba automatizada es crucial para obtener código de calidad de producción al tiempo que se mantiene todo bajo control, sobre todo si está actualizando un proyecto existente.
Los formularios Web Forms de ASP.NET hicieron que fuera muy fácil comenzar, pero, por otro lado, tratar de aplicar mis principios de diseño a aplicaciones web fue un problema. Los formularios Web Forms están centrados inequívocamente en la IU, siendo la página el subcomponente fundamental. Se comienza con el diseño de la IU y se arrastran los controles. Es muy tentador comenzar plasmando la lógica de la aplicación en los controladores de eventos de la página (como habilitar Visual Basic® para aplicaciones de Windows®).
Además de eso, las pruebas unitarias de las páginas son a menudo difíciles. No puede ejecutar un objeto de página en su ciclo de vida sin poner en marcha todo ASP.NET. Aunque es posible probar las aplicaciones web mediante el envío de solicitudes HTTP a un servidor o mediante la automatización de un explorador, esta modalidad de prueba es frágil (se cambia un identificador de control y la prueba se interrumpe), es difícil de configurar (tiene que configurar el servidor en cada equipo del desarrollador exactamente de la misma manera) y su ejecución es lenta.
Cuando empecé a crear aplicaciones web más sofisticadas, las abstracciones que ofrecen los formularios Web Forms, como los controles, el estado de la vista y el ciclo de vida de la página, comenzaron a ser un estorbo más que una ayuda. Perdía cada vez más tiempo en configurar el enlace de los datos y en escribir miles de controladores de eventos para dar con la configuración correcta. Tuve que averiguar cómo reducir el tamaño del estado de la vista para que las páginas se cargaran más rápidamente. Los formularios Web Forms exigen la existencia de un archivo físico en cada URL, algo que los sitios dinámicos (como una wiki, por ejemplo) dificultan. Además, escribir correctamente un WebControl personalizado es un proceso tremendamente complejo que requiere una comprensión completa del ciclo de vida de la página y del diseñador de Visual Studio®.
Desde que trabajo para Microsoft, he tenido la oportunidad de compartir mis conocimientos sobre algunos puntos complicados de .NET y así poder reducir las dificultades. Esta oportunidad se me presentó recientemente a través de mi participación como desarrollador en el proyecto de patterns & practices Web Client Software Factory (codeplex.com/websf). De hecho, una de las cosas que patterns & practices incorpora en sus entregas son las pruebas unitarias automatizadas. En Web Client Software Factory, nos propusimos usar el patrón Presentador de vistas de modelo (MVP, Model View Presenter) para crear formularios Web Forms que se pudieran probar.
En pocas palabras, en vez de poner su lógica en la página, MVP le hace crear páginas de manera que la página (vista) simplemente hace llamadas a un objeto independiente, el presentador. El objeto Presentador lleva a cabo a continuación la lógica necesaria para responder a la actividad en la vista, normalmente usando otros objetos (el modelo) para tener acceso a las bases de datos, llevar a cabo la lógica empresarial, etc. Una vez completados esos pasos, el presentador actualizará la vista. Este enfoque le ofrece capacidad de prueba porque el presentador está aislado de la canalización de ASP.NET, se comunica con la vista mediante una interfaz y se puede probar independientemente de la página.
MVP funciona, pero la implementación puede ser algo difícil. Necesita una interfaz de vista independiente y tendrá que escribir muchas funciones de reenvío de eventos en los archivos de código subyacente. Pero si desea una IU que se pueda probar en las aplicaciones de formularios Web Forms, no va a encontrar nada mejor. Cualquier mejora requeriría un cambio en la plataforma subyacente.

Patrón Controlador de vista de modelo
Por suerte, el equipo de ASP.NET ha estado escuchando a desarrolladores como yo y ha iniciado el desarrollo de un nuevo marco para aplicaciones web que va aparejado a los formularios Web Forms que conoce y que tanto le gustan, pero que tiene un conjunto de objetivos de diseño totalmente diferente:
  • Adopte HTTP y HTML, no lo esconda.
  • La capacidad de prueba está integrada desde el principio.
  • Extensible en casi cada punto.
  • Control total sobre la salida.
Este nuevo marco está basado en el patrón Controlador de vista de modelo (MVC), de ahí el nombre: ASP.NET MVC. El patrón MVC tiene su origen en la década de los 70 como parte de Smalltalk. Tal y como describiré en este artículo, se ajusta bastante bien a la naturaleza de la Web. MVC divide su IU en tres objetos diferenciados: el controlador, que recibe y gestiona la entrada; el modelo, que contiene la lógica del dominio; y la vista, que genera la salida. En el contexto de la Web, la entrada es una solicitud HTTP y el flujo de la solicitud se parecerá a lo que muestra la Figura 1.
Figura 1 Flujo de solicitud de patrón de MVC (Hacer clic en la imagen para ampliarla)
Realmente es bastante diferente del proceso de los formularios Web Forms. En ellos, la entrada se introduce en la página (la vista), siendo ésta responsable tanto de administrar la entrada como de generar la salida. Por otra parte, cuando se trata de MVC las responsabilidades se separan.
Así que es posible que en estos momentos haya alguna de estas dos cosas rondándole la cabeza. Bien, "oye, esto es fantástico. ¿Cómo lo uso?", o bien "¿por qué tengo que escribir tres objetos cuando antes sólo tenía que escribir uno?". Ambas preguntas son excelentes y se explican mejor con un ejemplo. Así que voy a escribir una pequeña aplicación web con MVC Framework para demostrar sus ventajas.

Creación de un controlador
Para continuar, tendrá que instalar Visual Studio 2008 y conseguir una copia de MVC Framework. En el momento de escribir este artículo, está disponible como parte de la edición de Community Technology Preview (CTP) de diciembre de 2007 sobre las extensiones de ASP.NET (asp.net/downloads/3.5-extensions). Querrá tanto las extensiones CTP como el Kit de herramientas de MVC, que incluye algunos objetos auxiliares muy útiles. Una vez que haya descargado e instalado el CTP, aparecerá un nuevo tipo de proyecto en el cuadro de diálogo New Project denominado ASP.NET MVC Web Application.
Seleccionar el proyecto de aplicación web de MVC le da una solución que tiene un aspecto diferente al del sitio o aplicación web habituales. La plantilla de la solución crea una aplicación web con algunos directorios nuevos (tal y como se muestra en la Figura 2). En particular, el directorio de los controladores contiene las clases de controladores y el directorio de las vistas (y todos sus subdirectorios) contiene las vistas.
Figura 2 Estructura del proyecto de MVC 
Voy a escribir un controlador muy simple que devuelve un nombre en una URL. Hacer clic con el botón secundario en la carpeta de controladores y elegir Add Item muestra el cuadro de diálogo Add New Item, con algunas adiciones, incluida una clase de controlador de MVC y varios componentes de la vista de MVC. En este caso, estoy agregando una clase denominada HelloController:
using System;
using System.Web;
using System.Web.Mvc;

namespace HelloFromMVC.Controllers
{
    public class HelloController : Controller
    {
        [ControllerAction]
        public void Index()
        {
            ...
        }
    }
}
Una clase de controlador tiene bastante menos peso que una página. De hecho, las únicas cosas que son realmente necesarias son derivar desde System.Web.Mvc.Controller y poner el atributo [ControllerAction] en los métodos de acción. Una acción es un método al que se llama en respuesta a una solicitud realizada a una URL concreta. Las acciones son responsables de completar el procesamiento que sea necesario y, a continuación, representar una vista. Comenzaré escribiendo una acción sencilla que pasa el nombre a la vista, como puede ver aquí:
[ControllerAction]
 public void HiThere(string id)
 {
     ViewData["Name"] = id;
     RenderView("HiThere");
 }
El método de acción recibe el nombre de la dirección URL a través del parámetro de identificación (obtendrá más información al respecto un poco más adelante), lo almacena en la colección ViewData y después representa una vista denominada HiThere.
Antes de tratar cómo se llama a este método o el aspecto de la vista, me gustaría hablar de la capacidad de prueba. ¿Recuerda mis comentarios anteriores sobre lo difícil que es probar clases de páginas de formularios Web Forms? Bien, los controladores son más fáciles de probar. De hecho, se puede crear una instancia del controlador directamente y llamar a los métodos de acción sin ninguna infraestructura adicional. No necesita un contexto HTTP ni tampoco un servidor, sólo un agente de prueba. Como ejemplo, he incluido una prueba unitaria de Visual Studio Team System (VSTS) para esta clase en la Figura 3.
namespace HelloFromMVC.Tests
{
    [TestClass]
    public class HelloControllerFixture
    {
        [TestMethod]
        public void HiThereShouldRenderCorrectView()
        {
            TestableHelloController controller = new 
              TestableHelloController();
            controller.HiThere("Chris");

            Assert.AreEqual("Chris", controller.Name);
            Assert.AreEqual("HiThere", controller.ViewName);
        }

    }

    class TestableHelloController : HelloController
    {
        public string Name;
        public string ViewName;

        protected override void RenderView(
            string viewName, string master, object data)
        {
            this.ViewName = viewName;
            this.Name = (string)ViewData["Name"];
        }
    }

}

Aquí pasan varias cosas. La prueba real es muy sencilla: crear una instancia del controlador, llamar al método con los datos esperados, comprobar que se ha representado la vista correcta. Hago la comprobación mediante la creación de una subclase específica de la prueba que omite el método RenderView. Esto me permite interrumpir la creación real de HTML. Lo único que me preocupa es que se envíen los datos correctos a la vista y que se represente la vista correcta. Para el propósito de esta prueba, no voy a fijarme en los detalles subyacentes de la vista.

Creación de una vista
Por supuesto, en última instancia necesito generar código HTML, así que vamos a crear esa vista HiThere. Para ello, primero creo una carpeta nueva en la solución denominada Hello debajo de la carpeta de las vistas. De forma predeterminada, el controlador buscará una vista en la carpeta Views\<ControllerPrefix> (el prefijo del controlador es el nombre de la clase del controlador menos la palabra "Controller"). Así que para las vistas representadas por HelloController, mirará en Views\Hello. La solución acaba por parecerse a la Figura 4.
Figura 4 Adición de una vista al proyecto (Hacer clic en la imagen para ampliarla)
El HTML para la vista se parece a esto:
<html  >
<head runat="server">
    <title>Hi There!</title>
</head>
<body>
    <div>
        <h1>Hello, <%= ViewData["Name"] %></h1>
    </div>
</body>
</html>
Hay varias cosas que deberían llamarle la atención. No hay etiquetas runat="server". No hay etiqueta de formularios. No hay declaraciones de control. De hecho, esto parece más un ASP clásico que ASP.NET. Tenga en cuenta que las vistas de MVC sólo son responsables de generar la salida, de manera que no necesitan de la administración de eventos ni controles complejos que sí son necesarios para las páginas de formularios Web Forms.
MVC Framework toma prestado el formato de archivo .aspx como un lenguaje de plantillas de texto muy útil. Incluso puede usar código subyacente si así lo desea, aunque de manera predeterminada este código tiene el siguiente aspecto:
using System;
using System.Web;
using System.Web.Mvc;

namespace HelloFromMVC.Views.Hello
{
    public partial class HiThere : ViewPage
    {
    }
}
No hay métodos de carga ni de Init de página, ni controladores de eventos, nada excepto la declaración de la clase base, que no es Page sino ViewPage. Esto es todo lo que necesita para disponer de una vista de MVC. Ejecute la aplicación, navegue hasta http://hostlocal:<puerto>/Hello/HiThere/Chris y verá algo parecido a la Figura 5.
Figura 5 Vista correcta de MVC (Hacer clic en la imagen para ampliarla)
Si en vez de la Figura 5 viera una excepción de aspecto infame, no se asuste. Si obtiene el conjunto de archivos HiThere.aspx como documento activo en Visual Studio al presionar F5, Visual Studio intentará tener acceso al archivo .aspx directamente. Dado que las vistas de MVC exigen que se ejecute el controlador antes de mostrarlo, intentar navegar directamente a la página no surtirá efecto. Para que funcione, debería bastar con editar la URL para que coincida con lo que se muestra en la Figura 5.
¿Cómo sabría MVC Framework que debe llamar a mi método de acción? No había ninguna extensión de archivo para esa URL. La respuesta está en el enrutamiento de URL. Si mira dentro del archivo global.asax.cs, verá el trozo de código de la Figura 6. El RouteTable global almacena una colección de objetos Route. Cada objeto Route describe un formulario de URL y qué hacer con él. De forma predeterminada, se agregan dos rutas a la tabla. La primera es la que obra el milagro. Dice que por cada URL que conste de tres elementos después del nombre del servidor, el primer elemento debería tomarse como un nombre del controlador, el segundo como un nombre de acción y el tercero como el parámetro de identificación:
public class Global : System.Web.HttpApplication
{
    protected void Application_Start(object sender, EventArgs e)
    {
        // Change Url= to Url="[controller].mvc/[action]/[id]" 
        // to enable automatic support on IIS6 

        RouteTable.Routes.Add(new Route
        {
            Url = "[controller]/[action]/[id]",
            Defaults = new { action = "Index", id = (string)null },
            RouteHandler = typeof(MvcRouteHandler)
        });

        RouteTable.Routes.Add(new Route
        {
            Url = "Default.aspx",
            Defaults = new { 
                controller = "Home", 
                action = "Index", 
                id = (string)null },
            RouteHandler = typeof(MvcRouteHandler)
        });
    }
}

Url = "[controller]/[action]/[id]"
Esta ruta predeterminada es la que habilitó la invocación de mi método HiThere. Recuerde esta URL: http://hostlocal/Hello/HiThere/Chris? Esta ruta asignó Hello al controlador, HiThere a la acción y Chris al identificador. MVC Framework creó una instancia de HelloController, llamó al método HiThere y pasó Chris como el valor del parámetro del identificador.
Esta ruta predeterminada le da mucho, pero puede agregar sus propias rutas también. Por ejemplo, me gustaría tener un sitio realmente agradable donde los usuarios sólo tuvieran que escribir su nombre para obtener un saludo personalizado. Si agrego esta ruta a la parte superior de la tabla de enrutamiento
  RouteTable.Routes.Add(new Route
  {
    Url = "[id]",
    Defaults = new { 
        controller = "Hello", 
        action = "HiThere" },
    RouteHandler = typeof(MvcRouteHandler)
  });
Puedo ir simplemente a http://hostlocal/Chris, se seguirá invocando mi acción y veré mi saludo amistoso.
¿Cómo supo el sistema qué controlador y qué acción debía invocar? La respuesta está en el parámetro Defaults, que utiliza la nueva sintaxis de tipo anónimo de C# 3.0 para crear un pseudodiccionario. El objeto Defaults en Route puede contener información adicional arbitraria, pero para MVC también puede contener algunas entradas muy conocidas: controlador y acción. Si no se ha especificado ningún controlador ni acción en la URL, usará el nombre que aparece en Defaults. Por eso los puedo dejar fuera de la URL y seguir teniendo la solicitud asignada al controlador y la acción correctos.
Otra cosa a tener en cuenta: ¿recuerda que dije "agregar a la parte superior de la tabla"? Si lo pone en la parte inferior, aparecerá un error. El enrutamiento funciona por orden de llegada. Al procesar las direcciones URL, el sistema de enrutamiento recorre la tabla de arriba abajo y la primera ruta que coincide gana. En este caso, la ruta predeterminada "[controller]/[action]/[id]" coincide porque hay valores predeterminados para la acción y el identificador. Así, va a buscar ChrisController y, como no tengo un controlador, aparece un error.

Un ejemplo mejor
Ahora que he demostrado los principios de MVC Framework, me gustaría mostrarle un ejemplo de más entidad donde se muestra más que cómo mostrar una cadena. Una wiki es un sitio web que puede modificarse en el explorador. Las páginas pueden agregarse o editarse con facilidad. He escrito una wiki de ejemplo pequeña con MVC Framework. La pantalla "Editar esta página" se muestra en la Figura 7.
Figura 7 Edición de la página principal (Hacer clic en la imagen para ampliarla)
Puede comprobar la descarga de código de este artículo para consultar cómo se implementa la lógica subyacente de wiki. En este momento quiero concentrarme en cómo MVC Framework me facilitó poner la wiki en la Web. Empecé diseñando mi estructura de URL. Quería lo siguiente:
  • /[pagename] muestra la página con ese nombre.
  • /[pagename]?version=n muestra la versión solicitada de la página, donde 0 = la versión actual, 1 = la anterior, etc.
  • /Edit/[pagename] abre la pantalla de edición para esa página.
  • /CreateNewVersion/[pagename] es la dirección URL que se publica para enviar una edición.
Vamos a comenzar con la muestra básica de una página wiki. Para ello, creé una clase nueva denominada "WikiPageController". Después agregué una acción denominada "ShowPage". WikiPageController empezó con un aspecto parecido al de la Figura 8. El método ShowPage es bastante sencillo. Las clases WikiSpace y WikiPage representan un conjunto de páginas wiki y una página específica (y sus revisiones), respectivamente. Esta acción simplemente carga el modelo y llama a RenderView. ¿Pero qué es esa nueva línea "WikiPageViewData" de ahí?
public class WikiPageController : Controller 
{
  ISpaceRepository repository;

  public ISpaceRepository Repository 
  {
    get {
      if (repository == null) 
      {
        repository = new FileBasedSpaceRepository(
            Request.MapPath("~/WikiPages"));
      }
      return repository;
    }

    set { repository = value; }
  }

  [ControllerAction]
  public void ShowPage(string pageName, int? version) 
  {
    WikiSpace space = new WikiSpace(Repository);
    WikiPage page = space.GetPage(pageName);

    RenderView("showpage", 
      new WikiPageViewData 
      { 
        Name = pageName,
        Page = page,
        Version = version ?? 0 
      });
  }
}

En el ejemplo anterior he mostrado una manera de pasar datos del controlador a la vista: el diccionario ViewData. Los diccionarios son adecuados, pero también peligrosos. Pueden contener cualquier cosa, usted no dispone de una solución IntelliSense® para el contenido y, puesto que el diccionario ViewData es del tipo Diccionario<cadena, objeto>, para hacer uso de su contenido, tiene que convertirlo todo.
Cuando sepa qué datos va a necesitar en la vista, podrá pasar en su lugar el objeto ViewData con establecimiento inflexible de tipos. En mi caso, creé un objeto sencillo, WikiPageViewData, tal y como se muestra en la Figura 9. Este objeto lleva la información de la página wiki a la vista junto con un par de métodos de la utilidad para hacer algunas cosas como obtener la versión HTML del marcado de wiki.
public class WikiPageViewData {

    public string Name { get; set; }
    public WikiPage Page { get; set; }
    public int Version { get; set; }

    public WikiPageViewData() {
        Version = 0;
    }

    public string NewVersionUrl {
        get {
            return string.Format("/CreateNewVersion/{0}", Name);
        }
    }

    public string Body {
        get { return Page.Versions[Version].Body; }
    }

    public string HtmlBody {
        get { return Page.Versions[Version].BodyAsHtml(); }
    }

    public string Creator {
        get { return Page.Versions[Version].Creator; }
    }

    public string Tags {
        get { return string.Join(",", Page.Versions[Version].Tags); }
    }
}

¿Cómo uso los datos de la vista ahora que ya los tengo definidos? En ShowPage.aspx.cs, verá lo siguiente:
namespace MiniWiki.Views.WikiPage {
    public partial class ShowPage : ViewPage<WikiPageViewData>
    {
    }
}
Tenga en cuenta que he definido la clase base para que sea del tipo ViewPage<WikiPageViewData>. Esto significa que la propiedad ViewData de la página es del tipo WikiPageViewData y no de un diccionario como en el ejemplo anterior.
El marcado real en el archivo .aspx es bastante sencillo:
<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master"
  AutoEventWireup="true" CodeBehind="ShowPage.aspx.cs" 
  Inherits="MiniWiki.Views.WikiPage.ShowPage" %>
<asp:Content 
  ID="Content1"  
  ContentPlaceHolderID="MainContentPlaceHolder" 
  runat="server">
  <h1><%= ViewData.Name %></h1>
  <div id="content" class="wikiContent">
    <%= ViewData.HtmlBody %>
  </div>
</asp:Content>
Tenga en cuenta que no uso el operador de indización [] al hacer referencia a ViewData. En su lugar, puesto que ahora dispongo de un ViewData con establecimiento inflexible de tipos, sólo puedo tener acceso a la propiedad directamente. No es necesaria ninguna conversión y Visual Studio le proporciona IntelliSense.
El observador astuto se habrá dado cuenta de la presencia de la etiqueta <asp:Content> en este archivo. Sí, las páginas maestras trabajan con vistas de MVC. Y estas páginas también pueden tener condición de vistas. Echemos un vistazo al código subyacente de la página maestra:
namespace MiniWiki.Views.Layouts
{
    public partial class Site :  
        System.Web.Mvc.ViewMasterPage<WikiPageViewData>
    {
    }
}
El marcado asociado se muestra en la Figura 10. En este momento, la página maestra obtiene el mismo objeto ViewData que la vista. He declarado que la clase base de la página maestra será ViewMasterPage<WikiPageViewData>, de modo que tengo el tipo adecuado de ViewData. A partir de ahí, configuré las distintas etiquetas DIV para diseñar mi página, rellenar la lista de versiones y finalizar con el marcador de posición de contenido habitual.
<%@ Master Language="C#" 
  AutoEventWireup="true" 
  CodeBehind="Site.master.cs" 
  Inherits="MiniWiki.Views.Layouts.Site" %>
<%@ Import Namespace="MiniWiki.Controllers" %>
<%@ Import Namespace="MiniWiki.DomainModel" %>
<%@ Import Namespace="System.Web.Mvc" %>
<html >
<head runat="server">
  <title><%= ViewData.Name %></title>
  <link href="http://../../Content/Site.css" rel="stylesheet" type="text/css" />
</head>
<body>
  <div id="inner">
    <div id="top">
      <div id="header">
        <h1><%= ViewData.Name %></h1>
      </div>
      <div id="menu">
        <ul>
          <li><a href="http://Home">Home</a></li>
          <li>
            <%= Html.ActionLink("Edit this page", 
                  new { controller = "WikiPage", 
                        action = "EditPage", 
                        pageName = ViewData.Name })%>
        </ul>
      </div>
    </div>
    <div id="main">
      <div id="revisions">
        Revision history:
        <ul>
          <% 
            int i = 0;
            foreach (WikiPageVersion version in ViewData.Page.Versions)
            { %>
              <li>
                <a href="http://<%= ViewData.Name %>?version=<%= i %>">
                  <%= version.CreatedOn %>
                  by
                  <%= version.Creator %>
                </a>
              </li>
          <%  ++i;
          } %>
        </ul>
      </div>
      <div id="maincontent">
        <asp:ContentPlaceHolder 
          ID="MainContentPlaceHolder" 
          runat="server">
        </asp:ContentPlaceHolder>
      </div>
    </div>
  </div>
</body>
</html>

Otra cosa que hay que destacar es la llamada a Html.ActionLink. Esto es un ejemplo de una aplicación auxiliar de representación. Las distintas clases de vista tienen dos propiedades, HTML y URL. Cada una tiene métodos útiles para producir fragmentos de HTML. En este caso, Html.ActionLink toma un objeto (aquí, de un tipo anónimo) y lo ejecuta de nuevo en el sistema de enrutamiento. Esto produce una dirección URL que dirigirá al controlador y a la acción que haya especificado. De este modo, por mucho que cambie mis rutas, el vínculo "Editar esta página" siempre señalará al lugar correcto.
Puede que haya advertido también que he tenido que recurrir a crear un vínculo manualmente (los vínculos a versiones de página anteriores). Desgraciadamente, el sistema de enrutamiento actual no funciona tan bien a la hora de generar direcciones URL donde hay cadenas de consulta implicadas. Esto deberá solucionarse en versiones posteriores del marco.

Creación de formularios y devolución
Ahora, echemos un vistazo a la acción EditPage en el controlador:
[ControllerAction]
public void EditPage(string pageName)
{
  WikiSpace space = new WikiSpace(Repository);
  WikiPage page = space.GetPage(pageName);

  RenderView("editpage", 
    new WikiPageViewData { 
      Name = pageName, 
      Page = page });
}
De nuevo, la acción no hace mucho, tan sólo representa una vista con una página determinada. Las cosas se ponen más interesantes en la vista, tal y como se muestra en la Figura 11. Este archivo está creando un formulario HTML, pero no se ve ningún atributo Runat="server". La aplicación auxiliar Url.Action se usa para generar la URL a la que devuelve el formulario. Hay también varios usos de varias aplicaciones auxiliares HTML como TextBox, TextArea y SubmitButton. Hacen bastante más de lo que cabría esperar: generar HTML para varios campos de entrada.
<%@ Page Language="C#" 
  MasterPageFile="~/Views/Shared/Site.Master" 
  AutoEventWireup="true" 
  CodeBehind="EditPage.aspx.cs" 
  Inherits="MiniWiki.Views.WikiPage.EditPage" %>
<%@ Import Namespace="System.Web.Mvc" %>
<%@ Import Namespace="MiniWiki.Controllers" %>
<asp:Content ID="Content1" 
  ContentPlaceHolderID="MainContentPlaceHolder" 
  runat="server">
  <form action="<%= Url.Action(
    new { controller = "WikiPage", 
    action = "NewVersion", 
    pageName = ViewData.Name })%>" method=post>
    <%
      if (ViewContext.TempData.ContainsKey("errors"))
      {
    %>
    <div id="errorlist">
      <ul>
      <%
        foreach (string error in 
          (string[])ViewContext.TempData["errors"])
        {
      %>
        <li><%= error%></li>
      <% } %>
      </ul>
    </div>
    <% } %>
    Your name: <%= Html.TextBox("Creator",
                   ViewContext.TempData.ContainsKey("creator") ? 
                   (string)ViewContext.TempData["creator"] : 
                   ViewData.Creator)%>
    <br />
    Please enter your updates here:<br />
    <%= Html.TextArea("Body", ViewContext.TempData.ContainsKey("body") ? 
        (string)ViewContext.TempData["body"] : 
        ViewData.Body, 30, 65)%>
    <br />
    Tags: <%= Html.TextBox(
              "Tags", ViewContext.TempData.ContainsKey("tags") ? 
              (string)ViewContext.TempData["tags"] : 
              ViewData.Tags)%>
    <br />
    <%= Html.SubmitButton("SubmitAction", "OK")%>
    <%= Html.SubmitButton("SubmitAction", "Cancel")%>
  </form>
</asp:Content>

Uno de los aspectos más molestos de la programación web son los errores en un formulario. Más concretamente, quiere mostrar mensajes de error pero desea mantener los datos escritos previamente. Todos hemos tenido la experiencia de cometer un error en un formulario con 35 campos y que se nos muestren un montón de mensajes de error y un formulario nuevo y en blanco. MVC Framework ofrece TempData como un lugar donde almacenar la información escrita previamente de manera que el formulario se pueda rellenar de nuevo. Esto es algo que ViewState facilitó en gran medida en los formularios Web Forms, ya que el almacenamiento del contenido de los controles era mucho más automático.
Le gustaría hacer esto también en MVC y aquí es donde TempData entra en juego. TempData es un diccionario parecido a ViewData sin tipo. Sin embargo, el contenido de TempData sólo está presente para una solicitud y, a continuación, se elimina. Para ver cómo se usa, eche un vistazo a la Figura 12, la acción NewVersion.
[ControllerAction]
public void NewVersion(string pageName) {
  NewVersionPostData postData = new NewVersionPostData();
  postData.UpdateFrom(Request.Form);

  if (postData.SubmitAction == "OK") {
    if (postData.Errors.Length == 0) {
      WikiSpace space = new WikiSpace(Repository);
      WikiPage page = space.GetPage(pageName);
      WikiPageVersion newVersion = new WikiPageVersion(
        postData.Body, postData.Creator, postData.TagList);
      page.Add(newVersion);
    } else {
      TempData["creator"] = postData.Creator;
      TempData["body"] = postData.Body;
      TempData["tags"] = postData.Tags;
      TempData["errors"] = postData.Errors;

      RedirectToAction(new { 
        controller = "WikiPage", 
        action = "EditPage", 
        pageName = pageName });
      return;
    }
  }

  RedirectToAction(new { 
    controller = "WikiPage",
    action = "ShowPage", 
    pageName = pageName });
}

Primero, crea un objeto NewVersionPostData. Se trata de otro objeto de la aplicación auxiliar que tiene propiedades y métodos que almacenan los contenidos de la publicación y alguna validación. Para cargar el objeto postData, estoy usando una función de aplicación auxiliar del Kit de herramientas de MVC. UpdateFrom es realmente un método de extensión ofrecido por el kit de herramientas que usa procedimientos de reflexión para emparejar los nombres de los campos de formularios con los nombres de las propiedades en mi objeto. El resultado final es que todos los valores de campo se cargan en el objeto postData. El uso de UpdateFrom tiene la desventaja, sin embargo, de obtener los datos del formulario directamente desde HttpRequest, con lo que las pruebas unitarias son más difíciles.
Lo primero que NewVersion comprueba es SubmitAction. El resultado será correcto si el usuario hizo clic en el botón Aceptar y desea publicar realmente la página editada. Si existe cualquier otro valor, la acción volverá a dirigir a ShowPage, que vuelve a mostrar la página original.
Si el usuario hizo clic en Aceptar, comprobaré la propiedad postData.Errors. De esta forma se ejecutan varias validaciones sencillas del contenido publicado. Si no hay errores, realizaré el procesamiento para escribir la nueva versión de la página en la wiki. Sin embargo, si hay errores, esto se pone interesante.
Si hay errores, estableceré los distintos campos del diccionario TempData para que contenga el contenido de PostData. Después, volveré a redireccionar a la página de edición. Ahora, puesto que ya se ha definido TempData, la página se volverá a mostrar con el formulario inicializado con los valores que el usuario publicó la última vez.
El procesamiento de publicaciones, validaciones y TempData es un poco molesto en estos momentos y precisa más trabajo manual del que realmente es deseable. Las próximas versiones deberían incluir métodos auxiliares que automaticen al menos algunas de las comprobaciones de TempData. Una última nota sobre TempData: el contenido de TempData se almacena en la sesión del lado servidor del usuario. Si desactiva la sesión, TempData no funcionará.

Creación del controlador
Los principios básicos de la wiki ya están en funcionamiento, pero hay algunos puntos importantes en la implementación que me gustaría aclarar antes de seguir adelante. Por ejemplo, la propiedad Repository se usa para desconectar la lógica de la wiki del almacenamiento físico. Puede ofrecer repositorios que almacenen el contenido en el sistema de archivos (tal y como he hecho aquí), en una base de datos o en cualquier otro lugar que desee. Desgraciadamente, hay dos problemas que resolver.
Primero, mi clase de controlador está estrechamente ligada a la clase concreta FileBasedSpaceRepository. Necesito tener un valor predeterminado, de modo que si no se establece la propiedad, tenga algo razonable que usar. Aún peor, aquí también está codificada la ruta de acceso a los archivos en disco. Como mínimo, todo esto debería proceder de la configuración.
Segundo, el repositorio es realmente una dependencia necesaria. Tanto es así, que mi objeto no se ejecutaría sin él. El buen diseño me indica que el repositorio debería ser un parámetro de constructor, no una propiedad. Pero yo no lo puedo agregar al constructor porque MVC Framework requiere un constructor sin argumentos en los controladores.
Por suerte, hay un enlace de extensibilidad que me puede sacar de este enlace: la fábrica de controladores. La fábrica de controladores hace lo que su nombre indica: crea instancias de controlador. Sólo tiene que crear una clase que implemente la interfaz de IControllerFactory y la registre con el sistema de MVC. Puede registrar fábricas de controladores para todos los controladores o sólo para algunos tipos específicos. La Figura 13 muestra una fábrica de controladores para WikiPageController, con lo que ahora se pasa el repositorio como un parámetro de constructor.
public class WikiPageControllerFactory : IControllerFactory {

  public IController CreateController(RequestContext context, 
    Type controllerType)
  {
    return new WikiPageController(
      GetConfiguredRepository(context.HttpContext.Request));
  }

  private ISpaceRepository GetConfiguredRepository(IHttpRequest request)
  {
    return new FileBasedSpaceRepository(request.MapPath("~/WikiPages"));
  }
}

En este caso, la implementación es bastante trivial, pero puede habilitar los controladores de creación que usan herramientas mucho más eficaces (en concreto, contenedores de inserción de dependencia). En cualquier caso, ahora tengo todos los detalles de la obtención de las dependencias para el controlador separado en un objeto que es más fácil de administrar y mantener.
El último paso para que esto funcione es registrar la fábrica con el marco. Esto lo haré mediante la clase ControllerBuilder, agregando la línea siguiente a Global.asax.cs en el método Application_Start (antes o después de las rutas):
ControllerBuilder.Current.SetControllerFactory(
  typeof(WikiPageController), typeof(WiliPageControllerFactory));
Esto registrará una fábrica para WikiPageController. Si tuviera otros controladores en este proyecto, no usarían esta fábrica ya que sólo está registrada para el tipo WikiPageController. También puede llamar a SetDefaultControllerFactory si desea definir una fábrica que usar para cada controlador.

Otros puntos de extensibilidad
La fábrica de controladores es sólo el comienzo de la extensibilidad del marco. No tengo espacio en este artículo para entrar en detalles acerca de todos ellos, así que sólo destacaré los puntos principales. Primero, si desea producir algo distinto de HTML o si desea usar un motor de plantilla distinto de los formularios Web Forms, podrá definir el atributo ViewFactory del controlador en un valor diferente. Puede implementar la interfaz de IViewFactory, y después tendrá control completo sobre la generación de la salida. Esto es útil para generar RSS, XML o incluso gráficos.
El sistema del enrutamiento es bastante flexible, como habrá podido comprobar. Pero no hay nada en el sistema de enrutamiento que sea específico de MVC. Cada ruta tiene una propiedad RouteHandler que hasta ahora siempre he establecido en MvcRouteHandler. Pero es posible implementar la interfaz de IRouteHandler y enlazar el sistema de enrutamiento a otras tecnologías web. La próxima versión del marco vendrá con un WebFormsRouteHandler y habrá otras tecnologías en el futuro que saquen partido al sistema de enrutamiento genérico.
Los controladores no tienen que derivar desde System.Web.Mvc.Controller. Todo lo que necesita un controlador es implementar la interfaz de IController, que sólo tiene un método llamado Execute. A partir de ahí, puede hacer lo que quiera. Si sólo desea aprovechar unos cuantos comportamientos de la clase base de Controller, éste tiene muchas funciones virtuales que puede omitir:
  • OnPreAction, OnPostAction y OnError le permiten enlazar el procesamiento previo y posterior genérico en cada acción que se ejecute. OnError le proporciona un mecanismo de administración de errores en todo el controlador.
  • Se llama a HandleUnknownAction cuando se dirige una URL al controlador pero éste no implementa la acción solicitada en la ruta. De forma predeterminada, este método genera una excepción, pero puede omitirla y hacer lo que desee.
  • InvokeAction es el método que resuelve a qué método de acción hay que llamar y lo llama. Si quisiera personalizar el proceso (por ejemplo, para deshacerse del requisito para los atributos [ControllerAction]), este es el lugar para hacerlo.
Hay varios métodos virtuales en el controlador, pero existen principalmente como enlaces de prueba más que como puntos de extensión. Por ejemplo, RedirectToAction es virtual para que pueda crear una clase derivada que no redirige realmente. Esto le permite probar acciones que redirigir sin necesidad de que haya un servidor web completo funcionando.

¿Adiós a los formularios Web Forms?
En este punto puede que se esté preguntando "¿Qué pasa con los formularios Web Forms? ¿Los está reemplazando MVC?" La respuesta es no. Los formularios Web Forms constituyen una tecnología que se ha entendido bien y Microsoft continuará apoyándola y mejorándola. Hay muchas aplicaciones donde los formularios Web Forms funcionan muy bien. Por ejemplo, se puede crear una aplicación típica de informes de base de datos en una intranet mediante los formularios Web Forms en una mínima parte del tiempo que se tardaría en escribirla en MVC. Además, los formularios Web Forms son compatibles con una amplia gama de controles, muchos de los cuales son extremadamente sofisticados y ahorran gran cantidad de trabajo.
De modo que, ¿cuándo debería elegir usar MVC en lugar de los formularios Web Forms? En muchos sentidos, depende de los requisitos y preferencias que tenga. ¿Está luchando para conseguir que las URL se formen de la manera que quiere? ¿Desea realizar una prueba unitaria de su IU? Cualquiera de esas situaciones le guiará hasta MVC. Por otro lado, ¿está mostrando muchos datos, con cuadrículas editables y controles elaborados de vista de árbol? Entonces, quizás sea mejor que opte por los formularios Web Forms por el momento.
Con el tiempo, probablemente se adoptará MVC Framework en el departamento de control de IU, pero probablemente nunca será tan fácil comenzar con él como con los formularios Web Forms, donde se puede obtener acceso a una ingente cantidad de funciones mediante el procedimiento de arrastrar y colocar. Sin embargo, mientras tanto, ASP.NET MVC Framework ofrece a los desarrolladores de web una nueva manera de crear aplicaciones web en Microsoft .NET Framework. Framework está diseñado para la capacidad de prueba, adopta HTTP en lugar de intentar abstraerla y es extensible en cada punto. Es un complemento obligatorio para los formularios Web Forms para aquellos desarrolladores que deseen control total sobre sus aplicaciones web.

Chris Tavareses un desarrollador que forma parte del equipo de Microsoft patterns & practices, donde trabaja para ayudar a la comunidad de desarrollo a comprender los procedimientos recomendados para crear sistemas en plataformas de Microsoft. También es un miembro virtual del equipo de ASP.NET MVC, donde ayuda a diseñar el nuevo marco. Puede ponerse en contacto con Chris en cct@tavaresstudios.com.

Page view tracker