Compartir a través de


ASP.NET

Aproveche diferentes marcos de programación con One ASP.NET

Jeff Fritz

En el año 2001, cuando Microsoft lanzó Microsoft .NET Framework y simultáneamente una nueva tecnología llamada ASP.NET, los desarrolladores web la adoptaron para el desarrollo de sitios con un marco basado en formularios. Este marco, conocido como Web Forms, resistió el paso del tiempo durante ocho años, con mejoras y cambios, para sostener un entorno web en constante evolución. En aquellos tiempos, la creación de una aplicación web era una elección sencilla, con un cuadro de diálogo de Nuevo proyecto que presentaba cuatro opciones para ASP.NET, tal como se aprecia en la Figura 1. La mayoría de nosotros no hacía caso de los proyectos para sitios web ASP.NET Mobile ni de las bibliotecas de control web ASP.NET y solo creamos proyectos de aplicaciones web ASP.NET. Si necesitábamos servicios web, agregábamos un servicio basado en SOAP a un sitio web existente, con un archivo .asmx.

The Original New Project ASP.NET Choices in Visual C#Figura 1 Las opciones originales de Nuevo proyecto ASP.NET en Visual C#

A principios de 2009, el panorama de ASP.NET cambió drásticamente, con la introducción del patrón Model-View-Controller (MVC). Con la promesa de no tener que controlar más el estado de las vistas, el ciclo de vida de los eventos de página ni los eventos de devolución, los desarrolladores acudieron al nuevo marco en tropel. Yo fui uno de ellos, intrigado por el potencial de esta tecnología web que se prestaba mucho mejor para las pruebas. Tuvimos que encontrar la manera de justificar el presupuesto para cambiar las aplicaciones a MVC ante nuestros administradores y centros de costos, y muchos desarrolladores recorrieron todos los pasos necesarios para que el contenido MVC se presentara en la misma aplicación como formulario Web Forms existente. Las cosas funcionaron bien con MVC durante varios años, hasta que la Web creció un poquito. ASP.NET tuvo que volver a evolucionar.

En el año 2012, Microsoft entregó dos marcos nuevos para el kit de herramientas de ASP.NET: Web API y SignalR. Ambos marcos contribuyeron algo especial al entorno y cada uno es único, a su manera:

  • Web API proporciona una experiencia similar a MVC, para que los desarrolladores entreguen contenidos diseñados para la interpretación automatizada. No hay interfaz de usuario y las transacciones se realizan en forma compatible con REST. Los tipos de contenido se negocian y Web API puede dar formato de manera automática a los contenidos como JSON y XML, basándose en los encabezados HTTP que se envían al extremo Web API.
  • SignalR es el nuevo modelo de entrega de “web en tiempo real” de Microsoft. Esta tecnología abre el canal de comunicaciones cliente-servidor, para permitir comunicaciones inmediatas y sofisticadas desde el servidor al cliente. El modelo de entrega de contenidos de SignalR invierte nuestras expectativas normales, ya que el servidor llama al cliente para interactuar con el contenido.

Tenga en cuenta los pros y los contras que se ven entre Web Forms y MVC con los de Web API y MVC, tal como se aprecian en la Figura 2.

Figura 2 Beneficios de cada marco de componentes de ASP.NET

Framework Productividad Control IU Tiempo real
Formularios web    
MVC    
Web API    
SignalR      

Productividad implica las características que nos permiten desarrollar y entregar una solución de manera rápida. Control es la medida en la que podemos afectar los bits que se transmiten por la red a los usuarios conectados. Interfaz de usuario (IU) indica si podemos usar el marco para entregar una interfaz de usuario completa. Tiempo real, por último, sugiere lo bien que el marco presenta los contenidos a tiempo, lo que se podría percibir como una actualización inmediata.

Ahora, en el año 2013, cuando abro mi copia de Visual Studio y trato de iniciar un proyecto ASP.NET, me enfrento a los cuadros de diálogo que se muestran en la Figura 3 y en la Figura 4.

New Web Project in Visual Studio 2012Figura 3 Nuevo proyecto web en Visual Studio 2012

New Project Template Dialog in Visual Studio 2012Figura 4 Cuadro de diálogo de Nueva plantilla de proyecto en Visual Studio 2012

