Для
нашей задачи создадим тестовую таблицу Customers в базе данных (БД) MS SQL, так как хотим проверить, как будет работать наше
разбиение страниц с большим количеством записей. Пример скрипта для создания
данной таблицы можно посмотреть ниже.
CREATE TABLE [dbo].[Customers](
[Id] [int] IDENTITY(1,1) NOT NULL,
[Name] [nvarchar](255) NOT NULL,
[Address] [nvarchar](255) NOT NULL
CONSTRAINT
[PK_Customers] PRIMARY KEY
CLUSTERED
(
[Id] ASC
)WITH (PAD_INDEX
= OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS
= ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON
[PRIMARY]
Для
теста в таблицу было добавлено 100 тысяч строк. Пример скрипта добавления
записей в базу через ADO.NET провайдер можно
посмотреть в примере ниже. Для вставки данных использовалась наработка с BLToolkit с паттерном
репозиторий, пример которой вы можете найти по ссылках Источник 1, Источник 2 и
Источник 3. Ниже
представлено только готовое решение. Мапинг на таблицу Customers выглядит так:
[TableName("Customers")]
public abstract class CustomerEntity : EntityBase
{
[MaxLength(255), Required]
[MapField("Name")]
[Description("Имя")]
public abstract string Name { get; set; }
[MaxLength(255), Required]
[MapField("Address")]
[Description("Адрес
проживания")]
public abstract string Address
{ get; set; }
}
Код для добавления 100 тысяч записей приведен ниже.
class Program
{
static void Main(string[] args)
{
try
{
var
repository = new Repository<CustomerEntity>();
for (int i = 0;
i < 100000; i++)
{
var entity
= repository.CreateNewEntity();
entity.Name
= "Name" + i;
entity.Address
= "Address" + i;
repository.Save(entity);
}
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
}
Console.ReadLine();
}
}
Для самого примера с разбиением страниц будет использован классический
подход с ADO.NET, чтобы не углубляться в детали работы с BLToolkit и паттерном Repository. Основная задача
состоит в том, чтобы показать все как можно проще, без лишних сложностей. Перейдем у
папку Models в нашем проекте и добавим класс Customer, который будет сохранять информацию о клиентах.
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
public string Address
{ get; set; }
}
Путь к базе данных задается через файл конфига Web.config. Например:
<connectionStrings>
<!--<add name="StatisticDatabase"
connectionString="Data Source=vm-0161;Initial Catalog=Statistic;User
id=statistic;Password=123;" />-->
<add
name = "TestDatabase"
connectionString = "Server=.;Database=Test;Integrated Security=SSPI"
providerName = "System.Data.SqlClient" />
</connectionStrings>
Подключение
к БД у вас может отличаться, в зависимости от того, как вы настроили MS SQL сервер. Следующим
этапом создадим папку Helpers, в которую добавим
класс ScriptsProvider,
задача которого будет предоставлять описание для скриптов.
Немного
лирическое отступление по поводу запроса для разбиения на страницы имеет
следующий вид:
SELECT
* FROM(SELECT ROW_NUMBER()
OVER (ORDER BY [Name]) AS rowNum,
[Name] ,
[Address]
FROM [dbo].[Customers]) AS x WHERE rowNum Between 1 AND 30
В нашем классе ScriptsProvider это выглядит так:
public static class ScriptsProvider
{
public static string
GetConnectionString()
{
return ConfigurationManager.ConnectionStrings["TestDatabase"].ConnectionString;
}
public static string
SelectActiveCustomers()
{
return "SELECT * FROM(" +
"SELECT ROW_NUMBER() " +
"OVER (ORDER BY [Name]) AS rowNum,
" +
" [Name] " +
",[Address] " +
"FROM
[Statistic].[dbo].[Customers]" +
")
AS x WHERE rowNum Between {0} AND {1}";
}
public static string
SelectCustomersCount()
{
return "SELECT COUNT(*) " +
"FROM
[Statistic].[dbo].[Customers];";
}
}
Запрос выше с использованием функции OVER и ROW_NUMBER() необходим нам, чтобы использовать к
привычным нам Linq конструкциям Skip() и Take(). Перейдем
после проделанных действий и немного изменим реализацию нашего контроллера HomeController .
public class HomeController : Controller
{
private int
PageSize = 30;
public ActionResult Index(int? page = 1)
{
int
currentPageIndex = page.HasValue ? page.Value - 1 : 0;
var
customerCount = GetCustomersCount();
var
customers = GetAllCustomers(PageSize, currentPageIndex * PageSize);
return View();
}
private int
GetCustomersCount()
{
var
connectionString = ScriptsProvider.GetConnectionString();
using (var
connection = new SqlConnection(connectionString))
{
connection.Open();
using (var command
= connection.CreateCommand())
{
command.CommandText
=
ScriptsProvider.SelectCustomersCount();
return (int)command.ExecuteScalar();
}
}
}
private IEnumerable<Customer> GetAllCustomers(int
pageSize, int skipRow)
{
var
customerList = new List<Customer>();
var connectionString
= ScriptsProvider.GetConnectionString();
using (var
connection = new SqlConnection(connectionString))
{
connection.Open();
using (var command
= connection.CreateCommand())
{
command.CommandText
= string.Format(
ScriptsProvider.SelectActiveCustomers(), skipRow + 1, skipRow +
pageSize);
using (var reader
= command.ExecuteReader())
{
while
(reader.Read())
{
customerList.Add(GetCustomer(reader));
}
}
}
}
return
customerList;
}
private Customer GetCustomer(SqlDataReader reader)
{
var
customer = new Customer();
customer.Id
= (int)reader.GetInt64(0);
customer.Name
= SafeGetString(reader, 1);
customer.Address
= SafeGetString(reader, 2);
return
customer;
}
private static string
SafeGetString(SqlDataReader reader, int colIndex)
{
return
!reader.IsDBNull(colIndex) ?
reader.GetString(colIndex)
:
string.Empty;
}
public ActionResult About()
{
ViewBag.Message
= "Your app description
page.";
return View();
}
public ActionResult Contact()
{
ViewBag.Message
= "Your contact
page.";
return View();
}
}
В нашем контроллере добавлено функции для получения количества клиентов (Customers) и
функции для получения этих самых клиентов для одной страницы. Следующим этапом
добавим модель PagingInfoModel, в которой будем хранить информацию о текущей
странице, количестве страниц и количестве записей на странице. Реализация данной модели приведена ниже.
public class PagingInfoModel
{
public int
TotalRecords { get; set; }
public int
RecordPerPage { get; set; }
public int
CurrentPage { get; set; }
public int
TotalPages
{
get { return (int)Math.Ceiling((decimal)TotalRecords / RecordPerPage); }
}
}
Также
добавим в наш пример класс RecordPager, который вернет нам
список страниц для перехода. Этот класс нужно добавить в созданную ранее
папочку Helpers.
public static class RecordPager
{
public static MvcHtmlString RecordPage(this HtmlHelper html, PagingInfoModel pageInfo,
Func<int, string> url)
{
var result
= new StringBuilder();
for (int i = 1;
i <= pageInfo.TotalPages; i++)
{
var builder
= new TagBuilder("a");
builder.MergeAttribute("href", url(i));
builder.InnerHtml
= i.ToString();
if (i ==
pageInfo.CurrentPage)
builder.AddCssClass("current");
result.Append(builder.ToString());
}
return MvcHtmlString.Create(result.ToString());
}
}
Осталось сделать несколько финальных штрихов. Во-первых, нужно добавить модель CustomerViewModel, которая будет реализовывать всю работу, связанную с разбиением страниц.
public class CustomerViewModel
{
public IEnumerable<Customer> Customers { get; set; }
public PagingInfoModel PagingInfoModel { get; set; }
}
После проделанных действий перепишем логику нашего контроллера.
public ActionResult Index(int? page = 1)
{
int
currentPageIndex = page.HasValue ? page.Value - 1 : 0;
var
customerCount = GetCustomersCount();
var
customers = GetAllCustomers(PageSize, currentPageIndex * PageSize);
var vm = new CustomerViewModel
{
Customers
= customers,
PagingInfoModel
= new PagingInfoModel
{
CurrentPage
= currentPageIndex,
RecordPerPage
= PageSize,
TotalRecords
= customerCount
}
};
return
View(vm);
}
Осталось подправить представление View. Для
этого откроем файл Views\Home\Index.cshtml и перепишем его, как показано в примере ниже.
@{
ViewBag.Title = "Home Page";
}
<table>
<tr>
<th>Id</th>
<th>Name</th>
<th>Address</th>
</tr>
@using SimplePagingSample.Helpers
@model SimplePagingSample.Models.CustomerViewModel
@foreach (var item in Model.Customers) {
<tr>
<td>
@item.Id
</td>
<td>
@item.Name
</td>
<td>
@item.Address
</td>
</tr>
}
</table>
<nav class="pagination">
@Html.RecordPage(Model.PagingInfoModel,
x => Url.Action("Index", new { page = x }))
</nav>
Затем нужно подправить файл RouteConfig.cs, добавив в него реализацию, как показано
ниже.
routes.MapRoute(
name: null,
url: "Page/{page}",
defaults: new {
Controller = "Home", action = "Index" }
);
Ваш проект будет очень сильно тормозить из-за того, что для вывода страниц
их пришлось стразу отрисовать. В реальных проектах так не делается. Этот
пример будет работать, если только страниц у нас немного. В чем-то более
сложном он будет сильно подвисать. Поэтому пойдем другим путем и
воспользуемся готовой разбивкой страниц MvcPaging, которая доступна через NuGet.
После
этого переделаем нашу модель CustomerViewModel, как показано ниже.
public class CustomerViewModel
{
public IPagedList<Customer> Customers { get; set; }
}
Наш контроллер изменился совсем немного. Просто добавилась логика по
использованию extension метода ToPagedList.
public ActionResult Index(int? page = 1)
{
int currentPageIndex
= page.HasValue ? page.Value - 1 : 0;
var
customerCount = GetCustomersCount();
var
customers = GetAllCustomers(PageSize, currentPageIndex * PageSize);
var
pagedCustomers = customers.ToPagedList(currentPageIndex, PageSize,
customerCount);
var vm = new CustomerViewModel();
vm.Customers =
pagedCustomers;
return View(vm);
}
Еще пропала ненужная теперь модель PagingInfoModel. UI для отображения тоже изменился лишь немного.
@{
ViewBag.Title = "Home Page";
}
@model SimplePagingSample.Models.CustomerViewModel
<table>
<tr>
<th>Id</th>
<th>Name</th>
<th>Address</th>
</tr>
@foreach (var item in Model.Customers) {
<tr>
<td>
@item.Id
</td>
<td>
@item.Name
</td>
<td>
@item.Address
</td>
</tr>
}
</table>
<div class="pager">
@Html.Pager(Model.Customers.PageSize,
Model.Customers.PageNumber, Model.Customers.TotalItemCount)
</div>
Он стал немного меньше, чем предыдущий вариант. После запуска нашего
приложения мы увидим результат, как на рисунке ниже.
После
установки Bootstrap перейдем в форму
Views\Shared\ _Layout.cshtml и перенесем рендеринг
jQuery на самый верх формочки, как показано в участке кода ниже.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>@ViewBag.Title - My ASP.NET MVC Application</title>
<link href="~/favicon.ico" rel="shortcut icon" type="image/x-icon" />
<meta name="viewport" content="width=device-width" />
@Styles.Render("~/Content/css")
@Styles.Render("~/Content/themes/base/css")
@Scripts.Render("~/bundles/modernizr")
@Scripts.Render("~/bundles/jquery")
@Scripts.Render("~/bundles/jqueryui")
</head>
Затем
создадим папку DisplayTemplates в
папке Home,
а в созданную папку добавим шаблон Bootstrap3Pagination.cshtml, в которой
переместим логику, отвечающую за отображения страниц.
@using System.Web.Mvc
@using MvcPaging
@model PaginationModel
<ul class="pagination">
@foreach (var link in Model.PaginationLinks)
{
@BuildLink(link)
}
</ul>
@helper BuildLink(PaginationLink link)
{
var
liBuilder = new TagBuilder("li");
if
(link.IsCurrent)
{
liBuilder.MergeAttribute("class", "active");
}
if (!
link.Active)
{
liBuilder.MergeAttribute("class", "disabled");
}
var
aBuilder = new TagBuilder("a");
if
(link.Url == null)
{
aBuilder.MergeAttribute("href", "#");
}
else
{
aBuilder.MergeAttribute("href", link.Url);
}
if
(link.DisplayText == "«")
{
aBuilder.InnerHtml = "«";
}
else if
(link.DisplayText == "»")
{
aBuilder.InnerHtml = "»";
}
else
{
aBuilder.SetInnerText(link.DisplayText);
}
liBuilder.InnerHtml = aBuilder.ToString();
@Html.Raw(liBuilder.ToString())
}
Осталось
подправить наш файл Index.cshtml, и вся работа будет
сделана. Исправленный Index.cshtml со всеми плюшками и новыми возможностями будет иметь
следующий вид:
@{
ViewBag.Title = "Home Page";
}
<script src="~/Scripts/bootstrap.js"></script>
<link href="~/Content/bootstrap.css" rel="stylesheet" />
@model SimplePagingSample.Models.CustomerViewModel
<div class="container">
<table class="table table-bordered table-hover">
<tr>
<th>Id</th>
<th>Name</th>
<th>Address</th>
</tr>
@foreach (var item in Model.Customers) {
<tr>
<td>
@item.Id
</td>
<td>
@item.Name
</td>
<td>
</td>
</tr>
}
</table>
@Html.Pager(Model.Customers.PageSize,
Model.Customers.PageNumber, Model.Customers.TotalItemCount).Options(o => o
.DisplayTemplate("Bootstrap3Pagination")
.MaxNrOfPages(10)
.AlwaysAddFirstPageNumber())
</div>
У
нас стала доступна подсветка строк, кнопки выбора страницы стали иметь
вменяемый вид. На этой позитивной ноте мы закончим тему с разбиением страниц в ASP.NET MVC 4. Надеюсь, у вас в
будущем проблема с paging попросту не
возникнет.
Список литературы
No comments:
Post a Comment