Что к чему
В прошлой статье есть ссылка на проект, его мы и будем доводить до ума. В противном случае, вы можете скачать уже обновленный проект и поэкспериментировать с ним (ссылка в конце статьи)
Pager
Чтобы заработал пейджинг, надо добавить функционал разбития на страницы на стороне Web API. Добавим обработку Index и Size:
1: public HttpResponseMessage GetPersons(JsonQueryParams query) { 2: var size = query.Size.HasValue ? query.Size.Value : 10; 3: var items = _listOfPerson.OrderBy(x => x.Name).AsQueryable(); 4: if (query.Index.HasValue) { 5: items = items.Skip(size * query.Index.Value).Take(size); 6: } 7: 8: return Request.CreateResponse(HttpStatusCode.OK, 9: new { 10: success = «все данные», 11: total = _listOfPerson.Count, 12: items = items.ToList() 13: }); 14: }
После доработки наша страница отобразила только 10 записей.
Чтобы а нас появился пейджер, надо просто немного поправить html-разметку. Я добавил одну строку под таблицу:
1: @{ 2: ViewBag.Title = «DataSource: Master/Details»; 3: } 4: 5: <spandata-bind=»text: clock.time»></span> 6: 7: <table> 8: <thead> 9: <tr> 10: <th>Name</th> 11: <th>Age</th> 12: <th>Weight</th> 13: </tr> 14: </thead> 15: <tbodydata-bind=»foreach: dsPerson.items»> 16: <tr> 17: <tddata-bind=»text: name»></td> 18: <tddata-bind=»text: age»></td> 19: <tddata-bind=»text: weight»></td> 20: </tr> 21: </tbody> 22: </table> 23: 24: <divdata-bind=»pager: dsPerson»></div> 25: 26: @section scripts 27: { 28: <scriptsrc=»~/Scripts/app/site.m.person.js»></script> 29: <script src=»~/Scripts/app/site.vm.homeIndex.js»></script> 30: }
Строка 24 подключила пейджер на страницу:
Внешний вид или ода Twitter Bootstrap
Немного не приглядный вид, давайте подключим какой-нибудь стиль или несколько. Я воспользуюсь Twitter Bootstrap. Я скачал bootstrap.zip распаковал его в папку bootstrap и добавил ссылки на файлы в BundleConfig. Запистил проект, нажал F5 и …:
BusyIndicator
Теперь более приглядный вид. Добавим немного WEB 2.0, то есть увеличим индекс дружелюбности 🙂 Для этого выведем индикатор обработки запроса.
Для того чтобы заработал BusyIndicator, я немного поправил Index.cshtml:
1: <divdata-bind=»blockUI: dsPerson.indicator»> 2: 3: <tableclass=»table table-bordered»> 4: <thead> 5: <tr> 6: <th>Name</th> 7: <th>Age</th> 8: <th>Weight</th> 9: </tr> 10: </thead> 11: <tbodydata-bind=»foreach: dsPerson.items»> 12: <tr> 13: <tddata-bind=»text: name»></td> 14: <tddata-bind=»text: age»></td> 15: <tddata-bind=»text: weight»></td> 16: </tr> 17: </tbody> 18: </table> 19: 20: <divdata-bind=»pager: dsPerson»></div> 21: </div>
А если быть точнее, то я просто обернул весь (смотри строка 1 и 21) контент в div, который блокируется, чтобы избежать команд пользователя во время выполнения запроса.
Управления размером страниц (Pager size)
Я добавил еще немного html-разметки:
1: <spanclass=»pull-right»data-bind=»if: dsPerson.hasItems()»> 2: <spanclass=»icon-eye-open»></span> 3: <selectdata-bind=»options: site.cfg.pageSizes,
value: dsPerson.queryParams.size»></select> 4: <spanclass=»icon-filter»></span><spandata-bind=»
text: dsPerson.queryParams.total»></span> 5: </span>
И после этого, у меня появилась возможность выбрать размер страницы, а так же смотреть общее количество записей. Вот установлен размер страницы равный 5:
Простая фильтрация на основе QueryParams
А теперь добавим возможность фильтровать пользователей по имени. Для это надо доработать метод сервиса:
1: public HttpResponseMessage GetPersons(JsonQueryParams query) { 2: var items = _listOfPerson.OrderBy(x => x.Name).AsQueryable(); 3: if (query != null) { 4: var size = query.Size.HasValue ? query.Size.Value : 10; 5: if (query.Filters != null && query.Filters.FilterParams.Select(x => x.Name).Contains(«Name»)) { 6: var param = query.Filters.FilterParams.FirstOrDefault(x => x.Name.Equals(«Name»)); 7: if (param != null && param.Value != null) { 8: var filter = param.Value.ToString(); 9: if (!string.IsNullOrEmpty(filter)) { 10: items = items.Where(x => x.Name.Contains(filter)); 11: } 12: } 13: } 14: if (query.Index.HasValue && items.Count() > size) { 15: items = items.Skip(size * query.Index.Value).Take(size); 16: } 17: } 18: return Request.CreateResponse(HttpStatusCode.OK, 19: new { 20: success = «все данные», 21: total = items.Count(), 22: items = items.ToList() 23: }); 24: }
Следует обратить внимание на строки 5-13, где проверяется наличие параметра в списке фильтров. После этого надо расширить queryParams для DataSource:
1: site.vm.homeIndex = function () { 2: var clock = new site.controls.Clock(), 3: queryParamsFilter = { 4: «filters»: { 5: «logicalOperator»: «And», 6: «filterParams»: [ 7: { 8: «Name»: «Name», 9: «Operator»: «Contains», 10: «Value»: ko.observable(), 11: «DisplayName»: «Имя» 12: } 13: ] 14: } 15: }, 16: dsPerson = new site.controls.DataSource({ 17: autoLoad: true, 18: service: site.services.person 19: }, queryParamsFilter); 20: 21: dsPerson.queryParams.filters.filterParams[0].Value.subscribe(function () { 22: dsPerson.getData(); 23: }); 24: 25: return { 26: dsPerson: dsPerson, 27: clock: clock 28: }; 29: }();
Строка 3-15: Создаем объект для переопределения настроек по умолчанию для QueryParams, который является структурированным параметром для DataSource.
Строка 10: Указываем, что параметр должен ko.observable().
Строка 21-23: Подписываемся на обновления параметра. При обновлении происходит перезагрузка данных. Если учесть, что измененный параметр (в силу магии KnockoutJs) сразу же применяется к QueryParams, то нам достаточно просто перезапросить новый набор данных с учетом фильтра.
Осталось добавить поле для ввода значения фильтра:
1: <input type=»text» data-bind=»value: dsPerson.queryParams.filters.filterParams[0].Value, 2: valueUpdate: ‘afterkeydown'» />
Поле ввода напрямую привязываю к параметру фильтрации и запускаю приложение. Для того чтобы обновить скрипты на странице, нажимаю F5 и вводу букву “J” в поле фильтра:
Master/Details
Как известно, в связки “Master/Details” используется два источника данных. А зависимость между ними сводится к простой формуле: “Обновился главный – обнови зависимые”. В нашем примере уже есть один источник данных, для второго придется сделать практически те же самые манипуляции: Web API сервис, JavaScript обертку и всё остальное.
Хорошим примером для построения такой зависимости, я построю связку на двух классах из пакета SampleData. Класс Person и класс Department связаны по типу связи “мастер/детализация”. Создадим Web API контролер для класса Department:
1: publicclass DepartmentApiController : ApiController { 2: 3: privatereadonly List<Department> _listOfPerson = new List<Department>(); 4: 5: public DepartmentApiController() { 6: _listOfPerson.AddRange(People.GetDepartments()); 7: } 8: 9: public HttpResponseMessage GetDepartments(JsonQueryParams query) { 10: var items = _listOfPerson.OrderBy(x => x.Name).AsQueryable(); 11: var total = _listOfPerson.Count; 12: if (query != null) { 13: var size = query.Size.HasValue ? query.Size.Value : 10; 14: if (query.Filters != null 15: && query.Filters 16: .FilterParams 17: .Select(x => x.Name) 18: .Contains(«Name»)) { 19: var param = query.Filters.FilterParams 20: .FirstOrDefault(x => x.Name.Equals(«Name»)); 21: if (param != null && param.Value != null) { 22: var filter = param.Value.ToString(); 23: if (!string.IsNullOrEmpty(filter)) { 24: items = items.Where(x => x.Name.Contains(filter)); 25: total = items.Count(); 26: } 27: } 28: } 29: if (query.Index.HasValue && items.Count() > size) { 30: items = items.Skip(size * query.Index.Value).Take(size); 31: } 32: } 33: return Request.CreateResponse(HttpStatusCode.OK, 34: new { 35: success = «все данные», 36: total, 37: items = items.ToList() 38: }); 39: } 40: 41: public HttpResponseMessage GetDepartment(int id) { 42: return Request.CreateResponse(HttpStatusCode.OK, 43: new { 44: success = «один по идентификатору», 45: item = _listOfPerson.Where(x => x.Id.Equals(id)) 46: }); 47: } 48: 49: public HttpResponseMessage PostDepartment(Department department) { 50: thrownew NotImplementedException(); 51: } 52: 53: public HttpResponseMessage PutDepartment(Department department) { 54: thrownew NotImplementedException(); 55: } 56: 57: public HttpResponseMessage DeleteDepartment(Department department) { 58: thrownew NotImplementedException(); 59: } 60: }
Web API cервис успешно запустился, теперь сделаем JavaScript-обертка сервиса. Я не буду приводить его код потому что, практические нет никакого отличия от сервиса для Web API Person. Я также добавил упоминание о нем в файл BundleConfig.cs (строка 7).
1: bundles.Add(new ScriptBundle(«~/bundles/site»).Include( 2: «~/Scripts/app/site.core.js», 3: «~/Scripts/app/site.core.js», 4: «~/Scripts/app/site.controls.js», 5: «~/Scripts/app/site.bindingHandlers.js», 6: «~/Scripts/app/site.services.person.js», 7: «~/Scripts/app/site.services.department.js», 8: «~/Scripts/app/site.utils.js»));
В сервисе site.services.department.js упоминается site.m.Department, и мне потребуется создать класс ViewModel на JavaScript для Department:
1: (function (site, ko) { 2: 3: site.m.Department = function (dto) { 4: var me = this, data = dto || {}; 5: 6: me.id = ko.observable(data.Id); 7: me.name = ko.observable(data.Name); 8: 9: me.selected = ko.observable(false); 10: 11: return me; 12: }; 13: })(site, ko)
Для того чтобы показать два источника данных рядом, я немного поправил html-разметку, предварительно добавив код для отображения dsDepartment:
1: <div data-bind=»blockUI: dsDepartment.indicator»class=»span6″> 2: <div class=»pull-left»> 3: <i class=»icon-filter»></i> 4: <input type=»text» 5: data-bind=»value: dsDepartment.queryParams.filters.filterParams[0].Value, 6: valueUpdate: ‘afterkeydown'» class=»span2″ /> 7: </div> 8: 9: <div class=»pull-right» data-bind=»if: dsDepartment.hasItems()»> 10: <i class=»icon-eye-open»></i> 11: <select data-bind=»options: site.cfg.pageSizes, 12: value: dsDepartment.queryParams.size» class=»span1″></select> 13: <i class=»icon-filter»></i> 14: <span data-bind=»text: dsDepartment.queryParams.total»></span> 15: </div> 16: 17: <table class=»table table-bordered»> 18: <thead> 19: <tr> 20: <th>Name</th> 21: </tr> 22: </thead> 23: <tbody data-bind=»foreach: dsDepartment.items»> 24: <tr> 25: <td data-bind=»text: name»></td> 26: </tr> 27: </tbody> 28: </table> 29: 30: <div data-bind=»pager: dsDepartment»></div> 31: </div>
Отличия от dsPerson вообще никакого нет. (В будущем планируется сделать контрол типа GridView для DataSource). После всех нововведений мне остается во ViewModel страницы добавить еще один DataSource, тот самый – dsDepartment:
1: site.vm.homeIndex = function () { 2: var clock = new site.controls.Clock(), 3: queryParamsFilter = { 4: «filters»: { 5: «logicalOperator»: «And», 6: «filterParams»: [ 7: { 8: «Name»: «Name», 9: «Operator»: «Contains», 10: «Value»: ko.observable(), 11: «DisplayName»: «Имя» 12: } 13: ] 14: } 15: }, 16: queryParamsFilter0 = { 17: «filters»: { 18: «logicalOperator»: «And», 19: «filterParams»: [ 20: { 21: «Name»: «Name», 22: «Operator»: «Contains», 23: «Value»: ko.observable(), 24: «DisplayName»: «Имя» 25: } 26: ] 27: } 28: }, 29: dsPerson = new site.controls.DataSource({ 30: autoLoad: true, 31: service: site.services.person 32: }, queryParamsFilter), 33: dsDepartment = new site.controls.DataSource({ 34: autoLoad: true, 35: service: site.services.department 36: }, queryParamsFilter0); 37: 38: dsPerson.queryParams.filters.filterParams[0].Value.subscribe(function () { 39: dsPerson.getData(); 40: }); 41: 42: dsDepartment.queryParams.filters.filterParams[0].Value.subscribe(function () { 43: dsDepartment.getData(); 44: }); 45: 46: return { 47: dsDepartment: dsDepartment, 48: dsPerson: dsPerson, 49: clock: clock 50: }; 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.
1: <table class=»table table-bordered»> 2: <thead> 3: <tr> 4: <th>Name</th> 5: </tr> 6: </thead> 7: <tbody data-bind=»foreach: dsDepartment.items»> 8: <tr data-bind=»css: {‘info’:selected}, click: $parent.dsDepartment.select»> 9: <td data-bind=»text: name»></td> 10: </tr> 11: </tbody> 12: </table>
Строка 8: Привязывает событие click на изменение свойства selected. А также визуально подсвечиваем выбранную строку устанавливая CSS для этой строки в значение info.
Осталось совсем немного: надо добавить параметр DepartmentId в dsPerson и подписаться на изменение этого значения у dsDepartment.
Новый параметр для dsPerson выглядит таким образом (строка 11-16):
1: queryParamsFilter = { 2: «filters»: { 3: «logicalOperator»: «And», 4: «filterParams»: [ 5: { 6: «Name»: «Name», 7: «Operator»: «Contains», 8: «Value»: ko.observable(), 9: «DisplayName»: «Имя» 10: }, 11: { 12: «Name»: «DepartmentId», 13: «Operator»: «IsEqualTo», 14: «Value»: ko.observable(), 15: «DisplayName»: «Идентифиактор подразделения» 16: } 17: ] 18: } 19: },
У DataSource (dsDepartment) подписываемся на событие выбора Department (строка 5):
1: dsDepartment = new site.controls.DataSource({ 2: autoLoad: true, 3: service: site.services.department, 4: events: { 5: selectedHandler: reloadPersons 6: } 7: }, queryParamsFilter0);
Конструкция работает так, как и предполагалось: При выборе подразделения, происходит обновление dsPerson.
Кстати, не забудьте добавить обработку параметра DepartmentId в Web API сервисе.
Заключение
DataSource достаточно гибкий контрол для работы на UI. Он может очень многое, например:
В настоящий момент уже существуют некоторые вспомогательные контролы, которые дополняют функционал DataSource:
Ссылки
Скачать проект для экспериментов
Подробнее: http://feedproxy.google.com/~r/blogmusor/~3/8rn6zxzcsIQ/134
Источник: