В минувшую субботу я имел честь читать доклад о MVVM и KnockoutJS на .NET Saturday в Днепорпетровске.
Доклад был достаточно тепло встречен публикой и у многих появились интересные вопросы,
которые не были раскрыты во время самого доклада.
Собственно говоря, я решил написать публичные ответы на некоторые из них на Хабре.
Сегодня я отвечу на вопрос о template-binding. «Как быть, если мне надо отобразить не все записи, а только подходящие определённым условиям».
Ответ находится под хабракатом.
Я не буду рассказывать, о том, что такое MVVM и KnockoutJS. На хабре можно прочитать эту статью, также будет видео с моего доклада.
Итак, для начала давайте поставим перед собой задачу — в списке людей отобразить только мужчин.
У нас уже есть код, который позволяет просто отобразить список людей и их пол (посмотреть работу в живую)
Самое простое решение задачи, это отфильтровать массив непосредственно в биндинге. Это делается модификацией параметра foreach:
Работать это будет и будет работать правильно. При изменении коллекции persons список будет поддерживаться в актуальном состоянии. При этом не будет производится полный ререндеринг шаблона. Новые элементы добавятся, а удалённые — исчезнут. Посмотреть в работе.
Итак, код необходимый для решения нашей задачи ниже:
и само представление:
Теперь, если включить думалку мы понимаем, что написали каку. Мы используем MVVM для разделения логики представления и бизнеслогики, и при этом во View пишем код для фильтрации. Это нонсенс — в MVVM за это отвечает ViewModel.
Итак, правильное решение задачи — создать во ViewModel поле в котором будут только мужчины. Это поле должно поддерживаться в актуальном состоянии при изменении поля persons. Для этого в KnockoutJS служат Dependent Observables. Давайте взглянем на код, который добавляет поле males.
Dependent observables имеют одну неочевидную особенность работы. Во время первого запуска Knockout «запоминает» к каким наблюдаемым объектам (observables) производился доступ и подписывается на их изменения. При возникновении изменений в любом из связанных observables — KO перевыполнит функцию описанную в dependentObservable.
Также стоит обратить внимение на второй аттрибут, он указывает на то, что будет this во время выполнения функции для получения значения.
Результат работы
На самом деле я слукавил, когда говорил, что KO смотрит к каким observables мы доступались при первом запуске. На самом деле, он делает это каждый раз при вычислении значения dependent observable. Я приведу уже финальную версию нашего демо приложения, в которым мы можем выбирать кого отображать (males or females), а также менять пол человеку (о, простите меня грешного).
Из нового отметим:
Изменения View тоже достаточно скромные:
Мы добавили radio-button с биндингом checked, который определяет какой из них будет выбран в соответствии со значением в поле genderToFilter. Это двунаправленный биндинг так что при изменении выбранного radio изменения будут приходить во viewModel. Мы обращались к полю genderToFilter во время фильтрации, значит фильтрация произойдёт снова.
Аналогично произойдёт со сменой пола. Она учавствовала в методе фильтрации, значи список будет перефильтрован при изменении пола любого из людей.
В свете этого, моё признание о лукавстве было своевременным. Если бы KO не пересканировал к каким observables мы обращались во время каждого перевычисления dependentObservable — изменения пола людям добавленным в runtime не приводили бы к перефильтрации.
Последния абзацы были немного запутанными, но, надеюсь, всё таки понятными.
Посмотреть финальную версию
На самом деле вся эта статья была посвящена использованию dependentObservable в качестве значения для параметра foreach в template биндинге.
Как видим, dependentObservable — очень мощная штука. Следит за всеми изменениями связанных объектов. В реальной работе вы не один раз столкнётесь с аналогичными задачами. По этому очень рекомендую для себя понять почему последний пример работает.
Спасибо, что дочитали до конца. Буду рад вопросам и конструктивной критике.
Доклад был достаточно тепло встречен публикой и у многих появились интересные вопросы,
которые не были раскрыты во время самого доклада.
Собственно говоря, я решил написать публичные ответы на некоторые из них на Хабре.
Сегодня я отвечу на вопрос о template-binding. «Как быть, если мне надо отобразить не все записи, а только подходящие определённым условиям».
Ответ находится под хабракатом.
Я не буду рассказывать, о том, что такое MVVM и KnockoutJS. На хабре можно прочитать эту статью, также будет видео с моего доклада.
Итак, для начала давайте поставим перед собой задачу — в списке людей отобразить только мужчин.
У нас уже есть код, который позволяет просто отобразить список людей и их пол (посмотреть работу в живую)
ViewModel
var Person = function(gender, name) { this.gender = ko.observable(gender); this.name = ko.observable(name); }; var viewModel = { persons: ko.observableArray([ new Person('M', 'John Smith'), new Person('M', 'Mr. Sanderson'), new Person('F', 'Mrs. Sanderson'), new Person('M', 'Agent Ralf'), new Person('F', 'Gangretta Peterson') ]) }; ko.applyBindings(viewModel);
View
<script type="text/html" id="PersonInfo"> <li> <span data-bind="text: gender"></span> <span data-bind="text: name"></span> </li> </script> <div data-bind=" template: { name: 'PersonInfo', foreach: persons}"></div>
Самое простое решение задачи, это отфильтровать массив непосредственно в биндинге. Это делается модификацией параметра foreach:
foreach: ko.utils.arrayFilter( persons(), function(p){ return p.gender() == 'M';} )
Работать это будет и будет работать правильно. При изменении коллекции persons список будет поддерживаться в актуальном состоянии. При этом не будет производится полный ререндеринг шаблона. Новые элементы добавятся, а удалённые — исчезнут. Посмотреть в работе.
Итак, код необходимый для решения нашей задачи ниже:
ViewModel
var Person = function(gender, name) { this.gender = ko.observable(gender); this.name = ko.observable(name); }; var viewModel = { persons: ko.observableArray([ new Person('M', 'John Smith'), new Person('M', 'Mr. Sanderson'), new Person('F', 'Mrs. Sanderson'), new Person('M', 'Agent Ralf'), new Person('F', 'Gangretta Peterson') ]), addMale: function() { this.persons.push(new Person('M', 'New male')); }, addFemale: function() { this.persons.push(new Person('F', 'New female')); }, removePerson: function(person) { this.persons.remove(person); } }; ko.applyBindings(viewModel);
и само представление:
View
<script type="text/html" id="PersonInfo"> <li> <span data-bind="text: gender"></span> <span data-bind="text: name"></span> <small data-bind="text: new Date()"></small> <a href="#remove" data-bind="click: function() { viewModel.removePerson($data); }">x</a> </li> </script> <div data-bind=" template: { name: 'PersonInfo', foreach: ko.utils.arrayFilter( persons(), function(p){ return p.gender() == 'M';} )}"></div> <a href="#add-male" data-bind="click: addMale">Add male</a> <a href="#add-male" data-bind="click: addFemale">Add female</a>
Теперь, если включить думалку мы понимаем, что написали каку. Мы используем MVVM для разделения логики представления и бизнеслогики, и при этом во View пишем код для фильтрации. Это нонсенс — в MVVM за это отвечает ViewModel.
Итак, правильное решение задачи — создать во ViewModel поле в котором будут только мужчины. Это поле должно поддерживаться в актуальном состоянии при изменении поля persons. Для этого в KnockoutJS служат Dependent Observables. Давайте взглянем на код, который добавляет поле males.
viewModel.males = ko.dependentObservable(function() { return ko.utils.arrayFilter(this.persons(), function(p) { return p.gender() == 'M'; }); }, viewModel);
Dependent observables имеют одну неочевидную особенность работы. Во время первого запуска Knockout «запоминает» к каким наблюдаемым объектам (observables) производился доступ и подписывается на их изменения. При возникновении изменений в любом из связанных observables — KO перевыполнит функцию описанную в dependentObservable.
Также стоит обратить внимение на второй аттрибут, он указывает на то, что будет this во время выполнения функции для получения значения.
Результат работы
На самом деле я слукавил, когда говорил, что KO смотрит к каким observables мы доступались при первом запуске. На самом деле, он делает это каждый раз при вычислении значения dependent observable. Я приведу уже финальную версию нашего демо приложения, в которым мы можем выбирать кого отображать (males or females), а также менять пол человеку (о, простите меня грешного).
ViewModel
var Person = function(gender, name) { this.gender = ko.observable(gender); this.name = ko.observable(name); this.changeGender = function() { var g = this.gender() == 'F' ? 'M' : 'F'; this.gender(g); } }; var viewModel = { genderToFilter: ko.observable('M'), persons: ko.observableArray([ new Person('M', 'John Smith'), new Person('M', 'Mr. Sanderson'), new Person('F', 'Mrs. Sanderson'), new Person('M', 'Agent Ralf'), new Person('F', 'Gangretta Peterson') ]), addMale: function() { this.persons.push(new Person('M', 'New male')); }, addFemale: function() { this.persons.push(new Person('F', 'New female')); } }; viewModel.males = ko.dependentObservable(function() { var g = this.genderToFilter(); return ko.utils.arrayFilter(this.persons(), function(p) { return p.gender() == g; }); }, viewModel); ko.applyBindings(viewModel);
Из нового отметим:
- В классе Person добавился метод для смены пола на противоположный;
- Во viewModel добавилось наблюдаемое поле «кого отображать»;
- В методе фильтрации мы сравниваем пол человека со значением поля «кого отображать».
Изменения View тоже достаточно скромные:
<script type="text/html" id="PersonInfo"> <li> <a href="#change" data-bind="text: gender, click: changeGender"></a> <span data-bind="text: name"></span> </li> </script> <table width="100%"> <tr valign="top"> <td width="50%"> <label> <input type="radio" value="M" data-bind="checked: genderToFilter" />Males </label> <label> <input type="radio" value="F" data-bind="checked: genderToFilter" />Femails </label> <div data-bind=" template: { name: 'PersonInfo', foreach: males }"></div> </td> <td> <strong>All</strong> <div data-bind=" template: { name: 'PersonInfo', foreach: persons }"></div> </td> </tr> </table> <a href="#add-male" data-bind="click: addMale">Add male</a> <a href="#add-male" data-bind="click: addFemale">Add female</a>
Мы добавили radio-button с биндингом checked, который определяет какой из них будет выбран в соответствии со значением в поле genderToFilter. Это двунаправленный биндинг так что при изменении выбранного radio изменения будут приходить во viewModel. Мы обращались к полю genderToFilter во время фильтрации, значит фильтрация произойдёт снова.
Аналогично произойдёт со сменой пола. Она учавствовала в методе фильтрации, значи список будет перефильтрован при изменении пола любого из людей.
В свете этого, моё признание о лукавстве было своевременным. Если бы KO не пересканировал к каким observables мы обращались во время каждого перевычисления dependentObservable — изменения пола людям добавленным в runtime не приводили бы к перефильтрации.
Последния абзацы были немного запутанными, но, надеюсь, всё таки понятными.
Посмотреть финальную версию
Заключение
На самом деле вся эта статья была посвящена использованию dependentObservable в качестве значения для параметра foreach в template биндинге.
Как видим, dependentObservable — очень мощная штука. Следит за всеми изменениями связанных объектов. В реальной работе вы не один раз столкнётесь с аналогичными задачами. По этому очень рекомендую для себя понять почему последний пример работает.
Спасибо, что дочитали до конца. Буду рад вопросам и конструктивной критике.