Estas ventanas contienen algunas preguntas delicadas. ¿Con qué tipo de proyecto debo comenzar? ¿Qué plantilla me llevará más cerca de la solución y en forma más rápida? Y, ¿qué pasa si quiero incluir algunos componentes de cada plantilla? ¿Puedo crear una aplicación móvil con algunos controles de servidor y una Web API?

¿Tengo que elegir solo un enfoque?

¿Tengo que elegir solo un enfoque? La respuesta corta es no, no tenemos por qué seleccionar solo uno de estos marcos para crear una aplicación web. Existen técnicas que permiten usar los formularios Web Forms y MVC juntos y, contrario a lo que aparece en las ventanas de diálogo, se pueden agregar Web API y SignalR fácilmente como características de una aplicación web. Recuerde, todo el contenido de ASP.NET se representa a través de una serie de HttpHandler y HttpModule. Siempre que hagamos referencia a los controladores y módulos adecuados, podremos crear una solución con cualquiera de estos marcos.

Esto es lo más importante del concepto “One ASP.NET”: no elija solo uno de estos marcos, cree su solución con todas las piezas que mejor se adapten a sus necesidades. Tiene varias opciones, no se limite a una sola.

Para ver esto en acción, prepararé una pequeña aplicación web que tendrá un diseño unificado, una pantalla de búsqueda y una de creación para una lista de productos. La pantalla de búsqueda empleará las tecnologías de Web Forms y Web API y presentará actualizaciones automáticas de SignalR. La pantalla de creación se generará automáticamente con plantillas MVC. Y para que los formularios Web Forms se vean atractivos, usaré una biblioteca de controles de terceros, Telerik RadControls para ASP.NET AJAX. Una versión de prueba de estos controles está disponible en bit.ly/15o2Oab.

Configuración del proyecto de ejemplo y del diseño compartido

Para empezar, tengo que crear un proyecto mediante el cuadro de diálogo que se muestra en la Figura 3. Aunque aquí podría elegir una aplicación vacía o Web Forms, la solución más completa es la aplicación MVC. Comenzar con un proyecto MVC es una excelente opción, ya que obtenemos todas las herramientas de Visual Studio que nos ayudarán a configurar los modelos, las vistas y los controladores, así como la capacidad de agregar objetos de Web Forms en cualquier lugar de la estructura de archivos del proyecto. Existe la posibilidad de volver a agregar las herramientas de MVC en una aplicación web existente, al cambiar parte del contenido XML del archivo .csproj. Este proceso se puede automatizar al instalar el paquete NuGet llamado AddMvc3ToWebForms.

Para configurar los controles de Telerik y usarlos en este proyecto, debo realizar algunos remiendos en Web.config, para agregar los HttpHandler y HttpModule que normalmente se configuran en los proyectos Telerik RadControls estándar. Primero, agregaré un par de líneas para definir los controles AJAX de Telerik de la máscara de la interfaz de usuario:

<add key="Telerik.Skin" value="WebBlue" />
</appSettings>

Luego, agregaré el prefijo de etiqueta Telerik:

<add tagPrefix="telerik" namespace="Telerik.Web.UI" assembly="Telerik.Web.UI" />
</controls>

Haré las adiciones mínimas necesarias a Http­Handlers de Web.config para los controles de Telerik:

<add path="Telerik.Web.UI.WebResource.axd" type="Telerik.Web.UI.WebResource"
    verb="*" validate="false" />
</httpHandlers>

Y finalmente, haré las adiciones a los controladores de Web.config para los controles de Telerik:

<system.WebServer>
  <validation validateIntegratedModeConfiguration="false" />
  <handlers>
    <remove name="Telerik_Web_UI_WebResource_axd" />
    <add name="Telerik_Web_UI_WebResource_axd"
      path="Telerik.Web.UI.WebResource.axd"
      type="Telerik.Web.UI.WebResource" verb="*" preCondition="integratedMode" />

