Catálogo en ASP.NET MVC3 sin usar BBDD

Publicado 14/4/2012 0:38 por Eduard Tomàs i Avellana

Bueno… este es un post por “encargo”… Hoy he recibido un tweet de @JanoRuiz que decía lo siguiente: Hola, Saludos, una Consulta, Como Hacer Un Catalogo En asp.net mvc3 Sin Usar BD, Hacer Altas, Bajas y Modificaciones.

Bueno, vamos a explorar algunas “formas de hacerlo”… :D

Vamos a utilizar el siguiente modelo para representar los productos:

  1. public class Producto
  2. {
  3.     public string Nombre { get; set; }
  4.     public int Precio { get; set; }
  5. }

Y el catálogo en sí:

  1. public class Catalogo
  2. {
  3.     private readonly string _nombre;
  4.     private readonly List<Producto> _productos;
  5.  
  6.     public Catalogo()
  7.     {
  8.         _productos = new List<Producto>();
  9.     }
  10.  
  11.     public IEnumerable<Producto> Productos
  12.     {
  13.         get { return _productos; }
  14.     }
  15.  
  16.     public void AnyadirProducto(Producto p)
  17.     {
  18.         _productos.Add(p);
  19.     }
  20. }

Opción 1. Usar la sesión

Bueno, la opción más sencilla es usar la sesión para mantener nuestro catálogo. Para ello directamente creamos el catálogo cuando se crea la sesión, añadiendo el siguiente código en global.asax.cs:

  1. protected  void Session_Start()
  2. {
  3.     Session["catalogo"] = new Catalogo();
  4. }

La sesión se crea en la primera petición que realiza el usuario a nuestra web y está vinculada al usuario (usuario en este contexto = ventana de navegador, por norma general).

Ahora cuando queramos acceder al catálogo:

  1. [HttpPost]
  2. public ActionResult Nuevo(Producto producto)
  3. {
  4.     var catalogo = Session["Catalogo"] as Catalogo;
  5.     catalogo.AnyadirProducto(producto);
  6.     return RedirectToAction("Index");
  7. }

Y listos!

Un par de consideraciones sobre el uso de sesión:

  1. No es persistente. Es decir, cuando el usuario cierre la ventana del navegador y vuelva más tarde a nuestra web, habremos perdido los datos. Si queremos hacer los datos persistentes hay que guardarlos en algún almacenamiento persistente, esto es… una base de datos :P (aunque podría ser un fichero en el servidor también).
  2. Se crea incluso si el usuario no está autenticado. Si queremos que tan solo los usuarios autenticados tengan catálogos, podemos crear el catálogo justo cuando autenticamos al usuario (y por supuesto tener protegidos con [Authorize] las acciones correspondientes de los controladores que acceden a la sesión).

Por lo tanto, la sesión nos evita el uso de una BBDD solo mientras el usuario navega por nuestra aplicación, pero si queremos que sea persistente deberemos usar un almacenamiento externo que persista (o sea, una BBDD).

Opcion 2: Serializar los datos en cada petición

De acuerdo, la sesión la podemos usar pero es un recurso digamos… caro. Tiene ciertas implicaciones en escalabilidad y tolerancia a fallos así que quizá no queremos o podemos usarla. Hay alguna manera de mantener el estado como si tuviéramos sesión? Pues sí, serializar los datos de la sesión en cada petición y colocarlas en un campo hidden. ¿Te suena a algo esto? ¿Cómo? ¿Quien ha dicho viewstate? Premio!

Vamos a ver una prueba de concepto de esto. Para empezar necesitamos una clase que dado un objeto nos devuelva su representación en Base64 y vicerversa:

  1. public class ObjectToBase64
  2. {
  3.  
  4.     public static string ToBase64(object source)
  5.     {
  6.         var base64 = string.Empty;
  7.         if (source != null)
  8.         {
  9.             using (var ms = new MemoryStream())
  10.             {
  11.                 var formatter = new BinaryFormatter();
  12.                 formatter.Serialize(ms, source);
  13.                 var buffer = ms.ToArray();
  14.                 base64 = Convert.ToBase64String(buffer);
  15.             }
  16.         }
  17.         return base64;
  18.     }
  19.  
  20.     public static object FromBase64 (string base64)
  21.     {
  22.         if (string.IsNullOrEmpty(base64)) return null;
  23.         var buffer = Convert.FromBase64String(base64);
  24.         using (var ms = new MemoryStream(buffer))
  25.         {
  26.             var formatter = new BinaryFormatter();
  27.             return formatter.Deserialize(ms);
  28.         }
  29.     }
  30. }

La idea ahora es que:

  1. Cada acción tiene un parámetro adicional (p.ej. llamado _viewstate).
  2. Este parámetro debe ser pasado en todas las llamadas, ya sea via GET o via POST.

Así, si p.ej. en una vista tenemos un formulario, podemos hacer:

  1. @if (ViewBag.ViewState != null)
  2. {
  3.     <input type="hidden" name="_viewstate" value="@ViewBag.ViewState"/>
  4. }

Y si generamos un enlace:

@Html.ActionLink("Nuevo", "Nuevo", new {_viewstate = ViewBag.ViewState})

Del mismo modo en el caso de una redirección de un controlador a otro:

return RedirectToAction("Index", new { _viewstate = ObjectToBase64.ToBase64(catalogo) });

Bueno… creo que la idea se entiende, no? ;-)

En el fondo lo que estamos haciendo es serializar la “sesión” cada vez y mandarla arriba y abajo. Hacerlo via GET (como en el ActionLink o en RedirectToAction) hace que esta sesión sea visible en la URL:

image

Lo ideal es usar siempre POST (que es lo que hace Webforms). Esta técnica se puede mejorar bastante, pero bueno… como prueba de concepto no está mal. Si alguna vez queréis usar esta técnica echad un vistazo al post del Maestro: http://www.variablenotfound.com/2010/07/un-viewstate-en-aspnet-mvc.html

He de decir que a mi esta técnica no me gusta nada, salvo que no sea para casos muy, muy, muy puntuales. Y por supuesto tiene el mismo “problema” que la sesión: no es persistente.

Y así llegamos a la opción 3…

Opción 3 – LocalStorage

Bueno… en la Opción 1 se trata de mantener el estado en el servidor y la Opción 2 mantiene el estado en el cliente pasándolo a través de campos hidden o en la URL. Ahora toca la tercera opción: mantener el estado en el cliente. No solo eso, sino que además lo haremos persistente. ¿Y eso como? Pues… usaremos un almacenamiento persistente… En este caso el Local Storage.

Local Storage es básicamente un almacén de objetos en el navegador. Llámalo base de datos si quieres, pero ten presente que no hay tablas, ni filas, ni columnas. Se trataría más bien de un diccionario (clave, valor). ¿Y qué guardas? Pues cadenas. Simple y llanamente cadenas.

La idea es la siguiente:

  1. El catálogo se guarda en el local storage del cliente
  2. El cliente añade el producto en el almacén

Todo se realiza en cliente, no hay comunicación en el servidor. Evidentemente en una aplicación real antes de añadir el producto en cliente, este preguntaría al servidor (mediante una llamada ajax) si puede añadirlo. Pero en nuestra prueba de concepto no lo haremos… :P

La prueba de concepto es muy, sencilla, en la página de Index accedemos al localStorage, lo recorremos y ponemos su contenido en una lista:

  1. <h2>Index</h2>
  2. <script type="text/javascript">
  3.     $(document).ready(function () {
  4.         for (var x = 0; x <= localStorage.length - 1; x++) {
  5.             var key = localStorage.key(x);
  6.             var value = JSON.parse(localStorage.getItem(key));
  7.             $("#lstCatalog").append(
  8.                 $("<li>").append(value.nombre + " " + value.precio)
  9.             );
  10.         }
  11.     });
  12.  
  13.  
  14. </script>
  15.  
  16. <ul id="lstCatalog">
  17.     
  18. </ul>

