Single Page Applications – Parte 3 – UI Patterns, KnockoutJS

En esta oportunidad vamos a hablar sobre como estructurar la aplicación y como organizar el código. Los patrones más comúnmente usados para organizar aplicaciones en el “front-end” son 2: MVC y MVVM.

MVC en Javascript

MVC es probablemente el patrón de presentación más antiguo que existe y es utilizado en múltiples lenguajes de programación. Dentro de Javascript existen las siguientes implementaciones:

Examinemos los componentes de este patrón dentro de Javascript:

Models: Contienen los datos que se van a manejar en la aplicación, muchas veces implementan el Observer Pattern(Publisher/Subscriber Pattern) para notificar a las vistas que necesitan actualizarse cuando se ha producido algún cambio en el modelo.

Views: No necesariamente constituyen directamente el HTML que se va a mostrar, sino son los encargados de renderizar el modelo, en varios de los casos utilizando “Templates”.

Controllers: Intermediarios entre las Vistas y el Modelo, coordinan la actualización del Modelo de acuerdo a los cambios producidos en las vistas y viceversa. Varias implementaciones MVC en Javascript tienen un concepto diferente de lo que es convencionalmente llamado un “Controller”, por esto podemos encontrar que la “C” puede estar representada por “Colecciones” de objetos o “Rutas” o dependiendo del caso.

MVVM en Javascript

Es otro patrón que busca separar el desarrollo de la interfaz de usuario, de la lógica de la aplicación. Fue originalmente definido por Microsoft para ser usado junto con WPF, pero ha sido adoptado por otros lenguajes.

En estos últimos años se han creados algunas implementaciones de este patrón dentro de Javascript:

Examinemos los componentes de este patrón dentro de Javascript:

Models: Representan la información del dominio sobre el cuál trabajará la aplicación. Dentro de este patrón, no siempre los miembros del modelo se encuentran directamente en código Javascript, muchas veces los objetos del modelo se encuentran en el lado del servidor.

Views: A diferencia del patrón MVC, estas vistas son “activas” es decir no solo se encargan de mostrar los datos sino también envían al ViewModel información sobre eventos. Generalmente son documentos HTML que contienen “bindings” hacia el ViewModel.

ViewModels: Representan a las vistas, a sus datos y operaciones que se realizan sobre estas. Se mantienen sincronizados con las vistas a través de los “bindings” que funcionan en doble sentido.

Para nuestra aplicación vamos a utilizar el Patrón MVVM junto con KnockoutJS.

Lo primero que haremos será agregar los scripts necesarios para usar KnockoutJS dentro del archivo /Views/Shared/Layout.cshtml. KnockoutJS lo podemos descargar desde su página web o utilizando Nuget.

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <title>Fourth Coffee</title>
    <link href="~/Content/Site.css" rel="stylesheet" type="text/css" />
    <script src="~/Scripts/Lib/jquery-1.6.2.min.js" type="text/javascript"></script>
    <script src="~/Scripts/Lib/knockout-2.1.0.js" type="text/javascript"></script>
</head>

Creamos la siguiente estructura de directorios para nuestros archivos Javascript.

FolderStructure

  • App: Contendrá los archivos de la aplicación que han sido desarrollados por nosotros..
    • Data: Contendrá los objetos mediante los cuales accederemos a los servicios REST.
    • Models: Contendrá a los objetos que constituyen el Modelo de la aplicación.
    • ViewModels: Contendrá a los ViewModels de la aplicación.
  • Lib: Contendrá los archivos pertenecientes a frameworks y librerías externas: Jquery, KnockoutJS, etc.

Creamos el HomeController, la acción Index y el archivo /Views/Home/Index.cshtml sin ningún contenido.

public class HomeController : Controller
{
   
public ActionResult
Index()
    {
       
return View();
    }
}

Luego de esto, podremos levantar la aplicación y ver una pantalla inicial.

fourthcoffee

Ahora crearemos las Vistas, Modelos y ViewModels de nuestra aplicación. En este post nos vamos a enfocar únicamente en la primera pantalla y vamos a crear los elementos necesarios para su funcionamiento.

Pantalla: Selección de Producto

Pantalla a través de la cuál un usuario puede seleccionar cuál será el producto que desea comprar.

spa_primerapagina

Creamos el ViewModel para esta pantalla dentro del archivo /Scripts/App/ViewModels/chooseproductviewmodel.js. Recordemos que un ViewModel es una representación de los datos y operaciones que se pueden realizar en la pantalla, entonces lo que vamos a encontrar en este ViewModel será una propiedad que representé al “Listado de “Productos” que vamos a mostrar en la pantalla y una función que nos permita gestionar el click al botón “Order Now”.

var ChooseProductViewModel = function () {
   
var self = this
;
    self.products = ko.observableArray();

    self.goToPlaceOrder =
function
(product) {
       
//TODO: Mostrar la vista para ingresar la orden de compra del producto 
    };

};

Podemos observar la sentencia ko.observableArray(), de esta manera le indicamos a KnockoutJS que cree una sincronización en doble sentido entre la propiedad y la vista. Si el valor de una propiedad “observable” es actualizado en el ViewModel este se verá automáticamente reflejado en la vista; de la misma manera, si este valor es cambiado en la vista, este se actualizará automáticamente dentro del ViewModel.

Completamos el código del ChooseProductViewModel para cargar los productos desde los servicios REST.

var ChooseProductViewModel = function () {
   
var self = this
;
    self.products = ko.observableArray();

    self.goToPlaceOrder =
function
(product) {
       
//TODO: Mostrar la vista para ingresar la orden de compra del producto 
    };

    self.init =
function
() {
        ProductsDataSource.getAll(
function
(data) {
            self.products(data);
        });
    };

    self.init();

};

Delegamos la responsabilidad de obtener los productos al objeto ProductsDataSource y lo implementamos dentro del archivo /Scripts/App/Data/productsdatasource.js. Podemos observar que la función getAll recibe como parámetro un ”callback” que será ejecutado cuando se reciban los datos del servicio.

var ProductsDataSource = (function () {
   
return
{
        getAll:
function
(success) {
            $.ajax({
                type:
"GET"
,
                url:
"/api/products"
,
                contentType:
"application/json;charset=utf-8"
,
                dataType:
"json",
                success: success
            });
        }
    };
} ());

Utilizaremos “Partial Views” para organizar nuestras vistas. Creamos la vista parcial /Views/Home/_ChooseProduct.cshtml y agregamos el código HTML correspondiente esta pantalla.

<ul id="products" data-bind="foreach: products">
    <li class="product">
        <div class="productInfo">
            <h3 data-bind="text: name">
            </h3>
            <img class="product-image"
 
                
data-bind
="attr:{
                        src:'/content/images/products/thumbnails/' + imagename,
                        alt:name }"
 />
            <p class="description" data-bind="text: description"></p>
        </div>
        <div>
            <p class="price" data-bind="text: price"></p>
            <a class="order-button"
 
              
data-bind="attr:{ title:name }, click:$parent.goToPlaceOrder">
                Order Now
           
</a>
        </div>
    </li
>
</
ul
>

Podemos observar la presencia de atributos data-bind dentro de los tags HTML, estos son los “bindings” a través de los cuales enlazamos los elementos de las vistas a las propiedades del ViewModel. Existen “bindings” de todo tipo: texto (value, text), apariencia (css, html, attr, style), control (foreach, if, with), etc.

Examinemos brevemente los “bindings” que estamos utilizando en esta oportunidad.

  • foreach: Duplica una sección por cada elemento dentro del arreglo. En este caso creará un bloque “<li></li>” por cada producto que tengamos.
  • text: Establece como texto el valor de la propiedad asociada.
  • attr: Establece el valor de cualquier atributo HTML.
  • click: Establece un evento que será llamado cada vez que se haga click sobre el elemento. En esta oportunidad estamos utilizando la sentencia $parent para referirnos al padre del objeto producto, ya que la función goToPlaceOrder se encuentra en el ViewModel.

Desde el archivo /Views/Home/Index.cshtml hacemos referencia a la vista parcial recién creada.

<div>
    @Html.Partial("_ChooseProduct");
</div
>

Necesitamos enlazar la vista al ChooseProductViewModel, para esto creamos el archivo /Scripts/application.js donde inicializaremos los viewmodels. Utilizaremos este archivo para configurar todo lo necesario para el funcionamiento de la aplicación.

function initializeApplication() {
    initializeViewModels();
}

function
initializeViewModels() {
    ko.applyBindings(
new ChooseProductViewModel());
}

Agregamos el siguiente script al final del archivo /Views/Home/Index.cshtml para inicializar la aplicación.

<div>
    @Html.Partial("_ChooseProduct");
</div>

<script type="text/javascript">
    $(function
() {
        initializeApplication();
    });

</script
>

Por último agregamos las referencias a todos los nuevos scripts dentro del archivo /Views/Shared/Layout.cshtml.

<script src="~/Scripts/Lib/jquery-1.6.2.min.js" type="text/javascript"></script>
<
script src="~/Scripts/Lib/knockout-2.1.0.js" type="text/javascript"></script
>
<
script src="~/Scripts/App/Data/productsdatasource.js" type="text/javascript"></script
>
<
script src="~/Scripts/App/ViewModels/chooseproductviewmodel.js" type="text/javascript"></script
>
<
script src="~/Scripts/application.js" type="text/javascript"></script>

Luego de todos estos cambios, la estructura final de archivos ha quedado de la siguiente manera.

structureSPA3

Si ejecutamos la aplicación podremos observar el listado de productos.

image

En los siguientes posts veremos el resto de pantallas del proceso de delivery.

Saludos
Angel Núñez Salazar