El scroll infinito.

Hola, os acordáis de Pepe, si hombre el usuario que nos ayudo a crear el patrón “Engañabobos”.  Seguro que a alguno y es normal le puede sonar a risa, por eso lo lógico, es que leáis antes este post .

Después de pensar que todo estaba solucionado, suena el teléfono y como no, es otra vez “Pepe”. Su frasecita “esto sigue igual de lento”.

Nos ponemos a trabajar,pensamos en voz alta y decimos :

1. Si estamos paginando.

2. Hemos hecho un análisis de las consultas y todas van bien.

Que puede pasar? Minutos después nos dimos cuenta que si que estábamos haciendo todo eso , pero….

Se ejecutaba un “SELECT COUNT()” en cada Round-Trip, para mostrar el número de paginas al usuario.

Efectivamente hemos localizado el problema, pero como lo solucionamos, pues sencillo y por eso el título.

Alguien utiliza twitter, a que sí, pues vamos a hacerlo para MVC.

1. Definimos una clase sencilla llamada Datos. Hoy no tenemos entidades ni EF, ni nada de eso.

   1: using System;

   2: using System.Collections.Generic;

   3: using System.Linq;

   4: using System.Web;

   5:  

   6: namespace MvcApplication17.Models

   7: {

   8:     public class Datos

   9:     {

  10:         public int Id { get; set; }

  11:         public string Nombre { get; set; }

  12:     }

  13: }

2. En nuestro controlador vamos a escribir lo siguiente en el método Index.

   1: public ActionResult Index(int? Pagina)

   2: {

   3:     ViewBag.Message = "Welcome to ASP.NET MVC!";

   4:  

   5:     List<Datos> datos = new List<Datos>();

   6:  

   7:     for (int i = 0; i < 500; i++)

   8:     {

   9:         int id = i+1;

  10:         datos.Add(new Datos() { Id = id, Nombre = string.Format("Nombre {0}", id) });

  11:     }

  12:  

  13:     var Skip = Pagina ?? 0;

  14:     var Take = 50;

  15:     var model = datos.Skip(Skip * Take).Take(Take);

  16:  

  17:     if (!Request.IsAjaxRequest())

  18:     {

  19:         return View(model);

  20:     }

  21:     else

  22:     {

  23:         return Json(model, JsonRequestBehavior.AllowGet);                                     

  24:     }

  25: }

Fijaos en un detalle “Request.IsAjaxRequest()”, con esto lo que hago es controlar si me están haciendo peticiones vía Ajax.

3. Nos queda la vista y un poco de algunas cosas. Siguiendo este magnífico post de @jmaguilar y viendo que podemos utilizar MVVM,uno de mis favoritos, nos ponemos manos a la obra:).

Para ello voy a utilizar dos referencias que podéis encontrar en la pagina oficial de knockout.

The "template" binding

The "attr" binding

Es decir nuestra vista va a quedar de la siguiente forma.

   1: @using MvcApplication17

   2: @model IEnumerable<MvcApplication17.Models.Datos>

   3:            

   4: @{

   5:     ViewBag.Title = "Home Page";    

   6: }

   7: <script src="@Url.Content("~/Scripts/knockout.js")" type="text/javascript"></script>
   1:  

   2: <h2>@ViewBag.Message</h2>

   3:  

   4: <p>

   5:     @Html.ActionLink("Create New", "Create")

   6: </p>

   7:  

   8: @*Tabla*@

   9: <table data-bind="template: { name: 'data-template', foreach: items }">

  10: <thead>

  11:     <tr>        

  12:         <th>Nombre</th>

  13:         <th></th>

  14:     </tr>

  15: </thead>

  16: </table> 

  17:  

  18: @*Template*@

  19: <script type="text/html" id="data-template">

  20:     <tr>    

  21:     <td data-bind="text: Nombre"></td>    

  22:     <td>

  23:         <a data-bind="attr: { href: 'Home/Edit/' + Id}">Edit</a>|

  24:         <a data-bind="attr: { href: 'Home/Details/' + Id}">Details</a>|

  25:         <a data-bind="attr: { href: 'Home/Delete/' + Id}">Delete</a>

  26:      </td>

  27:     </tr>

</script>

Claro esto no hace absolutamente nada, para ello, tenemos que utilizar javascript y dar vida a esta paginación.

   1: <script type="text/javascript">

   2: var Home_Index = {

   3:     items:ko.observableArray(@Html.Raw(@Json.Encode(Model))),

   4:     pagina:0,

   5:     endData:false,

   6:     redord:50,

   7:     scroll:function(){

   8:         if(($(window).scrollTop() == $(document).height() - $(window).height()) && !Home_Index.endData){

   9:             Home_Index.pagina++;

  10:             $.getJSON('Home/Index',{pagina:Home_Index.pagina},Home_Index.nextPage)

  11:         }

  12:     },

  13:     nextPage :function(data){

  14:         if (data.length>0){

  15:             $.each(data, function(i, item){

  16:                 Home_Index.items.push(item);

  17:             });

  18:         }      

  19:  

  20:         if (data.length < Home_Index.record){

  21:            Home_Index.endData=true;

  22:             Home_Index.paginga--;

  23:      

  24:         }

  25:     }              

  26: };

  27: ko.applyBindings(Home_Index);

  28: $(window).scroll(Home_Index.scroll);

  29: </script>

Y de repente nos encontramos con que “Pepe” está contento podemos paginar sin hacer un Count().

En definitiva nos tenemos que centrar en lo siguiente.

items:ko.observableArray(@Html.Raw(@Json.Encode(Model))),