Fijaos en el uso de JSON.parse para pasar la cadena JSON que guardamos en el localStorage a un objeto javascript. El resto del código se entiende bastante bien, ¿no?

Y la vista de alta? Bueno, pues se trata de un formulario de alta normal, salvo que el botón de enviar no es un botón de submit sino un botón normal. Y luego tenemos el siguiente código js:

  1. <script type="text/javascript">
  2.     $(document).ready(function () {
  3.         $("#btnAdd").click(function () {
  4.             var nombre = $("#Nombre").val();
  5.             var precio = $("#Precio").val();
  6.             localStorage.setItem(nombre, JSON.stringify({ nombre: nombre, precio: precio }));
  7.             location.href = "@Url.Action("Index")";
  8.         });
  9.     });
  10. </script>

(btnAdd es el ID del botón del formulario).

Es simple: accedemos al valor del input “Nombre” y del “Precio”. Construimos un objeto javascript, lo pasamos a cadena con JSON.stringify y lo guardamos en el localStorage. Para la clave usamos el nombre del producto. Es aquí, donde antes de insertar el elemento en el localStorage, nos iriamos al servidor via Ajax para confirmar que podemos añadirlo.

Al final nos volvemos a la página de Index.

Y ya hemos terminado. Como prueba de concepto, no está mal: 5 minutos para tener una lista de productos en el cliente. El servidor ni se ha enterado, este es el código del controlador:

  1. public class LocalStorageController : Controller
  2. {
  3.     public ActionResult Index()
  4.     {
  5.         return View();
  6.     }
  7.  
  8.     public ActionResult Nuevo()
  9.     {
  10.         return View();
  11.     }
  12. }

Por supuesto falta soporte para eliminar un elemento del localStorage, pero eso es trivial usando los métodos clear() (borra todo el localStorage) y removeItem (borra un elemento).

Nota: localStorage es persistente. Es decir, entre ejecución y ejecución de la aplicación (cerrar el browser y abrirlo de nuevo mañana) los datos se mantienen. Viene a ser como una cookie pero con esteroides. Si quieres usar un localStorage “no persistente” (que dure solo mientras la ventana está abierta” existe el sessionStorage. Además recuerda que localStorage, al estar en el cliente, es totalmente hackeable.

En resumen: Realizar un catálogo en MVC sin base de datos en el servidor es posible si queremos que los datos no sean persistentes. Pero si queremos que esos sean persistentes (que el usuario pueda cerrar el navegador, abrirlo y continuar viendo sus datos) debe usarse una BBDD (o algún otro almacenamiento persistente) sí o sí. Hasta antes de HTML5 este almacenamiento persistente tenía que estar en el servidor. Con HTML5 y localStorage puede estar en el cliente.

Nota final:

Además de esas tres opciones se me ocurren al menos DOS más para realizar lo mismo:

  1. Usar cookies (seria la versión pre-html5 de usar el localStorage, pero hay serias limitaciones en el tamaño de los datos a guardar).
  2. Usar indexedDB (sería parecido en filosofía a usar el localStorage).

Algunos links:

  1. WebStorage (localStorage y sessionStorage): http://dev.w3.org/html5/webstorage/
  2. Sesión en ASP.NET MVC (en mi blog): http://geeks.ms/blogs/etomas/archive/2010/06/30/asp-net-mvc-q-amp-a-c-243-mo-usar-la-sesi-243-n.aspx
  3. IndexedDB: http://www.w3.org/TR/IndexedDB/

Espero que el ejercicio os haya sido de interés! ;-)

PD: Os dejo un zip con el código de las opciones 2 y 3 en https://skydrive.live.com/redir.aspx?cid=6521c259e9b1bec6&resid=6521C259E9B1BEC6!233&parid=6521C259E9B1BEC6!167&authkey=!AHLp6PeH4Uw1hEg

Archivado en: ,
Comparte este post: