čtvrtek 6. listopadu 2014

Stránkování v MVC

Tenhle článeček je o stránkování záznamů v MVC trochu jinak - tak, aby jednotlivé části (controller's actions, models, views) zůstaly co nejvíce jednoduché.

Zdrojový kód je dostupný zde.

Models

Aby to nebylo moc složité, bude se zobrazovat seznam jmen, tedy jako základní třída  nám poslouží toto:
public class UserViewModel
{
 public string FirstName { get; set; }
 public string LastName { get; set; }
}

Pro zobrazení seznamu bude sloužit generická třída - takže ani tento příklad nebude pevně svázán s třídou UserViewModel, ale generickou třídu budeme moci použít s jakoukoliv třídou, z níž chceme zobrazovat data.
public interface IItemListInfo
{
 int TotalItems { get; }
}

public class ItemListViewModel<T> : IItemListInfo
{
 private IEnumerable<T> items;
 private int totalItems;

 public ItemListViewModel(IEnumerable<T> items, int totalItems)
 {
  this.items = items;
  this.totalItems = totalItems;
 }
 public IEnumerable<T> Items { get {return this.items;} }
 public int TotalItems { get { return this.totalItems; } }
}

a ještě musíme mít třídu, která bude držet "stránkovácí" informace:
public class PagingViewModel
{
 public int PageNumber { get; set; }
 public int PageSize { get; set; }
 public int Pages {get;set;}
}
Objekt této třídy bude vlastně jen držet číslo aktuální stránky, velikost stránky a celkový počet stránek.

Controller - poprvé

Akce v kontroleru bude jednoduchá - hlavním úkolem je načíst daný počet dat (=počet záznamů na stránku)  pro danou stránku (počet stránek)


public ActionResult Index(PagingViewModel paging)
{
 ItemListViewModel<UserViewModel> model = this.GetUsers(paging.PageSize, paging.PageNumber);

 return View(model);
}


Views

Ve view dojde k zrendrování informací a i k vykreslení navigační části. První view je docela jednoduché a vykreslí informace o uživateli - pro vykreslení navigační části pak předá řízení zpět do kontroleru a zavolá akci "Paging"




@using MvcPagingExample.Models;

@model ItemListViewModel<UserViewModel>
@{
    ViewBag.Title = "Home Page";
}
<div class="list">
    <h2>List of Users</h2>

    <table>
        <thead>
            <th width="120px">First name</th>
            <th width="120px">Last name</th>
        </thead>
        @foreach (UserViewModel user in Model.Items)
        { 
            <tr>
                <td>@user.FirstName </td>
                <td>@user.LastName</td>
            </tr>
        }
    </table>
    <br/>
    @{Html.RenderAction("Paging");}
</div>



Controller - podruhé

Tato metoda může být použita pro vykreslení stránkování libovolného seznamu - odpovídající view jen využívá javascript pro ajax volání zpět na server a vrácenou hodnotou nahrádí stávající obsah:


[ChildActionOnly]
public ActionResult Paging(PagingViewModel paging)
{
	return PartialView(paging);
}

a view:


@using MvcPagingExample.Models;

@model PagingViewModel
<script type="text/javascript">
    $(document).ready(function () {
        jQuery("a").click(function () {
            var $anchor = jQuery(this);
            var url = $anchor.closest('.ajaxPaging').attr("data-source");
            jQuery.get(
                url,
                { PageNumber: $anchor.attr("rel")},
                function (data) {
                    jQuery(".list").replaceWith(data);
                });
        });
    });
</script>

<div class="ajaxPaging" data-source="@Request.Url.AbsolutePath">
    @NaviLink(active: Model.PageNumber > 1, targetPageNumber: 1, label: "first")
    @NaviLink(active: Model.PageNumber > 1, targetPageNumber: (Model.PageNumber - 1), label: "previous")
    <b>page @Model.PageNumber of @Model.Pages</b>
    @NaviLink(active: Model.PageNumber < Model.Pages, targetPageNumber: (Model.PageNumber + 1), label: "next")
    @NaviLink(active: Model.PageNumber < Model.Pages, targetPageNumber: Model.Pages, label: "last")
</div>

@helper NaviLink(bool active, int targetPageNumber, string label)
{
    if (active)
    {
    <a href="#" rel="@targetPageNumber">@label</a>
    }
    else
    { 
    @label
    }
}


Prvotní nastavení

Zbývá ještě ošetřit případ, kdy při prvním volání akce není žádné stránkování nastaveno.  K tomu slouží atribut třída, která dědí z třídy ActionFilterAttribute a která nabízí dvě metody - jednu spouštěnou před provedením vlastní akce kontroleru (OnActionExecuting) a druhou, která se provadí  po vykonání akce v kontroleru (OnActionExecuted).



public override void OnActionExecuting(ActionExecutingContext filterContext)
{
	var paging = filterContext.ActionParameters.Select(p => p.Value as PagingViewModel).Where(f=> f !=null).FirstOrDefault();

	if (paging != null)
	{
		paging.PageSize = PageSize;
		filterContext.RouteData.Values.Add("PageSize", this.PageSize);

		if (paging.PageNumber == 0)
		{
			filterContext.RouteData.Values.Add("PageNumber", 1);
			paging.PageNumber = 1;
		}
	}
	base.OnActionExecuting(filterContext);
}

Nejprve se metoda pokouší najít hodnotu parametru předávaného metodě kontroleru - tedy PagingViewModel.  Pokud takový parametr nenajde, tak jej vytvoří a nastaví velikost stránky a první stránku jako aktuální. Zároveň tyto hodnoty vloží i do kolekce RouteData. Tato kolekce je MVC používáná při bindování a pokud tedy budeme později volat metodu Paging, tak MVC použije data z této kolekce pro vytvoření parametru (instance třídy PagingViewModel) předávaného do této metody.

Po spuštění metody se naplní kolekce a zároveň se zjistí, kolik záznamů je celkem k dispozici. Což je ten správný okamžik pro spuštění metody OnActionExecuted, která vypočte celkový počet stránek a získá tak poslední hodnotu, kterou je nutno uložit do kolekce RouteData, aby MVC mohlo později, při volání metody Paging, vytvořit plnohodnotnou instanci třídy PagingViewModel.



public override void OnActionExecuted(ActionExecutedContext filterContext)
{
	var model = filterContext.Controller.ViewData.Model as IItemListInfo;

	if (filterContext.HttpContext.Request.IsAjaxRequest())
	{
		ViewResult result = filterContext.Result as ViewResult;
		filterContext.Result = new PartialViewResult() { TempData = result.TempData, View=result.View, ViewData=result.ViewData, ViewEngineCollection=result.ViewEngineCollection};
	}
						
	if (model != null)
	{
		filterContext.RouteData.Values.Add("Pages", (int) Math.Ceiling( (double) model.TotalItems/this.PageSize));
	}

	base.OnActionExecuted(filterContext);
}

Můžete si také všimnout, že je kontrolován typ příchozího volání a pokud se jedná o Ajax volání, je výsledek změněn na PartialView - takže se rendreju pouze view odpovídající danému kontroleru a master view je ignorováno - vrací se tak pouze zrendrovaná tabulka a nikoliv celá stránka.


Tento atribut je pak použit pro dekorování akce  a zároveň nastavuje velikost stránky:


[Paging(PageSize = 5)]
public ActionResult Index(PagingViewModel paging)
{
	ItemListViewModel<UserViewModel> model = this.GetUsers(paging.PageSize, paging.PageNumber);

	return View(model);
}

Může se použít na libovolnou akci, která vyžaduje pro své výsledky stránkování. Vývojář se o stránkování nemusí starat a v kontroleru se tedy může soustředit pouze na získání dat.






Žádné komentáře:

Okomentovat