martes, 22 de diciembre de 2015

El Demo del Día: Aplicación de Simple Página (SPA) en ASP.NET MVC

Aplicación de Simple Página (SPA) en ASP.NET MVC

Después de mucho tiempo vamos a presentar un nuevo Demo, esta vez comparando una Aplicación de Múltiples Páginas (MPA) con una Aplicación de Simple Página (SPA), pero sin usar ningún Framework SPA como AngularJS, Knockout, Ember, Meteor, Kendo UI, etc.

Introducción a una SPA

El modelo por defecto en ASP.NET sea WebForms o MVC es MPA, quiere decir que nuestro sitio web se compone de varias páginas las cuales se solicitan desde el cliente una a una y cada una descarga sus archivos necesarios: HTML, JavaScript, CSS, Imágenes, Fuentes, etc.
En cambio si nuestro sitio solo tuviera una simple página solo se cargaría una sola vez todos los archivos.

Ventajas de una SPA

La gran ventaja de una Aplicación de Simple Página (SPA) es que evita conectarse varias veces al servidor para solicitar cada página, es decir, favorece el trabajo desconectado, ya que al haber una sola página se pueda cargar todos los datos necesarios al inicio, así como los JavaScripts y las hojas de estilo.

Trabajar con una sola página ayuda a la Performance, ya que evita conexiones innecesarias al servidor web.

Usos de una SPA

Generalmente, los sitios web móviles usan este modelo, ya que solo se carga una página y se va presentando secciones (llamadas cartas o barajas) de ésta, muchos de los Frameworks para Aplicaciones Web Móviles trabajan de esta forma, por ejemplo jQuery Mobile, Bootstrap, etc.

Pero si nuestra Aplicación Web no tiene demasiadas páginas ni datos que cargar, también podemos usar este modelo, ahorrando conexiones al servidor, es decir, trabajando en forma desconectada.

Características de una SPA

Existen muchas características de una SPA:
- La principal es que la aplicación solo tenga una página.
- Aunque no es exclusividad de una SPA, actualmente el Diseño Web debe ser Adaptativo (Responsive Web Design).
- Otra característica importante (que también no es exclusividad de SPA) es que debe hacer llamadas Asíncronas JavaScript HTTP: AJAX (Xml), AJAJ (JSON) o AJAS (Strings).

Ejemplo de Comparación entre MPA y SPA en ASP.NET MVC

A continuación vamos a presentar un ejemplo donde comparamos una Aplicación sobre Pedidos que tiene los 2 modelos:

- MPA: Compuesto por un Controlador llamado MPA que tiene 4 métodos de acción y 4 vistas, llamadas Login, Categoria, Producto y Pedido. La navegación entre cada una implica ir al servidor y esperar su respuesta, para lo cual a propósito se ha puesto un tiempo de espera. Este modelo usa 4 archivos de JavaScript, uno para cada página.

- SPA: Compuesto por un Controlador llamado SPA que tiene solo un método de acción y una vista llamada Pedido. Por otra parte solo hay un archivo de JavaScript llamado Todo.js que realiza la navegación entre páginas (divs) y que no viaja al servidor sino solo se hace en el cliente.

Crear una Aplicación ASP.NET MVC 4 en C#

- Abrir Visual Studio 2012 y crear un proyecto de tipo "Aplicación web de ASP.NET MVC4" y asignarle como nombre: "SinglePageApplication".

- Seleccionar la opción "Vacio" y como motor de vista "Razor" y "Aceptar".

Crear el Controlador MPA

- Clic derecho a la carpeta Controllers y seleccionar "Agregar" y luego "Controlador".

- Ingresar como nombre "MPAController" y luego escribir el siguiente código:

using System;
using System.Web;
using System.Web.Mvc;

namespace SinglePageApplication.Controllers
{
    public class MPAController : Controller
    {
        public ActionResult Login()
        {
            return View();
        }

        public ActionResult Categoria()
        {
            //Simular que esta conectandose a la BD para traer las Categorias
            System.Threading.Thread.Sleep(2000);
            return View();
        }

        public ActionResult Producto()
        {
            //Simular que esta conectandose a la BD para traer los Productos x Categoria
            System.Threading.Thread.Sleep(4000);
            return View();
        }

        public ActionResult Pedido()
        {
            //Simular que esta leyendo del TempData los Pedidos
            System.Threading.Thread.Sleep(1000);
            return View();
        }
    }
}

Crear una Hoja de Estilo para todas las vistas

- Crear una carpeta para los archivos CSS llamado: "Estilos".

- Crear una hoja de estilo llamada "ACME.css" dentro de la carpeta Estilos y escribir el siguiente código:

body {
    background-color:aqua;
    width:100%;
}
.Titulo {
    background-color:blue;
    color:white;
    font-family:Arial;
    font-size:xx-large;
    text-align:center;  
}
.Subtitulo {
    background-color:white;
    color:blue;
    font-family:Arial;
    font-size:x-large;
    text-align:center;
}
table {
    width:100%;
}
.FilaCabecera {
    background-color:lightgray;
    color:black;
}
.FilaDatos {
    background-color:white;
    color:black;
}
.Pagina {
    display:none;
}

Crear las Vistas para MPA

- Ir al controlador MPA, clic derecho sobre el nombre del método "Login" y crear una vista con el mismo nombre que tenga el siguiente código HTML:

@{
    Layout = null;
}
<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Login</title>
    <link href="~/Estilos/ACME.css" rel="stylesheet" />
</head>
<body>
    <div id="divLogin">
        <div id="divCabeceraLogin">
            <div id="divTituloLogin" class="Titulo">
                <span id="spnTitulo">Aplicación de Múltiples Paginas (MPA)</span>
            </div>
            <div id="divSubtituloLogin" class="Subtitulo">
                <span id="spnSubtituloLogin">Login del Sistema</span>
            </div>
        </div>
        <div id="divCuerpoLogin">
            <div id="divEtiquetaUsuario">
                <span id="spnUsuario">Usuario: </span>
            </div>
            <div id="divTextoUsuario">
                <input id="txtUsuario" type="text" />
            </div>
            <div id="divEtiquetaClave">
                <span id="spnClave">Clave: </span>
            </div>
            <div id="divTextoClave">
                <input id="txtClave" type="password" />
            </div>
            <div id="divBotonesLogin">
                <input id="btnAceptar" type="button" value="Aceptar" />
            </div>
        </div>
        <div id="divPieLogin">
            <div id="divUbicacion" class="Subtitulo">
                <span id="spnUbicacion">Lima - Perú</span>
            </div>
        </div>
    </div>
    <script src="~/Scripts/Login.js"></script>
</body>
</html>

- Ir al controlador MPA, clic derecho sobre el nombre del método "Categoria" y crear una vista con el mismo nombre que tenga el siguiente código HTML:

@{
    Layout = null;
}
<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Categoria</title>
    <link href="~/Estilos/ACME.css" rel="stylesheet" />
</head>
<body>
    <div id="divCategoria">
        <div id="divCabeceraCategoria">
            <div id="divSubtituloCategoria" class="Subtitulo">
                <span id="spnSubtituloCategoria">Selecciona una Categoria</span>
            </div>
        </div>
        <div id="divListaCategoria">
            <span id="spnListaCategoria">Aqui se debe mostrar un lista de Categorias</span>
        </div>
        <div id="divPieCategoria">
            <div id="divNavegarCategoria" class="Subtitulo">
                <a id="aCatProd" href="~/MPA/Producto">Productos</a>
            </div>
        </div>
    </div>
    <script src="~/Scripts/Categoria.js"></script>
</body>
</html>

- Ir al controlador MPA, clic derecho sobre el nombre del método "Producto" y crear una vista con el mismo nombre que tenga el siguiente código HTML:

@{
    Layout = null;
}
<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Producto</title>
    <link href="~/Estilos/ACME.css" rel="stylesheet" />
</head>
<body>
    <div id="divProducto">
        <div id="divCabeceraProducto">
            <div id="divSubtituloProducto" class="Subtitulo">
                <span id="spnSubtituloProducto">Agrega Productos a tu Pedido</span>
            </div>
        </div>
        <div id="divListaProducto">
            <span id="spnListaProducto">Aqui se debe mostrar un lista de Productos</span>
        </div>
        <div id="divPieProducto">
            <div id="divNavegarProducto" class="Subtitulo">
                <a id="aProdPed" href="~/MPA/Pedido">Pedidos</a>
                <a id="aProdCat" href="~/MPA/Categoria">Regresar</a>
            </div>
        </div>
    </div>
    <script src="~/Scripts/Producto.js"></script>