Ahora quiero crear una página de diseño para este proyecto, así que crearé una página site.master de Web Forms en Vistas | Carpeta compartida. Para este diseño de sitio, quiero agregar un logotipo y un menú estándar en todas las páginas. Para agregar la imagen del logotipo, simplemente la arrastro al diseño. Luego, para agregar un gran menú en cascada al diseño, arrastro un RadMenu del cuadro de herramientas de los controles al diseñador, justo debajo de la imagen. Desde la superficie del diseñador puedo crear rápidamente el menú, al hacer clic con el botón secundario en el control de menú y seleccionar Editar elementos, para obtener la ventana que se muestra en la Figura 5.

Telerik RadMenu Configuration Window
Figura 5 Ventana de configuración del RadMenu de Telerik

Me quiero enfocar en dos elementos de menú que se encuentran bajo Products: Search y New. Para cada uno de estos elementos, establecí las propiedades NavigateUrl y Text de la siguiente manera:

<telerik:RadMenuItem Text="Products">
  <Items>
    <telerik:RadMenuItem Text="Search" NavigateUrl="~/Product/Search" />
    <telerik:RadMenuItem Text="New" NavigateUrl="~/Product/New" />
  </Items>
</telerik:RadMenuItem>

Ahora que tengo configurado el menú, me enfrento a un problema, ya que definí el diseño con Web Forms y tengo que hospedar contenidos MVC. Este no es un problema trivial, pero tiene solución.

Puentes a la división: configuración de MVC para usar una página maestra de Web Forms

Al igual que la mayoría de las personas, prefiero las cosas simples. Quiero compartir el diseño que defino para este proyecto entre Web Forms y MVC. Existe una técnica bien documentada, diseñada por Matt Hawley, que demuestra cómo usar una página maestra de Web Forms con vistas basadas en Razor de MVC (bit.ly/ehVY3H). Emplearé esa técnica en este proyecto. Para crear este puente, configuraré un vista sencilla de Web Forms, llamada RazorView.aspx, que hace referencia a la página maestra:

<%@ Page Language="C#" AutoEventWireup="true"
  MasterPageFile="~/Views/Shared/Site.Master"
  Inherits="System.Web.Mvc.ViewPage<dynamic>" %>
<%@ Import Namespace="System.Web.Mvc" %>
<asp:Content id="bodyContent" runat="server" 
  ContentPlaceHolderID="body">
<% Html.RenderPartial((string)ViewBag._ViewName); %>
</asp:Content>

Para que mis controladores MVC usen esta vista y permitan que se ejecuten las vistas basadas en Razor, debo ampliar cada controlador para redirigir el contenido de la vista correctamente. Esto se logra mediante un método de extensión que vuelve a redirigir el modelo, ViewData y TempData de manera adecuada a través de RazorView.aspx, tal como se muestra en la Figura 6.

Figura 6 Método de extensión RazorView para volver a redirigir las vistas de MVC a través de una página maestra de Web Forms

public static ViewResult RazorView(this Controller controller,
  string viewName = null, object model = null)
{
  if (model != null)
    controller.ViewData.Model = model;
  controller.ViewBag._ViewName = !string.IsNullOrEmpty(viewName)
    ? viewName
    : controller.RouteData.GetRequiredString("action");
  return new ViewResult
  {
    ViewName = "RazorView",
    ViewData = controller.ViewData,
    TempData = controller.TempData
  };
}

Con este método listo, puedo redirigir fácilmente todas las acciones de MVC a través de la página maestra. El siguiente paso es establecer ProductsController, para que se puedan crear los productos.

MVC y la pantalla de creación de producto

La parte MVC de esta solución es una método bastante corriente en MVC. Definí un objeto de modelo sencillo, llamado BoardGame, en la carpeta Modelos de mi proyecto, tal como se aprecia en la Figura 7.

Figura 7 Objeto BoardGame

public class BoardGame
{
  public int Id { get; set; }
  public string Name { get; set; }
  [DisplayFormat(DataFormatString="$0.00")]
  public decimal Price { get; set; }
  [Display(Name="Number of items in stock"), Range(0,10000)]
  public int NumInStock { get; set; }
}

A continuación, mediante el uso de las herramientas estándar de MVC en Visual Studio, creo un ProductController vacío. Agrego una carpeta Views | Product, luego hago clic con el botón secundario en la carpeta Product y elijo Vista en el menú Agregar. Esta vista permitirá crear juegos de mesa nuevos, así que la crearé con las opciones que se muestran en la Figura 8.

Creating the “New” View
Figura 8 Creación de la vista “New”

Gracias a las herramientas y plantillas de MVC, no tengo que cambiar nada. La vista que se creó cuenta con etiquetas y validación y puede usar mi página maestra. La Figura 9 muestra cómo se define la acción New en ProductController.

Figura 9 Enrutamiento de ProductController a través de RazorView

public ActionResult New()
{
  return this.RazorView();
}
[HttpPost]
public ActionResult New(BoardGame newGame)
{
  if (!ModelState.IsValid)
  {
    return this.RazorView();
  }
  newGame.Id = _Products.Count + 1;
  _Products.Add(newGame);
  return Redirect("~/Product/Search");
}

Los desarrolladores de MVC deberían estar familiarizados con esta sintaxis, ya que el único cambio es la devolución de RazorView en vez de una vista. El objeto _Products es una colección estática y de solo lectura de productos artificiales que se definen en este controlador (en vez de usar una base de datos en el ejemplo):

public static readonly List<BoardGame> _Products = 
  new List<BoardGame>()
{
  new BoardGame() {Id=1, Name="Chess", Price=9.99M},
  new BoardGame() {Id=2, Name="Checkers", Price=7.99M},
  new BoardGame() {Id=3, Name="Battleship", Price=8.99M},
  new BoardGame() {Id=4, Name="Backgammon", Price= 12.99M}
};

Configuración de la página de búsqueda basada en Web Forms

Quiero que mis usuarios puedan acceder a la página de búsqueda del producto con una URL que no tenga la apariencia de una URL de Web Forms y que facilite las búsquedas. Gracias al lanzamiento de ASP.NET 2012.2, esto ahora se puede configurar fácilmente. Simplemente abro el archivo App_Start/RouteConfig.cs y llamo EnableFriendlyUrls para activar esta capacidad:

public static void RegisterRoutes(
    RouteCollection routes)
  {
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
    routes.EnableFriendlyUrls();
    routes.MapRoute(
      name: "Default",
      url: "{controller}/{action}/{id}",
      defaults: new { controller = "Home", action =
        "Index", id = UrlParameter.Optional }
    );
  }

Al agregar esta línea, ASP.NET redirigirá las solicitudes de /Product/Search al archivo físico que reside en /Product/Search.aspx

A continuación, quiero configurar una página de búsqueda que muestre una cuadrícula con los productos actuales y sus existencias. Crearé una carpeta de Product en el proyecto y le agregaré un nuevo formulario Web Forms llamado Search.aspx. En este archivo elimino todo el marcado, excepto la directiva @Page, y establezco MasterPageFile en el archivo Site.Master que se definió anteriormente. Elijo RadGrid de Telerik para mostrar los resultados, para poder configurar y mostrar los datos resultantes rápidamente:

<%@ Page Language="C#" AutoEventWireup="true"
  CodeBehind="Search.aspx.cs"
  Inherits="MvcApplication1.Product.Search"
  MasterPageFile="~/Views/Shared/Site.Master" %>
<asp:Content runat="server" id="main" ContentPlaceHolderID="body">
  <telerik:RadGrid ID="searchProducts" runat="server" width="500"
    AllowFilteringByColumn="True" CellSpacing="0" GridLines="None"
    AllowSorting="True">

La cuadrícula presentará automáticamente las columnas enlazadas del lado servidor y entregará funciones de ordenación y filtrado. Sin embargo, me gustaría que esto fuera más dinámico. Quiero que los datos se entreguen y administren del lado cliente. En este modelo, los datos se podrían transmitir y enlazar sin código del lado servidor en los formularios Web Forms. Para lograr esto, agrego una Web API que entregará y realizará las operaciones de datos.

Adición de Web API a la mezcla

Usaré el menú estándar Proyecto | Agregar nuevo para agregar un controlador de Web API llamado ProductController a una carpeta denominada “api” en mi proyecto. Esto me ayuda a aclarar las diferencias entre los controladores MVC y los controladores API. Esta API tiene una sola responsabilidad: proporcionar datos para la cuadrícula en formato JSON y permitir consultas OData. Para lograr esto en la Web API, escribo un método Get único y lo decoraré con el atributo Queryable:

