Чудеса случаются — это вам любой программист скажет. (вместо эпиграфа)
Добрый день!
Пример будет простой.
Есть вьюха:
и настолько же «навороченный» контроллер:
Выглядеть и вести себя всё это будет вполне ожидаемо:

По нажатии на кнопку будут всплывать перечисленные через запятую выделенные айдишники (проверки на непустоту массива и прочие ui-мелочи оставим за рамками статьи).
Но представьте, что по нажатию на кнопку «Поехали!» нужно не просто вывести идентификаторы выделенных элементов, как это происходит сейчас, а удалить их из базы данных.
Такие вещи, разумеется, нужно делать только после дополнительного подтверждения со стороны пользователя.
Что ж, плёвое дело — добавляем фрагмент-вьюху для диалога-подтверждения:
ConfirmDialog.fragment.xml
И контроллер к нему:
ConfirmDialog.controller.js
Как видите, всё вполне классически, на уровне примеров SAP UI5 SDK.
Теперь чтобы вызвать это диалоговое окно, переопределяем обработчик нашей главной кнопки следующим образом:
То есть здесь мы создаём наш диалог, передавая в качестве параметра «главную» вьюху, на которой он будет располагаться, и функцию-обработчик подтверждения.
Вот тут-то мы и подошли к самому интересному.
Прежде чем листать дальше, просмотрите-ка ещё разок код контроллера диалога.
Видите что-то странное?
Нет?
Ну тогда включаем режим трассировки и, выделив первые два элемента, смело жмём кнопку.
Ура, всё работает! Казалось бы, в чём подвох? Возможно в том, что пользователь попадётся ненасытный и захочет удалить что-нибудь ещё. Попробуем предсказать его действия: выделим третий элемент и нажмём «Поехали!»:
Почему [1,2], спросите у меня вы. Вам-то хорошо, есть у кого спросить.
А я, впервые увидев такое безобразное поведение интерпретатора, стал молча сомневаться в себе. Давно работаю с различными фреймворками, но с подобным не сталкивался: оно-то известно, что const не гарантирует неизменность объектов и массивов в течение своего существования — но нигде больше aSelected даже не упоминается. Вот он объявился-присвоился, и вот передался в коллбек.
Но не буду вас долго мучить.
Всё дело в любимых всеми js-программистами контекстах и замыканиях.
То есть фактически при первом выполнении handleConfirmBtn у нас остаётся ссылка на её обработчик вместе со всем контекстом (в том числе aSelected). И при последующих подтверждениях удаления вызывается именно она.
Путь исправления ошибки оказался не таким уж простым и однозначным. Было недостаточно просто передвинуть объявление oFragmentController (после 1го вызова терялся контекст). И наиболее лаконичным способом оказался следующий (приведу код только метода open):
Обратите внимание на последние 4 исполняемые строки: таким образом я «прокидываю» указатель на актуальный обработчик вместе с правильным контекстом.
По большому счёту, к изначальному варианту пришлось добавить всего пару строк.
Можно было уложиться и в одну —
но тут, сами понимаете, при каждом открытии диалога создавалась бы новая кнопка, это не ок.
Ещё я рассматривал вариант актуализации коллбека через oDialog.getBeginButton().attachPress, но он просто навешивает дополнительный обработчик, а снимать все имеющиеся через .detachPress в одну строчку было противно.
Вот такое приключение вышло практически на ровном месте (причём далеко не первое… ах, ui5!)
P.S. Передача функции в функцию-конструктор объекта, в котором в одном из методов есть объект-контроллер, в котором содержатся ссылки на функции-обработчики. Казалось бы, что может пойти не так?
По большому счёту описанная ситуация — это особенность именно языка, а фреймворк UI5 просто не даёт её красиво решать.
Добрый день!
Пример будет простой.
Есть вьюха:
<mvc:View controllerName="MyController" xmlns="sap.m" xmlns:core="sap.ui.core" xmlns:mvc="sap.ui.core.mvc"> <Button text="Поехали!" press="handlePress" /> <Table items="{view>/list/items}" mode="MultiSelect" selectionChange="handleTableSelection"> <columns> <Column> <Text text="Ид" /> </Column> <Column> <Text text="Описание" /> </Column> </columns> <items> <ColumnListItem> <cells> <ObjectIdentifier title="{view>id}" /> <Text text="{view>description}" /> </cells> </ColumnListItem> </items> </Table> </mvc:View>
и настолько же «навороченный» контроллер:
sap.ui.controller("MyController", { onInit: function () { this.__oViewModel = new sap.ui.model.json.JSONModel({ list: { items: [ { id: 1, description: 'one' }, { id: 2, description: 'two' }, { id: 3, description: 'three' } ], selected: [] } }); this.getView().setModel(this.__oViewModel, "view"); }, handlePress: function (oEvent) { sap.m.MessageToast.show(this.__oViewModel.getProperty('/list/selected').join(', ')); }, handleTableSelection: function (oEvent) { const aSelectedCtx = oEvent.getSource().getSelectedContexts(), aSelected = aSelectedCtx.map(o => o.getObject().id); this.__oViewModel.setProperty('/list/selected', aSelected); } });
Выглядеть и вести себя всё это будет вполне ожидаемо:

