Всем привет. Прошло немало времени с момента выхода Ext JS 5, где представили возможность разработки приложений с использованием паттерна MVVM. За это время я успел столкнуться с некоторыми трудностями, о которых хотел бы рассказать.
Начну с того, что в Ext JS 4 (а предварительно в Sencha Touch) при создании компонентов их конфигурационные свойства объявлялись в объекте config, для каждого из которых автоматически создавался свой getter и setter. Несмотря на то, что вручную писать все обработчики могло быть несколько утомительно, это был стандартный подход.
В пятой же версии Ext JS используя MVVM можно было легко избавиться от доброй части рутины: удалить конфигурационные свойства и их обработчики, а вместо этого привязаться к нужному свойству или формуле ViewModel'и. Кода становилось значительно меньше, а читаемость — лучше.
Но меня беспокоил вопрос инкапсуляции. Что если в процессе разработки часть функциональности я захочу вынести в отдельный компонент для повторного использования? Нужно ли при этом создавать собственную ViewModel? Как изменять состояние компонента: обращаться напрямую к его ViewModel'и или всё-таки стоит использовать конфигурационные свойства и их публичные setter'ы?
Мысли об этом и других вопросах, а также примеры с напильником — под катом.
Давайте попробуем создать, к примеру, таблицу каких-нибудь пользователей. Чтобы она могла добавлять и удалять записи, но при необходимости переходить в режим только чтения. Также я хочу, чтобы кнопка удаления содержала имя выделенного пользователя.
Как бы мы это сделали без использования MVVM?
Посмотреть в Sencha Fiddle
Довольно многословно, зато понятно: вся логика работы компонента находится внутри. Нужно оговориться, что можно использовать ViewController'ы, и это тоже будет считаться частью компонента, но в примерах я обойдусь без них.
Уберём обработчики кода и заменим их привязками (bind).
Посмотреть в Sencha Fiddle
Выглядит значительно лучше, правда? Особенно, если представить, что входных параметров кроме readOnly может быть гораздо больше — тогда разница будет колоссальной.
Сравнивая эти примеры, напрашиваются некоторые вопросы:
Вопрос 1. Где мы должны были создавать ViewModel? Можно ли было описать её во внешнем контейнере?
— С одной стороны, можно, но тогда мы получаем сильную связанность: каждый раз, когда мы переносим этот компонент в другое место, мы будем обязаны не забыть добавить свойство readOnly во ViewModel'и нового контейнера. Так легко ошибиться и вообще родительский контейнер не должен знать о внутренностях компонентов, которые в него добавляются.
Вопрос 2. Что такое reference? Почему мы прописали его внутри компонента?
— Reference — это аналог id компонента во ViewModel'и. Мы прописали его потому что для кнопки Remove стоит привязка к имени выделенного пользователя, а без указания reference это работать не будет.
Вопрос 3. А правильно ли так делать? Что если я захочу добавить два экземпляра в один контейнер — у них будет один reference?
— Да, и это конечно же неправильно. Нужно подумать, как это решить.
Вопрос 4. Правильно ли обращаться к ViewModel'и компонента извне?
— Вообще, работать оно будет, но это опять обращение к внутренностям компонента. Меня, по идее, не должно интересовать, есть у него ViewModel или нет. Если я хочу изменить его состояние, то я должен вызвать соответствующий setter как это и было когда-то задумано.
Вопрос 5. Можно ли всё-таки использовать конфигурационные свойства, и при этом привязываться к их значениям? Ведь в документации для этого случая есть свойство publishes?
— Можно и это хорошая идея. Кроме, конечно, проблемы с явным указанием reference в привязке. Установка режима readOnly в данном случае будет такой же, как и в Примере 1 — через публичный setter:
Посмотреть в Sencha Fiddle
Это касается последнего вопроса. Если мы привяжемся из внешнего контейнера на свойство внутреннего компонента (например, на выделенную строку таблицы) — привязка работать не будет (пруф). Это случается как только у внутреннего компонента появляется своя ViewModel — изменения свойств публикуются только в неё (а если точнее, то в первую по иерархии). На официальном форуме этот вопрос поднимался несколько раз — и пока тишина, есть лишь только зарегистрированный реквест (EXTJS-15503). Т.е, если взглянуть на картинку из КДПВ с этой точки зрения, то получается вот что:
Т.е. контейнер 1 может привязаться ко всем внутренним компонентам, кроме контейнера 2. Тот в свою очередь так же, кроме контейнера 3. Потому что все компоненты публикуют изменения свойств только в первую по иерархии ViewModel, начиная со своей.
Слишком много информации? Попробуем разобраться.
ПРЕДУПРЕЖДЕНИЕ. Решения, описанные ниже, носят статус экспериментальных. Используйте их с осторожностью, потому что обратная совместимость гарантируется не во всех случаях. Замечания, исправления и другая помощь приветствуются. Поехали!
Итак, для начала я бы хотел сформулировать своё видение разработки компонентов с MVVM:
Начнём с чего-нибудь попроще, например, с пункта 3. Здесь дело в классе-примеси
Демо на Sencha Fiddle.
Касаемо пункта 2. Кажется несправедливым, что снаружи есть возможность привязаться к свойствам компонента, а изнутри — нет. Вернее, с указанием
Демо на Sencha Fiddle
Выглядит лучше, правда? Снаружи привязываемся с указанием
Автоматизируем? Добавим к предыдущему методу
По сути всё. Правда, чуть позже мы заметим, что если задать конфигурационному свойству значение по умолчанию, то это оно не применяется ко ViewModel'и. Тоже не проблема:
Демо на Sencha Fiddle.
Самое сложное: пункт 4. Для чистоты эксперимента предыдущие фиксы не используем. Дано: два вложенных компонента с одинаковым конфигурационным свойвтвом —
Демо на Sencha Fiddle.
Выглядит просто, но не работает. Почему? Потому что если внимательно приглядеться, то следующие формы записи абсолютно идентичны:
Вариант 1.
Вариант 2.
Внимание, вопрос! К свойству
Как можно выйти из ситуации? Самое очевидное — убрать
Или нет? Надежда — это не то, с чем мы имеем дело. Другой вариант — это переназвать все конфигурационные свойства (и поля ViewModel'и) так, чтобы не было дублирования (в теории):
Вот было бы здорово, описывая внешний контейнер, указывать привязку как-нибудь так:
Не буду томить, это тоже можно сделать:
Теперь так и пишем (только вместо точки другой символ, т.к. она зарезервирована):
Демо на Sencha Fiddle.
Т.е. мы прописали более конкретный
Под капотом этого расширения к полям ViewModel'и добавляется префикс, состоящий из её имени (
Данные ViewModel'ей будут разделены по иерархии. В привязках будет конкретно видно, на свойство чьей ViewModel'и они ссылаются. Теперь можно не беспокоиться за дублирование свойств внутри иерархии ViewModel'ей. Можно писать повторно используемые компоненты без оглядки на родительский контейнер. В связке с предыдущими фиксами в сложных компонентах объём кода сокращается радикально.
Последний пример с фиксами №№1-3
Но на этом этапе частично теряется обратная совместимость. Т.е. если вы, разрабатывая компоненты, полагались на присутствие каких-то свойств во ViewModel'и родительского компонента, то последний фикс вам всё сломает: необходимо будет добавить в привязку префикс, соответствующий имени/alias'у родительской ViewModel'и.
Исходный код расширений лежит на GitHub, добро пожаловать:
github.com/alexeysolonets/extjs-mvvm-extensions
Они применены у нас в нескольких проектах — полёт более чем нормальный. Помимо того, что кода пишем меньше, появилось более чёткое понимание, как связаны компоненты — всё стало кристально ясно, голова уже не болит и перхоть исчезла.
Для себя есть один вопрос: оставить последнее расширение в виде глобального, которое действует на все ViewModel'и (
Какие у вас были нюансы при разработке c MVVM? Обсудим?
Начну с того, что в Ext JS 4 (а предварительно в Sencha Touch) при создании компонентов их конфигурационные свойства объявлялись в объекте config, для каждого из которых автоматически создавался свой getter и setter. Несмотря на то, что вручную писать все обработчики могло быть несколько утомительно, это был стандартный подход.
В пятой же версии Ext JS используя MVVM можно было легко избавиться от доброй части рутины: удалить конфигурационные свойства и их обработчики, а вместо этого привязаться к нужному свойству или формуле ViewModel'и. Кода становилось значительно меньше, а читаемость — лучше.
Но меня беспокоил вопрос инкапсуляции. Что если в процессе разработки часть функциональности я захочу вынести в отдельный компонент для повторного использования? Нужно ли при этом создавать собственную ViewModel? Как изменять состояние компонента: обращаться напрямую к его ViewModel'и или всё-таки стоит использовать конфигурационные свойства и их публичные setter'ы?
Мысли об этом и других вопросах, а также примеры с напильником — под катом.
Часть 1. Используем ViewModel
Давайте попробуем создать, к примеру, таблицу каких-нибудь пользователей. Чтобы она могла добавлять и удалять записи, но при необходимости переходить в режим только чтения. Также я хочу, чтобы кнопка удаления содержала имя выделенного пользователя.
Пример 1. Стандартный подход
Как бы мы это сделали без использования MVVM?
Посмотреть в Sencha Fiddle
Fiddle.view.UsersGrid
Ext.define('Fiddle.view.UsersGrid', {
extend: 'Ext.grid.Panel',
xtype: 'usersgrid',
config: {
/**
@cfg {Boolean} Read only mode
*/
readOnly: null
},
defaultListenerScope: true,
tbar: [{
text: 'Add',
itemId: 'addButton'
}, {
text: 'Remove',
itemId: 'removeButton'
}],
columns: [{
dataIndex: 'id',
header: 'id'
}, {
dataIndex: 'name',
header: 'name'
}],
listeners: {
selectionchange: 'grid_selectionchange'
},
updateReadOnly: function (readOnly) {
this.down('#addButton').setDisabled(readOnly);
this.down('#removeButton').setDisabled(readOnly);
},
grid_selectionchange: function (self, selected) {
var rec = selected[0];
if (rec) {
this.down('#removeButton').setText('Remove ' + rec.get('name'));
}
}
});
Установка режима Read only
readOnlyButton_click: function (self) {
this.down('usersgrid').setReadOnly(self.pressed);
}
Довольно многословно, зато понятно: вся логика работы компонента находится внутри. Нужно оговориться, что можно использовать ViewController'ы, и это тоже будет считаться частью компонента, но в примерах я обойдусь без них.
Пример 2. Добавляем MVVM
Уберём обработчики кода и заменим их привязками (bind).
Посмотреть в Sencha Fiddle
Fiddle.view.UsersGrid
Ext.define('Fiddle.view.UsersGrid', {
extend: 'Ext.grid.Panel',
xtype: 'usersgrid',
reference: 'usersgrid',
viewModel: {
data: {
readOnly: false
}
},
tbar: [{
text: 'Add',
itemId: 'addButton',
bind: {
disabled: '{readOnly}'
}
}, {
text: 'Remove',
itemId: 'removeButton',
bind: {
disabled: '{readOnly}',
text: 'Remove {usersgrid.selection.name}'
}
}],
columns: [{
dataIndex: 'id',
header: 'id'
}, {
dataIndex: 'name',
header: 'name'
}]
});
Установка режима Read only
readOnlyButton_click: function (self) {
this.down('usersgrid').getViewModel().set('readOnly', self.pressed);
}
Выглядит значительно лучше, правда? Особенно, если представить, что входных параметров кроме readOnly может быть гораздо больше — тогда разница будет колоссальной.
Сравнивая эти примеры, напрашиваются некоторые вопросы:
Вопрос 1. Где мы должны были создавать ViewModel? Можно ли было описать её во внешнем контейнере?
— С одной стороны, можно, но тогда мы получаем сильную связанность: каждый раз, когда мы переносим этот компонент в другое место, мы будем обязаны не забыть добавить свойство readOnly во ViewModel'и нового контейнера. Так легко ошибиться и вообще родительский контейнер не должен знать о внутренностях компонентов, которые в него добавляются.
Вопрос 2. Что такое reference? Почему мы прописали его внутри компонента?
— Reference — это аналог id компонента во ViewModel'и. Мы прописали его потому что для кнопки Remove стоит привязка к имени выделенного пользователя, а без указания reference это работать не будет.
Вопрос 3. А правильно ли так делать? Что если я захочу добавить два экземпляра в один контейнер — у них будет один reference?
— Да, и это конечно же неправильно. Нужно подумать, как это решить.
Вопрос 4. Правильно ли обращаться к ViewModel'и компонента извне?
— Вообще, работать оно будет, но это опять обращение к внутренностям компонента. Меня, по идее, не должно интересовать, есть у него ViewModel или нет. Если я хочу изменить его состояние, то я должен вызвать соответствующий setter как это и было когда-то задумано.
Вопрос 5. Можно ли всё-таки использовать конфигурационные свойства, и при этом привязываться к их значениям? Ведь в документации для этого случая есть свойство publishes?
— Можно и это хорошая идея. Кроме, конечно, проблемы с явным указанием reference в привязке. Установка режима readOnly в данном случае будет такой же, как и в Примере 1 — через публичный setter:
Пример 3. Fiddle.view.UsersGrid
Ext.define('Fiddle.view.UsersGrid', {
extend: 'Ext.grid.Panel',
xtype: 'usersgrid',
reference: 'usersgrid',
viewModel: {
},
config: {
readOnly: false
},
publishes: ['readOnly'],
tbar: [{
text: 'Add',
itemId: 'addButton',
bind: {
disabled: '{usersgrid.readOnly}'
}
}, {
text: 'Remove',
itemId: 'removeButton',
bind: {
disabled: '{usersgrid.readOnly}',
text: 'Remove {usersgrid.selection.name}'
}
}],
columns: [{
dataIndex: 'id',
header: 'id'
}, {
dataIndex: 'name',
header: 'name'
}]
});
Посмотреть в Sencha Fiddle
Кое-что ещё
Это касается последнего вопроса. Если мы привяжемся из внешнего контейнера на свойство внутреннего компонента (например, на выделенную строку таблицы) — привязка работать не будет (пруф). Это случается как только у внутреннего компонента появляется своя ViewModel — изменения свойств публикуются только в неё (а если точнее, то в первую по иерархии). На официальном форуме этот вопрос поднимался несколько раз — и пока тишина, есть лишь только зарегистрированный реквест (EXTJS-15503). Т.е, если взглянуть на картинку из КДПВ с этой точки зрения, то получается вот что:
Т.е. контейнер 1 может привязаться ко всем внутренним компонентам, кроме контейнера 2. Тот в свою очередь так же, кроме контейнера 3. Потому что все компоненты публикуют изменения свойств только в первую по иерархии ViewModel, начиная со своей.
Слишком много информации? Попробуем разобраться.
Часть 2. За работу!
ПРЕДУПРЕЖДЕНИЕ. Решения, описанные ниже, носят статус экспериментальных. Используйте их с осторожностью, потому что обратная совместимость гарантируется не во всех случаях. Замечания, исправления и другая помощь приветствуются. Поехали!
Итак, для начала я бы хотел сформулировать своё видение разработки компонентов с MVVM:
- Для изменения состояния компонента использовать конфигурационные свойства и их публичные setter'ы.
- Иметь возможность привязываться к собственным конфигурационным свойствам (внутри компонента).
- Иметь возможность привязываться к свойствам компонента снаружи вне зависимости, есть у него своя ViewModel или нет.
- Не задумываться об уникальности имён внутри иерархии данных ViewModel'ей.
Фикс №1. Публикуем изменения вверх
Начнём с чего-нибудь попроще, например, с пункта 3. Здесь дело в классе-примеси
Ext.mixin.Bindable
и его методе publishState. Если заглянуть внутрь, то мы увидим, что изменения публикуются во ViewModel, которая находится первой по иерархии. Давайте сделаем так, чтобы родительская ViewModel об этом тоже знала:publishState: function (property, value) {
var me = this,
vm = me.lookupViewModel(),
parentVm = me.lookupViewModel(true),
path = me.viewModelKey;
if (path && property && parentVm) {
path += '.' + property;
parentVm.set(path, value);
}
Ext.mixin.Bindable.prototype.publishState.apply(me, arguments);
}
До | После |
---|---|
Демо на Sencha Fiddle.
Фикс №2. Привязываемся к собственным конфигурационным свойствам
Касаемо пункта 2. Кажется несправедливым, что снаружи есть возможность привязаться к свойствам компонента, а изнутри — нет. Вернее, с указанием
reference
— можно, но раз мы решили, что это не очень красивый вариант, то как минимум вручную можем сделать лучше:Fiddle.view.UsersGrid
Ext.define('Fiddle.view.UsersGrid', {
extend: 'Ext.grid.Panel',
xtype: 'usersgrid',
viewModel: {
data: {
readOnly: false,
selection: null
}
},
config: {
readOnly: false
},
tbar: [{
text: 'Add',
itemId: 'addButton',
bind: {
disabled: '{readOnly}'
}
}, {
text: 'Remove',
itemId: 'removeButton',
bind: {
disabled: '{readOnly}',
text: 'Remove {selection.name}'
}
}],
// ...
updateReadOnly: function (readOnly) {
this.getViewModel().set('readOnly', readOnly);
},
updateSelection: function (selection) {
this.getViewModel().set('selection', selection);
}
});
Демо на Sencha Fiddle
Выглядит лучше, правда? Снаружи привязываемся с указанием
reference
, а изнутри — без. Теперь каким бы он ни был, код компонента не меняется. Более того, теперь мы можем добавить два компонента в один контейнер, дать им свои названия
reference
— и всё будет работать!Автоматизируем? Добавим к предыдущему методу
publishState
:if (property && vm && vm.getView() == me) {
vm.set(property, value);
}
По сути всё. Правда, чуть позже мы заметим, что если задать конфигурационному свойству значение по умолчанию, то это оно не применяется ко ViewModel'и. Тоже не проблема:
Ext.ux.mixin.Bindable
Ext.define('Ext.ux.mixin.Bindable', {
initBindable: function () {
var me = this;
Ext.mixin.Bindable.prototype.initBindable.apply(me, arguments);
me.applyInitialPublishedState();
},
/**
Notifying parent ViewModel about state changes
*/
publishState: function (property, value) {
var me = this,
vm = me.lookupViewModel(),
parentVm = me.lookupViewModel(true),
path = me.viewModelKey;
if (path && property && parentVm) {
path += '.' + property;
parentVm.set(path, value);
}
Ext.mixin.Bindable.prototype.publishState.apply(me, arguments);
if (property && vm && vm.getView() == me) {
vm.set(property, value);
}
},
/**
Getting published state
*/
getInitialPublishedState: function () {
var me = this,
state = me.publishedState || (me.publishedState = {}),
publishes = me.getPublishes();
for (name in publishes) {
if (state[name] === undefined) {
state[name] = me[name];
}
}
return state;
},
/**
Applying published state to own ViewModel
*/
applyInitialPublishedState: function () {
var me = this,
vm = me.lookupViewModel(),
state;
if (vm && vm.getView() == me) {
state = me.getInitialPublishedState();
vm.set(state);
}
}
}, function () {
Ext.Array.each([Ext.Component, Ext.Widget], function (Class) {
Class.prototype.initBindable = Ext.ux.mixin.Bindable.prototype.initBindable;
Class.prototype.publishState = Ext.ux.mixin.Bindable.prototype.publishState;
Class.mixin([Ext.ux.mixin.Bindable]);
});
});
Демо на Sencha Fiddle.
Фикс №3. Разделяем ViewModel'и компонентов
Самое сложное: пункт 4. Для чистоты эксперимента предыдущие фиксы не используем. Дано: два вложенных компонента с одинаковым конфигурационным свойвтвом —
color
. Каждый использует ViewModel для привязки к этому значению. Требуется: привязать свойство внутреннего компонента к свойству внешнего. Попробуем?Fiddle.view.OuterContainer
Ext.define('Fiddle.view.OuterContainer', {
// ...
viewModel: {
data: {
color: null
}
},
config: {
color: null
},
items: [{
xtype: 'textfield',
fieldLabel: 'Enter color',
listeners: {
change: 'colorField_change'
}
}, {
xtype: 'displayfield',
fieldLabel: 'Color',
bind: '{color}'
}, {
xtype: 'innercontainer',
bind: {
color: '{color}'
}
}],
colorField_change: function (field, value) {
this.setColor(value);
},
updateColor: function (color) {
this.getViewModel().set('color', color);
}
})
Fiddle.view.InnerContainer
Ext.define('Fiddle.view.InnerContainer', {
// ...
viewModel: {
data: {
color: null
}
},
config: {
color: null
},
items: [{
xtype: 'displayfield',
fieldLabel: 'Color',
bind: '{color}'
}],
updateColor: function (color) {
this.getViewModel().set('color', color);
}
})
Демо на Sencha Fiddle.
Выглядит просто, но не работает. Почему? Потому что если внимательно приглядеться, то следующие формы записи абсолютно идентичны:
Вариант 1.
|
|
Вариант 2.
|
|
Внимание, вопрос! К свойству
color
чьей ViewModel'и мы биндимся во внутреннем контейнере? Как ни странно, в обоих случаях — к внутренней. При этом, судя по документации и картинке из шапки, данные ViewModel'и внешнего контейнера являются прототипом для данных ViewModel'и внутреннего. А т.к. у последнего переопределено значение color
, то при изменении значения у прототипа, у наследника оно остаётся старым (null
). Т.е. в принципе, глюка нет — так и должно быть.Как можно выйти из ситуации? Самое очевидное — убрать
color
из внутренней ViewModel'и. Тогда нам также придётся убрать обработчик updateColor
. И конфигурационное свойство — тоже в топку! Будем надеяться, что в родительском контейнере всегда будет ViewModel со свойством color
. Или нет? Надежда — это не то, с чем мы имеем дело. Другой вариант — это переназвать все конфигурационные свойства (и поля ViewModel'и) так, чтобы не было дублирования (в теории):
outerContainerColor
и innerContainerColor
. Но это тоже ненадёжно. В больших проектах столько имён, да и вообще не очень красиво получается.Вот было бы здорово, описывая внешний контейнер, указывать привязку как-нибудь так:
Ext.define('Fiddle.view.OuterContainer', {
viewModel: {
data: {
color: null
}
},
items: [{
xtype: 'innercontainer',
bind: {
color: '{outercontainer.color}' // с префиксом
}
}]
})
Не буду томить, это тоже можно сделать:
Ext.ux.app.SplitViewModel
/**
An override to be able split ViewModel data by ViewModel instances
*/
Ext.define('Ext.ux.app.SplitViewModel', {
override: 'Ext.app.ViewModel',
config: {
/**
@cfg {String}
ViewModel name
*/
name: undefined,
/**
@cfg {String}
@private
name + sequential identifer
*/
uniqueName: undefined,
/**
@cfg {String}
@private
uniqueName + nameDelimiter
*/
prefix: undefined
},
nameDelimiter: '|',
expressionRe: /^(?:\{[!]?(?:(\d+)|([a-z_][\w\-\.|]*))\})$/i,
uniqueNameRe: /-\d+$/,
privates: {
applyData: function (newData, data) {
newData = this.getPrefixedData(newData);
data = this.getPrefixedData(data);
return this.callParent([newData, data]);
},
applyLinks: function (links) {
links = this.getPrefixedData(links);
return this.callParent([links]);
},
applyFormulas: function (formulas) {
formulas = this.getPrefixedData(formulas);
return this.callParent([formulas]);
},
bindExpression: function (path, callback, scope, options) {
path = this.getPrefixedPath(path);
return this.callParent([path, callback, scope, options]);
}
},
bind: function (descriptor, callback, scope, options) {
if (Ext.isString(descriptor)) {
descriptor = this.getPrefixedDescriptor(descriptor);
}
return this.callParent([descriptor, callback, scope, options]);
},
linkTo: function (key, reference) {
key = this.getPrefixed(key);
return this.callParent([key, reference]);
},
get: function (path) {
path = this.getPrefixedPath(path);
return this.callParent([path]);
},
set: function (path, value) {
if (Ext.isString(path)) {
path = this.getPrefixed(path);
}
else if (Ext.isObject(path)) {
path = this.getPrefixedData(path);
}
this.callParent([path, value]);
},
applyName: function (name) {
name = name || this.type || 'viewmodel';
return name;
},
applyUniqueName: function (id) {
id = id || Ext.id(null, this.getName() + '-');
return id;
},
applyPrefix: function (prefix) {
prefix = prefix || this.getUniqueName() + this.nameDelimiter;
return prefix;
},
/**
Apply prefix to property names
*/
getPrefixedData: function (data) {
var name, newName, value,
result = {};
if (!data) {
return null;
}
for (name in data) {
value = data[name];
newName = this.getPrefixed(name);
result[newName] = value;
}
return result;
},
/**
Add prefix to a string
*/
getPrefixed: function (name) {
var prefix = this.getPrefix();
var result = name.indexOf(this.nameDelimiter) != -1 ? name : prefix + name;
return result;
},
/**
Get descriptor with a correct prefix
*/
getPrefixedDescriptor: function (descriptor) {
var descriptorParts = this.expressionRe.exec(descriptor);
if (!descriptorParts) {
return descriptor;
}
var path = descriptorParts[2]; // '{foo}' -> 'foo'
descriptor = descriptor.replace(path, this.getPrefixedPath(path));
return descriptor;
},
/**
Get path with a correct prefix
*/
getPrefixedPath: function (path) {
var nameDelimiterPos = path.lastIndexOf(this.nameDelimiter),
hasName = nameDelimiterPos != -1,
name,
isUnique,
vmUniqueName,
vm;
if (hasName) {
// bind to a ViewModel by name: viewmodel|foo.bar
name = path.substring(0, nameDelimiterPos + this.nameDelimiter.length - 1);
isUnique = this.uniqueNameRe.test(name);
if (!isUnique) {
// replace name by uniqueName: viewmodel-123|foo.bar
vm = this.findViewModelByName(name);
if (vm) {
vmUniqueName = vm.getUniqueName();
path = vmUniqueName + path.substring(nameDelimiterPos);
}
else {
Ext.log({ level: 'warn' }, 'Cannot find a ViewModel instance by specifed name/type: ' + name);
}
}
}
else {
// bind to this ViewModel: foo.bar -> viewmodel-123|foo.bar
path = this.getPrefixed(path);
}
return path;
},
/**
Find a ViewModel by name up by hierarchy
@param {String} name ViewModel's name
@param {Boolean} skipThis Pass true to ignore this instance
*/
findViewModelByName: function (name, skipThis) {
var result,
vm = skipThis ? this.getParent() : this;
while (vm) {
if (vm.getName() == name) {
return vm;
}
vm = vm.getParent();
}
return null;
}
});
Теперь так и пишем (только вместо точки другой символ, т.к. она зарезервирована):
Ext.define('Fiddle.view.OuterContainer', {
viewModel: {
name: 'outercontainer',
data: {
color: null
}
},
items: [{
xtype: 'innercontainer',
bind: {
color: '{outercontainer|color}'
}
}]
})
Демо на Sencha Fiddle.
Т.е. мы прописали более конкретный
bind
с указанием имени ViewModel'и. При вынесении кода ViewModel'и в отдельный файл, имя можно не указывать — оно возьмётся из alias
. Всё, больше никаких изменений не требуется. На свою ViewModel можно привязываться по старинке без префикса. Его мы указываем для вложенных компонентов, у которых есть (или может появиться) своя ViewModel.Под капотом этого расширения к полям ViewModel'и добавляется префикс, состоящий из её имени (
name
или alias
) и уникального id
(как для компонентов). Затем, в момент инициализации компонентов, он добавляется к названиям всех привязок.Что это даёт?
Данные ViewModel'ей будут разделены по иерархии. В привязках будет конкретно видно, на свойство чьей ViewModel'и они ссылаются. Теперь можно не беспокоиться за дублирование свойств внутри иерархии ViewModel'ей. Можно писать повторно используемые компоненты без оглядки на родительский контейнер. В связке с предыдущими фиксами в сложных компонентах объём кода сокращается радикально.
Последний пример с фиксами №№1-3
Но на этом этапе частично теряется обратная совместимость. Т.е. если вы, разрабатывая компоненты, полагались на присутствие каких-то свойств во ViewModel'и родительского компонента, то последний фикс вам всё сломает: необходимо будет добавить в привязку префикс, соответствующий имени/alias'у родительской ViewModel'и.
Итого
Исходный код расширений лежит на GitHub, добро пожаловать:
github.com/alexeysolonets/extjs-mvvm-extensions
Они применены у нас в нескольких проектах — полёт более чем нормальный. Помимо того, что кода пишем меньше, появилось более чёткое понимание, как связаны компоненты — всё стало кристально ясно, голова уже не болит
Для себя есть один вопрос: оставить последнее расширение в виде глобального, которое действует на все ViewModel'и (
override
), или вынести как класс, от которого наследоваться? Второе решение вроде более демократично, но не внесёт ли оно большей путаницы? В общем, пока этот вопрос открытый.Какие у вас были нюансы при разработке c MVVM? Обсудим?
aleksandy
Плагин, который делает override для объекта, на котором вызван.
alexstz
Вариант. Правда, плагины поддерживаются только компонентами, т.е. можно попробовать пойти путём mixin'ов.