SAP 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);     }  });

Выглядеть и вести себя всё это будет вполне ожидаемо:

image

По нажатии на кнопку будут всплывать перечисленные через запятую выделенные айдишники (проверки на непустоту массива и прочие 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 просто не даёт её красиво решать.

ссылка на оригинал статьи https://habr.com/ru/post/498370/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *