В минувшую субботу я имел честь читать доклад о 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 — очень мощная штука. Следит за всеми изменениями связанных объектов. В реальной работе вы не один раз столкнётесь с аналогичными задачами. По этому очень рекомендую для себя понять почему последний пример работает.
Спасибо, что дочитали до конца. Буду рад вопросам и конструктивной критике.