</body>
</html>

- Ir al controlador MPA, clic derecho sobre el nombre del método "Pedido" y crear una vista con el mismo nombre que tenga el siguiente código HTML:

@{
    Layout = null;
}
<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Pedido</title>
    <link href="~/Estilos/ACME.css" rel="stylesheet" />
</head>
<body>
    <div id="divPedido">
        <div id="divCabeceraPedido">
            <div id="divSubtituloPedido" class="Subtitulo">
                <span id="spnSubtituloPedido">Agrega Productos a tu Pedido</span>
            </div>
        </div>
        <div id="divListaPedido">
            <span id="spnListaPedido">Aqui se debe mostrar un lista de Pedidos</span>
        </div>
        <div id="divPiePedido">
            <div id="divNavegarPedido" class="Subtitulo">
                <a id="aPedCat" href="~/MPA/Categoria">Regresar</a>
            </div>
        </div>
    </div>
    <script src="~/Scripts/Pedido.js"></script>
</body>
</html>

Crear los JavaSripts para MPA

- Crear una carpeta para los archivos js llamado: "Scripts"

- Crear una archivo de JavaScript llamado "Login.js" dentro de la carpeta Scripts y escribir el siguiente código:

window.onload = function () {
    var btnAceptar = document.getElementById("btnAceptar");
    btnAceptar.onclick = function () {
        var txtUsuario = document.getElementById("txtUsuario");      
        if (txtUsuario.value == "") {
            alert("Ingresa el Usuario");
            txtUsuario.focus();
            return false;
        }
        var txtClave = document.getElementById("txtClave");
        if (txtClave.value == "") {
            alert("Ingresa la Clave");
            txtClave.focus();
            return false;
        }
        if (txtUsuario.value == "Luis" && txtClave.value == "123") {
            window.location.href = "MPA/Categoria";
        }
        else {
            alert("Login invalido");
        }
    }
}

- Crear una archivo de JavaScript llamado "Categoria.js" dentro de la carpeta Scripts y escribir el siguiente código:

window.onload = function () {
    alert("Cargando Categorias");
}

- Crear una archivo de JavaScript llamado "Producto.js" dentro de la carpeta Scripts y escribir el siguiente código:

window.onload = function () {
    alert("Cargando Productos");
}

- Crear una archivo de JavaScript llamado "Pedido.js" dentro de la carpeta Scripts y escribir el siguiente código:

window.onload = function () {
    alert("Cargando Pedidos");
}

Configurar el Inicio del MPA

- Ir a la carpeta "App_Start" y abrir el archivo "RouteConfig.css" y cambiar el nombre del controlador y la acción que inicia.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;

namespace SinglePageApplication
{
    public class RouteConfig
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

            routes.MapRoute(
                name: "Default",
                url: "{controller}/{action}/{id}",
                defaults: new { controller = "MPA", action = "Login", id = UrlParameter.Optional }
            );
        }
    }
}

Probar y Ejecutar la Aplicación de Múltiples Páginas (MPA)

- Grabar y ejecutar la aplicación pulsando F5 y se mostrará una ventana similar a la siguiente figura:


- Ingresar como usuario "Luis" y como clave "123" y "Aceptar", observando que se demora en cargar la segunda página de Categoria, similar a la siguiente figura:


- Clic al enlace de Productos y después de esperar un momento se mostrará la página de Producto, similar a la siguiente figura:


- Clic al enlace de Pedidos y después de esperar un momento se mostrará la página de Pedido, similar a la siguiente figura:


- Si deseas en cualquier página puedes seleccionar el enlace "Regresar".

Nota: En todos los enlaces al dar clic se irá al servidor para cargar la página ya que el modelo es MPA.

Crear el Controlador SPA

- Clic derecho a la carpeta Controllers y seleccionar "Agregar" y luego "Controlador".

- Ingresar como nombre "SPAController" y luego escribir el siguiente código:

using System;
using System.Web;
using System.Web.Mvc;

namespace SinglePageApplication.Controllers
{
    public class SPAController : Controller
    {
        public ActionResult Pedido()
        {
            return View();
        }
    }
}

Crear la Vista para SPA

- Ir al controlador SPA, clic derecho sobre el nombre del método "Pedido" y crear una vista con el mismo nombre que tenga el siguiente código HTML:

@{
    Layout = null;
}
<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Pedido</title>
    <link href="~/Estilos/ACME.css" rel="stylesheet" />
</head>
<body>
    <div id="divTodo">
        <div id="divLogin" class="Pagina">
            <div id="divCabeceraLogin">
                <div id="divTituloLogin" class="Titulo">
                    <span id="spnTitulo">Aplicación de Simple Pagina (SPA)</span>
                </div>
                <div id="divSubtituloLogin" class="Subtitulo">
                    <span id="spnSubtituloLogin">Login del Sistema</span>
                </div>
            </div>
            <div id="divCuerpoLogin">
                <div id="divEtiquetaUsuario">
                    <span id="spnUsuario">Usuario: </span>
                </div>
                <div id="divTextoUsuario">
                    <input id="txtUsuario" type="text" />
                </div>
                <div id="divEtiquetaClave">
                    <span id="spnClave">Clave: </span>
                </div>
                <div id="divTextoClave">
                    <input id="txtClave" type="password" />
                </div>
                <div id="divBotonesLogin">
                    <input id="btnAceptar" type="button" value="Aceptar" />
                </div>
            </div>
            <div id="divPieLogin">
                <div id="divUbicacion" class="Subtitulo">
                    <span id="spnUbicacion">Lima - Perú</span>
                </div>
            </div>
        </div>
        <div id="divCategoria" class="Pagina">
            <div id="divCabeceraCategoria">
                <div id="divSubtituloCategoria" class="Subtitulo">
                    <span id="spnSubtituloCategoria">Selecciona una Categoria</span>
                </div>
            </div>
            <div id="divListaCategoria">
                <span id="spnListaCategoria">Aqui se debe mostrar un lista de Categorias</span>
            </div>
            <div id="divPieCategoria">
                <div id="divNavegarCategoria" class="Subtitulo">
                    <a id="aCatProd" href="#" data-url="divProducto">Productos</a>
                </div>
            </div>
        </div>
        <div id="divProducto" class="Pagina">
            <div id="divCabeceraProducto">
                <div id="divSubtituloProducto" class="Subtitulo">
                    <span id="spnSubtituloProducto">Agrega Productos a tu Pedido</span>
                </div>
            </div>
            <div id="divListaProducto">
                <span id="spnListaProducto">Aqui se debe mostrar un lista de Productos</span>
            </div>
            <div id="divPieProducto">
                <div id="divNavegarProducto" class="Subtitulo">
                    <a id="aProdPed" href="#" data-url="divPedido">Pedidos</a>
                    <a id="aProdCat" href="#" data-url="divCategoria">Regresar</a>
                </div>
            </div>
        </div>
        <div id="divPedido" class="Pagina">
            <div id="divCabeceraPedido">
                <div id="divSubtituloPedido" class="Subtitulo">
                    <span id="spnSubtituloPedido">Agrega Productos a tu Pedido</span>
                </div>
            </div>
            <div id="divListaPedido">
                <span id="spnListaPedido">Aqui se debe mostrar un lista de Pedidos</span>
            </div>
            <div id="divPiePedido">
                <div id="divNavegarPedido" class="Subtitulo">
                    <a id="aPedCat" href="#" data-url="divCategoria">Regresar</a>
                </div>
            </div>
        </div>
    </div>
    <script src="~/Scripts/Todo.js"></script>
</body>
</html>

Crear el JavaSript para SPA

- Crear una archivo de JavaScript llamado "Todo.js" dentro de la carpeta Scripts y escribir el siguiente código:

window.onload = function () {
    //Mostrar la primera pagina
    var divLogin = document.getElementById("divLogin");
    if (divLogin) {
        divLogin.style.display = "inline";
        scriptLogin();
    }
    //Programar toda la navegación
    var enlaces = document.getElementsByTagName("a");
    if (enlaces != null && enlaces.length > 0) {
        var n = enlaces.length;
        var enlace;
        var pagina;
        var script;
        for (var i = 0; i < n; i++) {
            enlace = enlaces[i];
            enlace.onclick = function () {
                ocultarPaginas();
                var id = this.getAttribute("data-url");
                pagina = document.getElementById(id);
                if (pagina != null) {
                    pagina.style.display = "inline";
                    script = id.replace("div", "script");
                    window[script]();
                }
            }
        }
    }
}

function ocultarPaginas() {
    var paginas = document.getElementsByClassName("Pagina");
    if (paginas != null && paginas.length > 0) {
        var n = paginas.length;
        var pagina;
        for (var i = 0; i < n; i++) {
            pagina = paginas[i];
            pagina.style.display = "none";
        }
    }
}

function scriptLogin() {
    var btnAceptar = document.getElementById("btnAceptar");
    btnAceptar.onclick = function () {
        var txtUsuario = document.getElementById("txtUsuario");
        if (txtUsuario.value == "") {
            alert("Ingresa el Usuario");
            txtUsuario.focus();
            return false;
        }
        var txtClave = document.getElementById("txtClave");
        if (txtClave.value == "") {
            alert("Ingresa la Clave");
            txtClave.focus();
            return false;
        }
        if (txtUsuario.value == "Luis" && txtClave.value == "123") {
            var divCategoria = document.getElementById("divCategoria");
            if (divCategoria) {
                ocultarPaginas();
                divCategoria.style.display = "inline";
                scriptCategoria();
            }
        }
        else {
            alert("Login invalido");
        }
    }
}

function scriptCategoria() {
    alert("Cargando Categorias");
}

function scriptProducto() {
    alert("Cargando Productos");
}

function scriptPedido() {
    alert("Cargando Pedidos");
}

Nota: En el evento load de la ventana se muestra solo el primer div (Login) y el resto esta oculto por el estilo de la clase Pagina. Además se obtiene todos los enlaces y se programa para que se navegue al div que especifique el atributo "data-url" ejecutando su script asociado que debe tener el prefijo script seguido del nombre usando: window[script]().

Configurar el Inicio del SPA

- Ir a la carpeta "App_Start" y abrir el archivo "RouteConfig.css" y cambiar el nombre del controlador y la acción que inicia.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;

namespace SinglePageApplication
{
    public class RouteConfig
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

            routes.MapRoute(
                name: "Default",
                url: "{controller}/{action}/{id}",
                defaults: new { controller = "SPA", action = "Pedido", id = UrlParameter.Optional }
            );
        }
    }
}

Probar y Ejecutar la Aplicación de Simple Página (SPA)

- Grabar y ejecutar la aplicación pulsando F5 y se mostrará una ventana similar a la siguiente figura:


- Ingresar como usuario "Luis" y como clave "123" y "Aceptar", observando que no se demora en cargar nada la segunda página de Categoria ya que es un div, similar a la siguiente figura:


- Clic al enlace de Productos e inmediatamente se mostrará la página de Producto ya que es un div, similar a la siguiente figura:


- Clic al enlace de Pedidos y e inmediatamente se mostrará la página de Pedido que también es un div, similar a la siguiente figura:


- Si deseas en cualquier página puedes seleccionar el enlace "Regresar"y navegará a la sección especificada.

Nota: En todos los enlaces al dar clic se ocultarán todos los divs y se mostrará uno solo ya que el modelo es SPA, es decir, no se va al servidor sino solo se muestra en el cliente.

Comentario Final

En este post un poco largo hemos querido mostrar como se puede crear sin ningún Framework de JavaScript una Aplicación de Simple Pagina (SPA) tan solo usando divs y un poco de JavaScript, lo cual es muy útil para trabajar en forma desconectada en aplicaciones pequeñas, por ejemplo, las aplicaciones web móviles.

Las personas que desean crear una SPA en MVC usando Frameworks, encontrarán muchos Blogs y Sitios Web, ya que para esto existe mucha información, tanto oficial como no oficial, por ejemplo les alcanzo un enlace del MSDN:
ASP.NET MVC 5: A .NET Developer Primer for SPA

Finalmente, como siempre hemos querido demostrar como hacerlo nosotros mismos sin necesidad de descargar nada, ni en el servidor ni en el cliente, ya que esto aumenta el tamaño de la aplicación perjudicando el ancho de banda, es decir, la performance.

Descarga del Código
SinglePageApplication