Чудеса случаются — это вам любой программист скажет. (вместо эпиграфа)
Добрый день!
Пример будет простой.
Есть вьюха:
и настолько же «навороченный» контроллер:
Выглядеть и вести себя всё это будет вполне ожидаемо:
По нажатии на кнопку будут всплывать перечисленные через запятую выделенные айдишники (проверки на непустоту массива и прочие 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 просто не даёт её красиво решать.
Vest
Спасибо за статью, но я с вами не совсем согласен. Если хотите, я могу подебажить ваш код, но пока я скажу вот что:
Проще присвоить null.
Это пока всё, что бросилось мне в глаза.
Keenest Автор
Благодарю за внимание к мелочам, однако скажу следующее:
Оператор delete удаляет свойство из объекта. В данном случае объектом является this, а полями — ссылка на родительскую View и функцию-обработчик. Можно присвоить и null, удалив ссылку, но само поле в объекте останется.
Весь код модуля диалога у меня практически один-в-один (за исключением бизнес-логики) совпадает с sap'овским из урока по использованию диалогов курса ui5, в т.ч. exit.
По поводу создания/удаления диалога: самая затратная операция здесь — подгрузка xml-фрагмента, и именно она выполняется лишь первый раз. Ну а инициализация начальными значениями полей объекта при новом открытии — штука необходимая.