ASP.NET MVC: DataSource на JavaScript или обертка на Web API сервис (часть 2)
Сайтостроение | создано: 16.07.2013 | опубликовано: 16.07.2013 | обновлено: 07.02.2024 | просмотров: 7263
В прошлой части статьи была проделана огромная работа по подготовке проекта к дальнейшему функционалу. В этой части будем доводить до логического завершения начатое. Добавим пейджинг, сделаем фильтрацию, “прикрутим” дополнительную детализацию.
Что к чему
В прошлой статье есть ссылка на проект, его мы и будем доводить до ума. В противном случае, вы можете скачать уже обновленный проект и поэкспериментировать с ним (ссылка в конце статьи)
Pager
Чтобы заработал пейджинг, надо добавить функционал разбития на страницы на стороне Web API. Добавим обработку Index и Size:
public HttpResponseMessage GetPersons(JsonQueryParams query) { var size = query.Size.HasValue ? query.Size.Value : 10; var items = _listOfPerson.OrderBy(x => x.Name).AsQueryable(); if (query.Index.HasValue) { items = items.Skip(size * query.Index.Value).Take(size); } return Request.CreateResponse(HttpStatusCode.OK, new { success = "все данные", total = _listOfPerson.Count, items = items.ToList() }); }
После доработки наша страница отобразила только 10 записей.
Чтобы а нас появился пейджер, надо просто немного поправить html-разметку. Я добавил одну строку под таблицу:
@{ ViewBag.Title = "DataSource: Master/Details"; } <span data-bind="text: clock.time"></span> <table> <thead> <tr> <th>Name</th> <th>Age</th> <th>Weight</th> </tr> </thead> <tbody data-bind="foreach: dsPerson.items"> <tr> <td data-bind="text: name"></td> <td data-bind="text: age"></td> <td data-bind="text: weight"></td> </tr> </tbody> </table> <div data-bind="pager: dsPerson"></div> @section scripts { <script src="~/Scripts/app/site.m.person.js"></script> <script src="~/Scripts/app/site.vm.homeIndex.js"></script> }
Строка 24 подключила пейджер на страницу:
Внешний вид или ода Twitter Bootstrap
Немного не приглядный вид, давайте подключим какой-нибудь стиль или несколько. Я воспользуюсь Twitter Bootstrap. Я скачал bootstrap.zip распаковал его в папку bootstrap и добавил ссылки на файлы в BundleConfig. Запистил проект, нажал F5 и …:
BusyIndicator
Теперь более приглядный вид. Добавим немного WEB 2.0, то есть увеличим индекс дружелюбности :) Для этого выведем индикатор обработки запроса.
Для того чтобы заработал BusyIndicator, я немного поправил Index.cshtml:
<div data-bind="blockUI: dsPerson.indicator"> <table class="table table-bordered"> <thead> <tr> <th>Name</th> <th>Age</th> <th>Weight</th> </tr> </thead> <tbody data-bind="foreach: dsPerson.items"> <tr> <td data-bind="text: name"></td> <td data-bind="text: age"></td> <td data-bind="text: weight"></td> </tr> </tbody> </table> <div data-bind="pager: dsPerson"></div> </div>
А если быть точнее, то я просто обернул весь (смотри строка 1 и 21) контент в div, который блокируется, чтобы избежать команд пользователя во время выполнения запроса.
Управления размером страниц (Pager size)
Я добавил еще немного html-разметки:
<span class="pull-right" data-bind="if: dsPerson.hasItems()"> <span class="icon-eye-open"></span> <select data-bind="options: site.cfg.pageSizes, value: dsPerson.queryParams.size"></select> <span class="icon-filter"></span><span data-bind=" text: dsPerson.queryParams.total"></span> </span>
И после этого, у меня появилась возможность выбрать размер страницы, а так же смотреть общее количество записей. Вот установлен размер страницы равный 5:
Простая фильтрация на основе QueryParams
А теперь добавим возможность фильтровать пользователей по имени. Для это надо доработать метод сервиса:
public HttpResponseMessage GetPersons(JsonQueryParams query) { var items = _listOfPerson.OrderBy(x => x.Name).AsQueryable(); if (query != null) { var size = query.Size.HasValue ? query.Size.Value : 10; if (query.Filters != null && query.Filters.FilterParams.Select(x => x.Name).Contains("Name")) { var param = query.Filters.FilterParams.FirstOrDefault(x => x.Name.Equals("Name")); if (param != null && param.Value != null) { var filter = param.Value.ToString(); if (!string.IsNullOrEmpty(filter)) { items = items.Where(x => x.Name.Contains(filter)); } } } if (query.Index.HasValue && items.Count() > size) { items = items.Skip(size * query.Index.Value).Take(size); } } return Request.CreateResponse(HttpStatusCode.OK, new { success = "все данные", total = items.Count(), items = items.ToList() }); }
Следует обратить внимание на строки 5-13, где проверяется наличие параметра в списке фильтров. После этого надо расширить queryParams для DataSource:
site.vm.homeIndex = function () { var clock = new site.controls.Clock(), queryParamsFilter = { "filters": { "logicalOperator": "And", "filterParams": [ { "Name": "Name", "Operator": "Contains", "Value": ko.observable(), "DisplayName": "Имя" } ] } }, dsPerson = new site.controls.DataSource({ autoLoad: true, service: site.services.person }, queryParamsFilter); dsPerson.queryParams.filters.filterParams[0].Value.subscribe(function () { dsPerson.getData(); }); return { dsPerson: dsPerson, clock: clock }; }();
Строка 3-15: Создаем объект для переопределения настроек по умолчанию для QueryParams, который является структурированным параметром для DataSource.
Строка 10: Указываем, что параметр должен ko.observable().
Строка 21-23: Подписываемся на обновления параметра. При обновлении происходит перезагрузка данных. Если учесть, что измененный параметр (в силу магии KnockoutJs) сразу же применяется к QueryParams, то нам достаточно просто перезапросить новый набор данных с учетом фильтра.
Осталось добавить поле для ввода значения фильтра:
<input type="text" data-bind="value: dsPerson.queryParams.filters.filterParams[0].Value, valueUpdate: 'afterkeydown'" />
Поле ввода напрямую привязываю к параметру фильтрации и запускаю приложение. Для того чтобы обновить скрипты на странице, нажимаю F5 и вводу букву “J” в поле фильтра:
Master/Details
Как известно, в связки “Master/Details” используется два источника данных. А зависимость между ними сводится к простой формуле: “Обновился главный – обнови зависимые”. В нашем примере уже есть один источник данных, для второго придется сделать практически те же самые манипуляции: Web API сервис, JavaScript обертку и всё остальное.
Хорошим примером для построения такой зависимости, я построю связку на двух классах из пакета SampleData. Класс Person и класс Department связаны по типу связи “мастер/детализация”. Создадим Web API контролер для класса Department:
public class DepartmentApiController : ApiController { private readonly List<Department> _listOfPerson = new List<Department>(); public DepartmentApiController() { _listOfPerson.AddRange(People.GetDepartments()); } public HttpResponseMessage GetDepartments(JsonQueryParams query) { var items = _listOfPerson.OrderBy(x => x.Name).AsQueryable(); var total = _listOfPerson.Count; if (query != null) { var size = query.Size.HasValue ? query.Size.Value : 10; if (query.Filters != null && query.Filters .FilterParams .Select(x => x.Name) .Contains("Name")) { var param = query.Filters.FilterParams .FirstOrDefault(x => x.Name.Equals("Name")); if (param != null && param.Value != null) { var filter = param.Value.ToString(); if (!string.IsNullOrEmpty(filter)) { items = items.Where(x => x.Name.Contains(filter)); total = items.Count(); } } } if (query.Index.HasValue && items.Count() > size) { items = items.Skip(size * query.Index.Value).Take(size); } } return Request.CreateResponse(HttpStatusCode.OK, new { success = "все данные", total, items = items.ToList() }); } public HttpResponseMessage GetDepartment(int id) { return Request.CreateResponse(HttpStatusCode.OK, new { success = "один по идентификатору", item = _listOfPerson.Where(x => x.Id.Equals(id)) }); } public HttpResponseMessage PostDepartment(Department department) { throw new NotImplementedException(); } public HttpResponseMessage PutDepartment(Department department) { throw new NotImplementedException(); } public HttpResponseMessage DeleteDepartment(Department department) { throw new NotImplementedException(); } }
Web API cервис успешно запустился, теперь сделаем JavaScript-обертка сервиса. Я не буду приводить его код потому что, практические нет никакого отличия от сервиса для Web API Person. Я также добавил упоминание о нем в файл BundleConfig.cs (строка 7).
bundles.Add(new ScriptBundle("~/bundles/site").Include( "~/Scripts/app/site.core.js", "~/Scripts/app/site.core.js", "~/Scripts/app/site.controls.js", "~/Scripts/app/site.bindingHandlers.js", "~/Scripts/app/site.services.person.js", "~/Scripts/app/site.services.department.js", "~/Scripts/app/site.utils.js"));
В сервисе site.services.department.js упоминается site.m.Department, и мне потребуется создать класс ViewModel на JavaScript для Department:
(function (site, ko) { site.m.Department = function (dto) { var me = this, data = dto || {}; me.id = ko.observable(data.Id); me.name = ko.observable(data.Name); me.selected = ko.observable(false); return me; }; })(site, ko)
Для того чтобы показать два источника данных рядом, я немного поправил html-разметку, предварительно добавив код для отображения dsDepartment:
<div data-bind="blockUI: dsDepartment.indicator" class="span6"> <div class="pull-left"> <i class="icon-filter"></i> <input type="text" data-bind="value: dsDepartment.queryParams.filters.filterParams[0].Value, valueUpdate: 'afterkeydown'" class="span2" /> </div> <div class="pull-right" data-bind="if: dsDepartment.hasItems()"> <i class="icon-eye-open"></i> <select data-bind="options: site.cfg.pageSizes, value: dsDepartment.queryParams.size" class="span1"></select> <i class="icon-filter"></i> <span data-bind="text: dsDepartment.queryParams.total"></span> </div> <table class="table table-bordered"> <thead> <tr> <th>Name</th> </tr> </thead> <tbody data-bind="foreach: dsDepartment.items"> <tr> <td data-bind="text: name"></td> </tr> </tbody> </table> <div data-bind="pager: dsDepartment"></div> </div>
Отличия от dsPerson вообще никакого нет. (В будущем планируется сделать контрол типа GridView для DataSource). После всех нововведений мне остается во ViewModel страницы добавить еще один DataSource, тот самый – dsDepartment:
site.vm.homeIndex = function () { var clock = new site.controls.Clock(), queryParamsFilter = { "filters": { "logicalOperator": "And", "filterParams": [ { "Name": "Name", "Operator": "Contains", "Value": ko.observable(), "DisplayName": "Имя" } ] } }, queryParamsFilter0 = { "filters": { "logicalOperator": "And", "filterParams": [ { "Name": "Name", "Operator": "Contains", "Value": ko.observable(), "DisplayName": "Имя" } ] } }, dsPerson = new site.controls.DataSource({ autoLoad: true, service: site.services.person }, queryParamsFilter), dsDepartment = new site.controls.DataSource({ autoLoad: true, service: site.services.department }, queryParamsFilter0); dsPerson.queryParams.filters.filterParams[0].Value.subscribe(function () { dsPerson.getData(); }); dsDepartment.queryParams.filters.filterParams[0].Value.subscribe(function () { dsDepartment.getData(); }); return { dsDepartment: dsDepartment, dsPerson: dsPerson, clock: clock }; 51: }();
Строка 16-28: Создаем параметр для dsDepartment.
Строка 33-36: Создаем еще один DataSource, параметром для service передаем наш свежеиспеченный site.services.department.
Строка 47: Не забываем “вытащить” наружу объект для UI.
В результате можно увидеть следующее:
У нас на данный момент два DataSource, которые не связаны ни коим образом между собой, но у обоих уже работает постраничная выборка и минимальная фильтрация по полю name.
Selected = true? Легко!
1. Для начала надо отключить автоматическую загрузку записей для dsPerson установив свойство autoLoad в значение false.
2. Теперь надо немного подправить разметку для dsDepartment. Надо сделать чтобы при клике на запись Department эта запись становилась выбранной, то есть свойство selected получало значение true.
<table class="table table-bordered"> <thead> <tr> <th>Name</th> </tr> </thead> <tbody data-bind="foreach: dsDepartment.items"> <tr data-bind="css: {'info':selected}, click: $parent.dsDepartment.select"> <td data-bind="text: name"></td> </tr> </tbody> </table>
Строка 8: Привязывает событие click на изменение свойства selected. А также визуально подсвечиваем выбранную строку устанавливая CSS для этой строки в значение info.
Осталось совсем немного: надо добавить параметр DepartmentId в dsPerson и подписаться на изменение этого значения у dsDepartment.
Новый параметр для dsPerson выглядит таким образом (строка 11-16):
queryParamsFilter = { "filters": { "logicalOperator": "And", "filterParams": [ { "Name": "Name", "Operator": "Contains", "Value": ko.observable(), "DisplayName": "Имя" }, { "Name": "DepartmentId", "Operator": "IsEqualTo", "Value": ko.observable(), "DisplayName": "Идентификатор подразделения" } ] } },
У DataSource (dsDepartment) подписываемся на событие выбора Department (строка 5):
dsDepartment = new site.controls.DataSource({ autoLoad: true, service: site.services.department, events: { selectedHandler: reloadPersons } }, queryParamsFilter0);
Конструкция работает так, как и предполагалось: При выборе подразделения, происходит обновление dsPerson.
Кстати, не забудьте добавить обработку параметра DepartmentId в Web API сервисе.
Заключение
DataSource достаточно гибкий контрол для работы на UI. Он может очень многое, например:
- добавлять
- удалять
- редактировать
- получать список
- получать под ID
- выбирать
- работать с коллекцией объектов (не Web API)
В настоящий момент уже существуют некоторые вспомогательные контролы, которые дополняют функционал DataSource:
- FormView – контрол для отображения модального окна с подменяемым шаблоном.
- FormEdit – контрол для редактирования в модальном окне сущности с отслеживанием статуса изменения свойств сущности.
- TreeView – контрол для отображение древовидной структоры.
- DbLookUp – контрол подбора комплексных сущностей при редактировании.