По нажатии на кнопку будут всплывать перечисленные через запятую выделенные айдишники (проверки на непустоту массива и прочие ui-мелочи оставим за рамками статьи).
Но представьте, что по нажатию на кнопку «Поехали!» нужно не просто вывести идентификаторы выделенных элементов, как это происходит сейчас, а удалить их из базы данных.
Такие вещи, разумеется, нужно делать только после дополнительного подтверждения со стороны пользователя.
Что ж, плёвое дело — добавляем фрагмент-вьюху для диалога-подтверждения:
ConfirmDialog.fragment.xml
<core:FragmentDefinition xmlns='sap.m' xmlns:core='sap.ui.core' > <Dialog id='confirmDialog' title='Подтверждение' type='Message' state='Warning'> <content> <Label text='Удалить выбранные записи?' /> </content> <beginButton> <Button text='Удалить' press='handleConfirmBtn'/> </beginButton> <endButton> <Button text='Отмена' press='handleCancelBtn'/> </endButton> </Dialog> </core:FragmentDefinition>
И контроллер к нему:
ConfirmDialog.controller.js
sap.ui.define([ "sap/ui/base/ManagedObject" ], function (ManagedObject) { "use strict"; return ManagedObject.extend("project.ConfirmDialog", { constructor: function (oView, fnConfirmBtn) { this.__oView = oView; this.__fnConfirmBtn = fnConfirmBtn; }, exit: function () { delete this.__oView; delete this.__fnConfirmBtn; }, open: function () { const oView = this.__oView; let oDialog = oView.byId("confirmDialog"); if (!oDialog) { const oFragmentController = { handleConfirmBtn: () => { this.__fnConfirmBtn(); oDialog.close(); }, handleCancelBtn: () => { oDialog.close(); } }; oDialog = sap.ui.xmlfragment(oView.getId(), "project.view.fragment.ConfirmDialog", oFragmentController); oView.addDependent(oDialog); } oDialog.open(); } }); });
Как видите, всё вполне классически, на уровне примеров SAP UI5 SDK.
Теперь чтобы вызвать это диалоговое окно, переопределяем обработчик нашей главной кнопки следующим образом:
handlePress: function (oEvent) { const aSelected = this.__oViewModel.getProperty('/list/selected'); this.__confirmDialog = new ConfirmDialog(this.getView(), () => { aSelected.forEach(o => { // здесь шлём запросы бэкэнду }); this.__confirmDialog.exit(); }); this.__confirmDialog.open(); }
То есть здесь мы создаём наш диалог, передавая в качестве параметра «главную» вьюху, на которой он будет располагаться, и функцию-обработчик подтверждения.
Вот тут-то мы и подошли к самому интересному.
Прежде чем листать дальше, просмотрите-ка ещё разок код контроллера диалога.
Видите что-то странное?
Нет?
Ну тогда включаем режим трассировки и, выделив первые два элемента, смело жмём кнопку.
handlePress: function (oEvent) { const aSelected = this.__oViewModel.getProperty('/list/selected'); // aSelected = [1,2], ага, всё хорошо this.__confirmDialog = new ConfirmDialog(this.getView(), () => { aSelected.forEach(o => { // шлём бэкэнду aSelected = [1,2] }); this.__confirmDialog.exit(); }); this.__confirmDialog.open(); }
Ура, всё работает! Казалось бы, в чём подвох? Возможно в том, что пользователь попадётся ненасытный и захочет удалить что-нибудь ещё. Попробуем предсказать его действия: выделим третий элемент и нажмём «Поехали!»:
handlePress: function (oEvent) { const aSelected = this.__oViewModel.getProperty('/list/selected'); // aSelected = [3] this.__confirmDialog = new ConfirmDialog(this.getView(), () => { aSelected.forEach(o => { // шлём бэкэнду aSelected = [1,2] ЧТОООО???
Почему [1,2], спросите у меня вы. Вам-то хорошо, есть у кого спросить.
А я, впервые увидев такое безобразное поведение интерпретатора, стал молча сомневаться в себе. Давно работаю с различными фреймворками, но с подобным не сталкивался: оно-то известно, что const не гарантирует неизменность объектов и массивов в течение своего существования — но нигде больше aSelected даже не упоминается. Вот он объявился-присвоился, и вот передался в коллбек.
Но не буду вас долго мучить.
Всё дело в любимых всеми js-программистами контекстах и замыканиях.
То есть фактически при первом выполнении handleConfirmBtn у нас остаётся ссылка на её обработчик вместе со всем контекстом (в том числе aSelected). И при последующих подтверждениях удаления вызывается именно она.
Путь исправления ошибки оказался не таким уж простым и однозначным. Было недостаточно просто передвинуть объявление oFragmentController (после 1го вызова терялся контекст). И наиболее лаконичным способом оказался следующий (приведу код только метода open):
open: function () { const oView = this.__oView; let oDialog = oView.byId("confirmDialog"); if (!oDialog) { const oFragmentController = { handleConfirmBtn: function () { this.__fnConfirm(); oDialog.close(); }, handleCancelBtn: function () { oDialog.close(); } }; oDialog = sap.ui.xmlfragment(oView.getId(), "project.view.fragment.ConfirmDialog", oFragmentController); oDialog.controller = oFragmentController; oView.addDependent(oDialog); } oDialog.controller.__fnConfirm = this.__fnConfirmBtn.bind(this); oDialog.open(); }
Обратите внимание на последние 4 исполняемые строки: таким образом я «прокидываю» указатель на актуальный обработчик вместе с правильным контекстом.
По большому счёту, к изначальному варианту пришлось добавить всего пару строк.
Можно было уложиться и в одну —
oDialog.setBeginButton(new sap.m.Button({ text: 'Подтвердить', press: this.__fnConfirmBtn }));
но тут, сами понимаете, при каждом открытии диалога создавалась бы новая кнопка, это не ок.
Ещё я рассматривал вариант актуализации коллбека через oDialog.getBeginButton().attachPress, но он просто навешивает дополнительный обработчик, а снимать все имеющиеся через .detachPress в одну строчку было противно.
Вот такое приключение вышло практически на ровном месте (причём далеко не первое… ах, ui5!)
P.S. Передача функции в функцию-конструктор объекта, в котором в одном из методов есть объект-контроллер, в котором содержатся ссылки на функции-обработчики. Казалось бы, что может пойти не так?
По большому счёту описанная ситуация — это особенность именно языка, а фреймворк UI5 просто не даёт её красиво решать.