Cómo ordenar por columnas una tabla paginada en ASP.NET MVC

En el anterior Post de este Blog Cómo desarrollar un filtro de búsqueda por texto en ASP.NET MVC, vimos como integrar un filtro de búsqueda por texto, en una tabla de consulta de registros con paginación basada en ASP.NET MVC.

En esta ocasión, y para extender el ejemplo del Post anterior, veremos como añadir la funcionalidad de ordenar por columnas a una tabla paginada, completando así las tres funcionalidades mas habituales en la consulta de registros: paginación, búsqueda y ordenación. 

ordenación-por-columnas-asp-net-mvc

Para desarrollar este ejemplo, utilizaremos la estructura ya creada en el Post Cómo desarrollar un filtro de búsqueda por texto en ASP.NET MVC, e iremos añadiendo al código las nuevas funcionalidades para integrar el sistema de ordenación por columnas.

El modelo de datos

El modelo de datos sigue siendo el mismo (la tabla Customers de la base de datos de pruebas Northwind). También utilizaremos Entity Framework como medio de acceso a la base de datos.

Nota: Los Scripts Sql de la tabla Customers están disponibles para descargar al final de este artículo.

public class Customer
    {
        [Key]
        [StringLength(5)]
        public string CustomerID { get; set; }

        [Required]
        [StringLength(40)]
        public string CompanyName { get; set; }

        [Required]
        [StringLength(30)]
        public string ContactName { get; set; }

        // ...

        [Required]
        [StringLength(100)]
        [DataType(DataType.EmailAddress)]
        public string Email { get; set; }
    }

La clase de paginación

Añadiremos a la clase de paginación PaginadorGenerico.cs, una nueva propiedad public string OrdenActual { get; set; } para poder mantener el estado de la ordenación mientras nos movemos por las páginas de resultados.

    public class PaginadorGenerico<T> where T:class
    {
        public int PaginaActual { get; set; }
        public int RegistrosPorPagina { get; set; }
        public int TotalRegistros { get; set; }
        public int TotalPaginas { get; set; }
        public string BusquedaActual { get; set; }
        public string OrdenActual { get; set; }
        public IEnumerable<T> Resultado { get; set; }
    }

La clase de ordenación

Adicionalmente, necesitaremos crear una nueva clase estática OrdenCustomer.cs, en la que definiremos los tipos de ordenación por columnas que posteriormente aplicaremos a nuestra tabla paginada.

Para una mejor legibilidad y claridad en el código, estos tipos de ordenación los definiremos como constantes de tipo string, dentro de la clase estática.

    // Clase estática para tipos de ordenación
    public static class OrdenCustomer
    {
        // Constantes estáticas para la ordenación de registros 
        public const string OrderByContactNameASC = "contactname_asc";
        public const string OrderByContactNameDESC = "contactname_desc";
        public const string OrderByCompanyNameASC = "companyname_asc";
        public const string OrderByCompanyNameDESC = "companyname_desc";        
    }

Como podemos ver, para cada columna que queramos aplicarle la ordenación (en este caso serían solo dos), definiremos un par de constantes con ordenación ascendente (ASC) y descendente (DESC).

El Controlador

Antes de entrar a explicar los nuevos cambios necesarios en el Controlador CustomersController.cs, crearemos una nueva clase de Servicio CustomersService.cs, a la cual trasladaremos la lógica ya existente de paginación y búsqueda, más la nueva lógica de ordenación por columnas.

Esto lo hacemos así, ya que la cantidad de líneas de código en la Acción del Controlador ya va siendo muy elevada, y según las convenciones MVC esto no es nada recomendable.

Así pues, crearemos entonces la clase CustomersService.cs con el método GetCustomers(...) , y posteriormente veremos cual será su código.

    public class CustomersService
    {        
        public PaginadorGenerico<Customer> GetCustomers(string orden, 
                                                        string buscar, 
                                                        int pagina)
        {
            PaginadorGenerico<Customer> _PaginadorCustomers;
        
            // ...
            // ...
            
            return _PaginadorCustomers;
        }
    }

En la Acción por defecto Index(...) del Controlador, añadiremos un nuevo parámetro de entrada string orden donde recibiremos el tipo de ordenación (constantes de la clase OrdenCustomer.cs) que le aplicaremos a los resultados de la consulta. Por defecto el tipo de ordenación será string orden = OrdenCustomer.OrderByContactNameASC.

Por otra parte, también es necesario indicar a la Vista, cual será el siguiente tipo de ordenación en función del actual, es decir, si el actual tipo de ordenación para la columna ContactName es OrdenCustomer.OrderByContactNameASC el siguiente debe ser necesariamente OrdenCustomer.OrderByContactNameDESC.

Esto lo haremos a través del ViewBag mediante la variables ViewBag.OrdenContactNameViewBag.OrdenCompanyName.

    public class CustomersController : Controller
    {
        private CustomersService _customersService;

        public CustomersController()
        {
            _customersService = new CustomersService();
        }

        // GET: Customers
        public ActionResult Index(string orden = OrdenCustomer.OrderByContactNameASC,
                                  string buscar = null, 
                                  int pagina = 1)
        {
            // Indicamos a la Vista, cual será el siguiente tipo de ordenación.
            ViewBag.OrdenContactName = (orden == OrdenCustomer.OrderByContactNameASC ?
                OrdenCustomer.OrderByContactNameDESC : OrdenCustomer.OrderByContactNameASC);
            ViewBag.OrdenCompanyName = (orden == OrdenCustomer.OrderByCompanyNameASC ?
                OrdenCustomer.OrderByCompanyNameDESC : OrdenCustomer.OrderByCompanyNameASC);
                        
            // Enviamos a la Vista la Clase de paginación
            return View(_customersService.GetCustomers(orden, buscar, pagina));
        }

    }

La clase de Servicio

Como ya vimos anteriormente, la nueva clase de Servicio CustomersService.cs a través de su método GetCustomers(...), será la encargada de implementar la lógica de paginación, búsqueda y ordenación.

Reorganizando algo el código existente (paginación y búsqueda), y añadiendo la nueva funcionalidad de ordenación por columnas, la clase de Servicio quedaría de esta manera:

    public class CustomersService
    {        
        public PaginadorGenerico<Customer> GetCustomers(string orden, 
                                                        string buscar, 
                                                        int pagina)
        {
            int _RegistrosPorPagina = 10;
            AppDbContext _DbContext;
            List<Customer> _Customers;
            PaginadorGenerico<Customer> _PaginadorCustomers;

            ////////////////////////
            // FILTRO DE BÚSQUEDA //
            ////////////////////////
            ///
            using (_DbContext = new AppDbContext())
            {
                // Recuperamos el 'DbSet' completo
                _Customers = _DbContext.Customers.ToList();

                // Filtramos el resultado por el 'texto de búqueda'
                if (!string.IsNullOrEmpty(buscar))
                {
                    foreach (var item in buscar.Split(new char[] { ' ' }, 
                            StringSplitOptions.RemoveEmptyEntries))
                    {
                        _Customers = _Customers.Where(x => x.ContactName.Contains(item) ||
                                                      x.CompanyName.Contains(item) ||
                                                      x.Email.Contains(item))
                                                      .ToList();
                    }
                }
            }

            /////////////////////////////
            // ORDENACIÓN POR COLUMNAS //
            /////////////////////////////
            ///
            // Ordenamos el resultado en función del parámetro "orden".
            switch (orden)
            {
                case OrdenCustomer.OrderByContactNameASC:
                    _Customers = _Customers.OrderBy(x => x.ContactName).ToList();
                    break;

                case OrdenCustomer.OrderByContactNameDESC:
                    _Customers = _Customers.OrderByDescending(x => x.ContactName).ToList();
                    break;

                case OrdenCustomer.OrderByCompanyNameASC:
                    _Customers = _Customers.OrderBy(x => x.CompanyName).ToList();
                    break;

                case OrdenCustomer.OrderByCompanyNameDESC:
                    _Customers = _Customers.OrderByDescending(x => x.CompanyName).ToList();
                    break;

                default:
                    _Customers = _Customers.OrderBy(x => x.ContactName).ToList();
                    break;
            }

            ///////////////////////////
            // SISTEMA DE PAGINACIÓN //
            ///////////////////////////
            ///
            int _TotalRegistros = 0;
            int _TotalPaginas = 0;

            // Número total de registros de la tabla Customers
            _TotalRegistros = _Customers.Count();

            // Obtenemos la "página de registros" de la tabla Customers
            _Customers = _Customers.Skip((pagina - 1) * _RegistrosPorPagina)
                                                .Take(_RegistrosPorPagina)
                                                .ToList();

            // Número total de páginas de la tabla Customers
            _TotalPaginas = (int)Math.Ceiling((double)_TotalRegistros / _RegistrosPorPagina);

            // Instanciamos la 'Clase de paginación' y asignamos los nuevos valores
            _PaginadorCustomers = new PaginadorGenerico<Customer>()
            {
                RegistrosPorPagina = _RegistrosPorPagina,
                TotalRegistros = _TotalRegistros,
                TotalPaginas = _TotalPaginas,
                PaginaActual = pagina,
                BusquedaActual = buscar,
                OrdenActual = orden,
                Resultado = _Customers
            };

            return _PaginadorCustomers;
        }
    }

La Vista

En la Vista Index.cshtml añadiremos en la cabecera de cada columna de la tabla, los Links hacia el Controlador para efectuar la ordenación, así como un icono (glyphicon de Bootstrap) que nos indicará el orden actual (ASC o DESC).

También actualizaremos el paginador de registros, para que envíe al Controlador el orden actual de la tabla, a través del parámetro orden = Model.OrdenActual.

@model PaginadorGenerico<Customer>
@{
    ViewBag.Title = "Customers";
    Layout = "~/Views/Shared/_Layout.cshtml";
}

<br />

@*CÓDIGO PARA EL FILTRO DE BÚSQUEDA*@
<div class="text-right form-inline">
    <form method="get" action=@Url.Action("Index", "Customers" )>
        <div class="form-group">
            @Html.TextBox("buscar", null, new { placeholder = "texto de búsqueda",
                                    @class = "form-control" })
        </div>
        <button class="btn btn-default" type="submit">Buscar</button>
    </form>
</div>

<br />
@*CÓDIGO PARA LA TABLA DE DATOS*@
<table class="table table-bordered">
    <thead>
        <tr>
            <th>
                <a href="@Url.Action("Index", new { orden = ViewBag.OrdenContactName, buscar = Model.BusquedaActual })">
                    <span>Contact Name</span>
                    @{

                        if (Model.OrdenActual == OrdenCustomer.OrderByContactNameASC)
                        {
                            <i class="glyphicon glyphicon-sort-by-attributes pull-right text-muted"></i>
                        }
                        else if (Model.OrdenActual == OrdenCustomer.OrderByContactNameDESC)
                        {
                            <i class="glyphicon glyphicon-sort-by-attributes-alt pull-right text-muted"></i>
                        }
                        else
                        {
                            <i class="glyphicon glyphicon-sort pull-right text-muted"></i>
                        }
                    }
                </a>
            </th>
            <th>
                <a href="@Url.Action("Index", new { orden = ViewBag.OrdenCompanyName, buscar = Model.BusquedaActual })">
                    <span>Company Name</span>
                    @{

                        if (Model.OrdenActual == OrdenCustomer.OrderByCompanyNameASC)
                        {
                            <i class="glyphicon glyphicon-sort-by-attributes pull-right text-muted"></i>
                        }
                        else if (Model.OrdenActual == OrdenCustomer.OrderByCompanyNameDESC)
                        {
                            <i class="glyphicon glyphicon-sort-by-attributes-alt pull-right text-muted"></i>
                        }
                        else
                        {
                            <i class="glyphicon glyphicon-sort pull-right text-muted"></i>
                        }
                    }
                </a>                
            </th>
            <th>
                Email
            </th>
        </tr>
    </thead>
    @foreach (var item in Model.Resultado)
    {
        <tbody>
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.ContactName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.CompanyName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Email)
                </td>
            </tr>
        </tbody>
    }
