SAP UI5 e Windows de confirmação: novamente sobre o contexto

Milagres acontecem - qualquer programador lhe dirá isso. (em vez da epígrafe)

Boa tarde!

Um exemplo será simples.

Há uma visão:

<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>

e o mesmo controlador "enganado":

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

});

Para parecer e se comportar, tudo isso é bastante esperado:

imagem

ao clicar no botão, os identificadores selecionados listados com uma vírgula serão exibidos (deixaremos a verificação de uma matriz não vazia e de outros ui-ninhais fora do escopo do artigo).

Mas imagine isso clicando no botão "Vamos lá!" Você precisa não apenas exibir os identificadores dos elementos selecionados, como está acontecendo agora, mas excluí-los do banco de dados.

Obviamente, essas coisas precisam ser feitas somente após confirmação adicional do usuário.

Bem, assunto trivial - adicione um fragmento de exibição para a caixa de diálogo de confirmação:

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>

E o controlador para ele:

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

    });
});

Como você pode ver, tudo é bastante clássico, no nível de exemplos do SAP UI5 SDK.
Agora, para chamar esta caixa de diálogo, redefinimos o manipulador do nosso botão principal da seguinte maneira:

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

Ou seja, aqui criamos nosso diálogo, passando como parâmetro a visualização "principal" na qual ele estará localizado e a função do manipulador de confirmação.

Aqui chegamos ao mais interessante.

Antes de ligar, revise o código do controlador de diálogo mais uma vez.

Viu algo estranho?

Não?

Bem, então, ative o modo de rastreamento e, após selecionar os dois primeiros elementos, fique à vontade para clicar no botão.

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

Viva, tudo funciona! Parece, qual é o problema? É possível que o usuário fique insaciável e queira excluir outra coisa. Vamos tentar prever suas ações: selecione o terceiro elemento e clique em "Vamos lá!":

        handlePress: function (oEvent) {
            const aSelected = this.__oViewModel.getProperty('/list/selected');  
            // aSelected = [3]
            this.__confirmDialog = new ConfirmDialog(this.getView(), () => {
                aSelected.forEach(o => {
                    //   aSelected = [1,2] ???

Por que [1,2], pergunte-me você. É bom para você, há alguém para perguntar.

E a primeira vez que vi um comportamento tão feio do intérprete, comecei a duvidar silenciosamente de mim mesma. Trabalho com várias estruturas há muito tempo, mas nunca encontrei nada parecido: sabe-se que const não garante a imutabilidade de objetos e matrizes ao longo de sua existência - mas aSelected nem é mencionado em nenhum outro lugar. Então ele apareceu, se apropriou e agora foi transferido para um retorno de chamada.

Mas não vou atormentá-lo por muito tempo.

É tudo sobre os contextos e encerramentos que todos os programadores de JS adoram.

Isto é, de fato, durante a primeira execução do handleConfirmBtn, ainda temos um link para o manipulador junto com todo o contexto (incluindo aSelected). E com a confirmação subsequente da exclusão, é isso que é chamado.

A maneira de corrigir o erro não era tão simples e inequívoca. Não bastava mover a declaração oFragmentController (após a 1ª chamada, o contexto foi perdido). E a seguinte foi a maneira mais concisa (darei apenas o código do método aberto):

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

Preste atenção nas últimas 4 linhas executáveis: dessa forma, eu "role" o ponteiro para o manipulador atual junto com o contexto correto.

Em geral, apenas algumas linhas tiveram que ser adicionadas à versão original.

Foi possível encontrar um
oDialog.setBeginButton(new sap.m.Button({ text: '', press: this.__fnConfirmBtn }));

mas aqui, toda vez que você abre uma caixa de diálogo, um novo botão é criado, isso não é permitido.

Também considerei a opção de atualizar o retorno de chamada via oDialog.getBeginButton (). AttachPress, mas apenas trava um manipulador adicional e foi nojento remover todos os disponíveis via .detachPress em uma linha.

Aqui, essa aventura surgiu quase do nada (longe do primeiro ... ah, ui5!)

PS Passando uma função para a função construtora do objeto, na qual um dos métodos possui um objeto controlador que contém referências às funções do manipulador. Parece que o que poderia dar errado?

Em geral, a situação descrita é um recurso da linguagem, e a estrutura da UI5 simplesmente não permite que ela seja resolvida com perfeição.

All Articles