[Queryable]
public IQueryable<dynamic> Get(ODataQueryOptions options)
{
  return Controllers.ProductController._Products.Select(b => new
  {
    Id = b.Id,
    Name = b.Name,
    NumInStock = b.NumInStock,
    Price = b.Price.ToString("$0.00")
  }).AsQueryable();
}

Este código devuelve mi colección de objetos BoardGame en la lista estática, junto con un poco de formato. Al decorar el método con [Queryable] y devolver una colección consultable, el marco de Web API controlará y procesará automáticamente el filtro de OData y ordenará los comandos. El método también se tiene que configurar con el parámetro de entrada ODataQueryOptions, para controlar los datos del filtro que envía la cuadrícula.

Para configurar la cuadrícula en Search.aspx para consumir esta nueva API, debo agregar algunas configuraciones del cliente en el marcado de la página. En este control de cuadrícula, defino el enlace de datos del cliente con un elemento ClientSettings y una configuración DataBinding. La configuración DataBinding muestra la ubicación de la API, el tipo de formato de respuesta y el nombre del controlador que se consultará, además del formato de la consulta OData. Con esta configuración, más una definición de las columnas que se presentarán en la cuadrícula, puedo ejecutar el proyecto y ver la cuadrícula enlazada a los datos en la lista de datos artificiales _Products, tal como se aprecia en la Figura 10.

Figura 10 Origen de formato completo de la cuadrícula

<telerik:RadGrid ID="searchProducts" runat="server" width="500"
  AllowFilteringByColumn="True" CellSpacing="0" GridLines="None"
  AllowSorting="True" AutoGenerateColumns="false"
  >
    <ClientSettings AllowColumnsReorder="True"
      ReorderColumnsOnClient="True"
      ClientEvents-OnGridCreated="GridCreated">
      <Scrolling AllowScroll="True" UseStaticHeaders="True"></Scrolling>
      <DataBinding Location="/api" ResponseType="JSON">
        <DataService TableName="Product" Type="OData"  />
      </DataBinding>
    </ClientSettings>
    <MasterTableView ClientDataKeyNames="Id" DataKeyNames="Id">
      <Columns>
        <telerik:GridBoundColumn DataField="Id" HeaderStyle-Width="0"
          ItemStyle-Width="0"></telerik:GridBoundColumn>
        <telerik:GridBoundColumn DataField="Name" HeaderText="Name"
          HeaderStyle-Width="150" ItemStyle-Width="150">
          </telerik:GridBoundColumn>
        <telerik:GridBoundColumn ItemStyle-CssClass="gridPrice"
          DataField="Price"
          HeaderText="Price" ItemStyle-HorizontalAlign="Right">
          </telerik:GridBoundColumn>
        <telerik:GridBoundColumn DataField="NumInStock"
          ItemStyle-CssClass="numInStock"
          HeaderText="# in Stock"></telerik:GridBoundColumn>
      </Columns>
    </MasterTableView>
  </telerik:RadGrid>

Activación de la cuadrícula con datos en tiempo real

La última pieza del rompecabezas es la capacidad de mostrar los cambios en los niveles de las existencias en tiempo real, a medida que se envían y reciben productos. Agregaré un concentrador SignalR para transmitir las actualizaciones y presentar los valores nuevos en la cuadrícula de búsqueda. Para agregar SignalR al proyecto, debo emitir los siguientes comandos de NuGet:

Install-Package -pre Microsoft.AspNet.SignalR.SystemWeb
Install-Package -pre Microsoft.AspNet.SignalR.JS

Estos comandos instalarán los componentes de servidor de ASP.NET, para el hospedaje dentro del servidor web de IIS y para dejar las bibliotecas cliente de JavaScript disponibles para los formularios Web Forms.

El componente del lado servidor de SignalR se llama Hub, y definiré el mío al agregar una clase llamada StockHub en la carpeta Hub de mi proyecto web. StockHub debe descender de la clase Microsoft.AspNet.SignalR.Hub. Definí un temporizador System.Timers.Timer estático para que la aplicación pueda simular los cambios en las existencias. Para esta simulación estableceré en forma aleatoria las existencias de un producto elegido al azar cada dos segundos (cuando se desencadene el controlador de eventos Elapsed del temporizador). Una vez establecido el nivel de existencias del producto, notificaré a todos los clientes que están conectados, al ejecutar un método en el cliente, llamado setNewStockLevel y que se muestra en la Figura 11.

Figura 11 Componente del lado servidor del concentrador SignalR

public class StockHub : Hub
{
  public static readonly Timer _Timer = new Timer();
  private static readonly Random _Rdm = new Random();
  static StockHub()
  {
    _Timer.Interval = 2000;
    _Timer.Elapsed += _Timer_Elapsed;
    _Timer.Start();
  }
  static void _Timer_Elapsed(object sender, ElapsedEventArgs e)
  {
    var products = ProductController._Products;
    var p = products.Skip(_Rdm.Next(0, products.Count())).First();
    var newStockLevel = p.NumInStock + 
      _Rdm.Next(-1 * p.NumInStock, 100);
    p.NumInStock = newStockLevel;
    var hub = GlobalHost.ConnectionManager.GetHubContext<StockHub>();
    hub.Clients.All.setNewStockLevel(p.Id, newStockLevel);
  }
}

Para que los datos de este concentrador queden accesibles desde el servidor, debo agregar una línea a RouteConfig, para indicar la presencia del concentrador. Al llamar a routes.MapHubs en el método RegisterRoutes de RouteConfig, finalizo la configuración del lado servidor de SignalR.

Luego, la cuadrícula tiene que escuchar estos eventos del servidor. Para lograrlo, debo agregar algunas referencias de JavaScript a la biblioteca de cliente de SignalR que se instala desde NuGet y al código que se genera del comando MapHubs. El servicio SignalR conecta y expone el método setNewStockLevel en el cliente mediante el código que se aprecia en la Figura 12.

Figura 12 Código del lado cliente de SignalR para activar la cuadrícula

<script src="/Scripts/jquery.signalR-1.0.0-rc2.min.js"></script>
<script src="/signalr/hubs"></script>
<script type="text/javascript">
  var grid;
  $().ready(function() {
      var stockWatcher = $.connection.stockHub;
      stockWatcher.client.setNewStockLevel = function(id, newValue) {
        var row = GetRow(id);
        var orgColor = row.css("background-color");
        row.find(".numInStock").animate({
          backgroundColor: "#FFEFD5"
        }, 1000, "swing", function () {
          row.find(".numInStock").html(newValue).animate({
            backgroundColor: orgColor
          }, 1000)
        });
      };
      $.connection.hub.start();
  })
</script>

En el controlador de eventos ready de jQuery, establecí una referencia llamada stockWatcher a StockHub, mediante la sintaxis $.connection.stockHub. Luego, definí el método setNewStockLevel en la propiedad client de stockWatcher. Este método emplea otros métodos auxiliares de JavaScript para atravesar la cuadrícula, encontrar la fila del producto adecuado y modificar las existencias con una extravagante animación en color, proporcionada por la interfaz de usuario de jQuery, tal como se aprecia en la Figura 13.

The Search Interface with Grid Generated by Web API and Maintained by SignalRFigura 13 Interfaz de búsqueda con una cuadrícula de Web API y mantenida por SignalR

En resumen

Demostré cómo crear un proyecto ASP.NET MVC y agregar un diseño de Web Forms, controles AJAX de terceros y formularios Web Forms que lo redirigen. Generé la interfaz de usuario con las herramientas de MVC y activé el contenido con Web API y SignalR. Este proyecto usó diferentes funciones de los cuatro marcos de ASP.NET para presentar una interfaz coherente y aprovechar las mejores características de cada componente. Usted puede hacer lo mismo. No elija solo un marco de ASP.NET para su próximo proyecto. En lugar de eso, opte por usarlos todos.

Jeffrey T. Fritz  es evangelizador desarrollador para Telerik con más de 15 años de experiencia en la creación de aplicaciones web multiempresa a gran escala con el modelo de software como servicio. Es orador de INETA y mantiene un blog en csharpfritz.com. Puede encontrarlo en Twitter en twitter.com/csharpfritz  y puede ponerse en contacto con él en jeff.fritz@telerik.com.

GRACIAS a los siguientes expertos técnicos por su ayuda en la revisión de este artículo: Scott Hanselman (Microsoft) y Scott Hunter (Microsoft)