</table>

@*CÓDIGO PARA EL PAGINADOR DE REGISTROS*@
@if (Model.Resultado.Count() > 0)
{
    <span>
        <strong>@Model.TotalRegistros</strong> registros encontrados
    </span>
    <span>&nbsp;|&nbsp;</span>
    <span>
        Página <strong>@(Model.PaginaActual)</strong> de
        <strong>@Model.TotalPaginas</strong>
    </span>
    <span>&nbsp;|&nbsp;</span>
}
else
{
    <span>No hay resultados para esta búsqueda</span>
    <span>&nbsp;|&nbsp;</span>
}

@if (Model.PaginaActual > 1)
{
    @Html.ActionLink("<<", "Index", new { pagina = 1, buscar = Model.BusquedaActual, orden = Model.OrdenActual },
                        new { @class = "btn btn-sm btn-default" })
    <span></span>
    @Html.ActionLink("Anterior", "Index", new { pagina = Model.PaginaActual - 1,
                                                buscar = Model.BusquedaActual, orden = Model.OrdenActual },
                                          new { @class = "btn btn-sm btn-default" })
}
else
{
    @Html.ActionLink("<<", "Index", new { pagina = 1,
                                          buscar = Model.BusquedaActual, orden = Model.OrdenActual },
                                    new { @class = "btn btn-sm btn-default disabled" })
    <span></span>
    @Html.ActionLink("Anterior", "Index", new { pagina = 1,
                                                buscar = Model.BusquedaActual, orden = Model.OrdenActual },
                                          new { @class = "btn btn-sm btn-default disabled" })
}

<span></span>

@if (Model.PaginaActual < Model.TotalPaginas)
{
    @Html.ActionLink("Siguiente", "Index", new { pagina = Model.PaginaActual + 1,
                                                 buscar = Model.BusquedaActual, orden = Model.OrdenActual },
                                           new { @class = "btn btn-sm btn-default" })
    <span></span>
    @Html.ActionLink(">>", "Index", new { pagina = Model.TotalPaginas,
                                          buscar = Model.BusquedaActual, orden = Model.OrdenActual },
                                    new { @class = "btn btn-sm btn-default" })
}
else
{
    @Html.ActionLink("Siguiente", "Index", new { pagina = Model.TotalPaginas - 1,
                                                 buscar = Model.BusquedaActual, orden = Model.OrdenActual },
                                           new { @class = "btn btn-sm btn-default disabled" })
    <span></span>
    @Html.ActionLink(">>", "Index", new { pagina = Model.TotalPaginas,
                                          buscar = Model.BusquedaActual, orden = Model.OrdenActual },
                                    new { @class = "btn btn-sm btn-default disabled" })
}

 

Descargas

Script Tabla- dbo.Customers.sql
Datos de prueba - dbo.Customers.data.sql

   EtiquetasASP.NET .NET MVC C# Entity Framework

  Compartir


  Nuevo comentario

El campo Comentario es obligatorio.
El campo Nombre es obligatorio.

Enviando ...

  Comentarios

Javier Javier

Estupendo. Gracias!
Rafael Acosta Administrador Rafael Acosta

@Javier: Gracias por tu comentario. Compartir los artículos en la redes sociales es fundamental para que este Blog siga ofreciendo publicaciones de calidad y en español.


Perfil para Rafael Acosta en Stack Overflow en español, preguntas y respuestas para programadores y profesionales de la informática.

  Etiquetas

.NET Core .NET Framework .NET MVC .NET Standard AJAX ASP.NET ASP.NET Core ASP.NET MVC Bootstrap Buenas prácticas C# Cookies Entity Framework Gráficos JavaScript jQuery JSON JWT PDF Pruebas Unitarias Seguridad SEO SOAP Sql Server SqLite Swagger Validación Web API Web Forms Web Services WYSIWYG

  Nuevos


  Populares















Utilizamos cookies propias y de terceros para mejorar nuestros servicios y ofrecerle una mejor experiencia de navegación. Si continúa navegando consideramos que acepta su uso. Más información   Acepto