Con esto evitamos que en la primera llamada tengamos que hacer una nueva llamada via ajax, cosa que he visto en muchas ocasiones. Cargar la pagina y después uno de los plugin hace una llamada para refrescar los datos.

El resto poca explicación tiene para los más puestos en javascript  excepto esta línea de la que podéis ampliar información Observables .

ko.applyBindings(Home_Index);

Claro alguno dirá, pero estar compiando esto en cada vista, como que no, pues os voy a dar una idea, podéis utilizar un método extensor y extender el objeto HmtlHelper, os paso la firma del método y quien quiera que se ponga manos a la obra:)

   1: public static class MisHelper

   2: {

   3:     public static MvcHtmlString InfiniteScroll(this System.Web.Mvc.HtmlHelper Helper,string Controller, string Action,int Record,object Model)

   4:     {

   5:         string Type = string.Format("{0}_{1}",Controller,Action);

   6:         StringBuilder sb = new StringBuilder();          

   7:         

   8:         //El resto es trabajo vuestro:)

   9:          

  10:         TagBuilder script = new TagBuilder("script");

  11:         script.Attributes.Add("Type", "text/javascript");

  12:         script.InnerHtml = sb.ToString();

  13:  

  14:       

  15:  

  16:         return new MvcHtmlString(script.ToString(TagRenderMode.Normal));

  17:     }

  18: }

Si hacemos esto conseguiremos en nuestra vista no tener esa cantidad de javascript y ejecutar esto vía razor.

@Html.InfiniteScroll("Home", "Index", 50, Model);

Como todos no son virtudes y existe algún que otro problema y sobre todo con Javascript y el formateo de fechas y números que lo odio:). Os voy a contar otra forma que se me ha ocurrido.

Separar mi vista en dos y las filas de la tabla generarlas en una vista parcial.

1. Controlador.

   1: public ActionResult Index(int? Pagina)

   2: {

   3:    ViewBag.Message = "Welcome to ASP.NET MVC!";

   4:  

   5:    List<Datos> datos = new List<Datos>();

   6:  

   7:    for (int i = 0; i < 500; i++)

   8:    {

   9:        int id = i+1;

  10:        datos.Add(new Datos() { Id = id, Nombre = string.Format("Nombre {0}", id) });

  11:    }

  12:  

  13:    var Skip = Pagina ?? 0;

  14:    var Take = 50;

  15:    var model = datos.Skip(Skip * Take).Take(Take);

  16:  

  17:    if (!Request.IsAjaxRequest())

  18:    {

  19:        return View(model);

  20:    }

  21:    else

  22:    {

  23:        return PartialView("_TableIndex", model);

  24:        //return Json(model, JsonRequestBehavior.AllowGet);                                     

  25:    }

  26: }

Si os fijáis la diferencia es sencilla en vez de devolver Json lo que hago es devolver un PartialViewResult.

2. Nuestra vista quedaría de la siguiente forma.

   1: @using MvcApplication17

   2: @model IEnumerable<MvcApplication17.Models.Datos>

   3:            

   4: @{

   5:     ViewBag.Title = "Home Page";    

   6: }

   7:  

   8: <h2>@ViewBag.Message</h2>

   9:  

  10: <p>

  11:     @Html.ActionLink("Create New", "Create")

  12: </p>

  13:  

  14: <table id = "Grid">

  15:     <tr>

  16:         <th>

  17:             Nombre

  18:         </th>

  19:         <th></th>

  20:     </tr>

  21:     @Html.Partial("_TableIndex",Model)

  22: </table>

  23: <script type="text/javascript">
   1:  

   2:     var Home_Index = {

   3:         pagina: 0,

   4:         endData: false,

   5:         redord: 50,

   6:         scroll: function () {

   7:             if (($(window).scrollTop() == $(document).height() - $(window).height()) && !Home_Index.endData) {

   8:                 Home_Index.pagina++;                

   9:                 $.get('Home/Index', { pagina: Home_Index.pagina }, Home_Index.nextPage);

  10:             }

  11:         },

  12:         nextPage: function (data) {           

  13:             data = data.replace(/^s*|s*$/g, "");

  14:             alert($(data).size())

  15:             if ($(data).length > 0) {

  16:             $('#Grid tr:last').after(data);

  17:             }

  18:             else {

  19:             Home_Index.pagina--;

  20:             Home_Index.endData = true;

  21:             }

  22:         }

  23:     };

  24: $(window).scroll(Home_Index.scroll);

</script>

3. Nuestra vista parcial.

 

   1: @model IEnumerable<MvcApplication17.Models.Datos>

   2: @foreach (var item in Model) {

   3:     <tr>

   4:         <td>

   5:             @Html.DisplayFor(modelItem => item.Nombre)

   6:         </td>

   7:         <td>

   8:             @Html.ActionLink("Edit", "Edit", new { id=item.Id }) |

   9:             @Html.ActionLink("Details", "Details", new { id=item.Id }) |

  10:             @Html.ActionLink("Delete", "Delete", new { id=item.Id })

  11:         </td>

  12:     </tr>

  13: }

 

Analicemos pues las ventajas y desventajas de utilizar esta última, tenemos más control como se puede ver gracias a Razor y el trabajo en el servidor, por contra al devolver html la diferencia en bytes entre utilizar la primera y la segunda forma  es notable 12 Kb frente a 1.5 Kb devolviendo Json en cada petición, es decir bastante diferencia.

Conclusiones.

Poco tiene esto que decir, mas que educar a nuestros usuarios para adaptarse a nuevas formas de ver la información y que pequeños detalles como este, hacen que una aplicación corra o se arrastre:).

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *