SAP UI5 et Windows de confirmation: encore une fois sur le contexte

Des miracles se produisent - tout programmeur vous le dira. (au lieu de l'épigraphe)

Bonjour!

Un exemple sera simple.

Il y a une vue:

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

et le même contrôleur "trompé":

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

});

Pour regarder et se comporter, tout cela sera tout à fait normal:

image

en cliquant sur le bouton, les identifiants sélectionnés répertoriés avec une virgule apparaîtront (nous laisserons la vérification du tableau non vide et d'autres ui-trifles en dehors du champ d'application de l'article).

Mais imaginez qu'en cliquant sur le bouton "Allons-y!" Vous devez non seulement afficher les identifiants des éléments sélectionnés, comme c'est le cas actuellement, mais les supprimer de la base de données.

De telles choses, bien sûr, ne doivent être effectuées qu'après confirmation supplémentaire de l'utilisateur.

Eh bien, question banale - ajoutez un fragment de vue pour la boîte de dialogue de confirmation:

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>

Et le contrôleur à elle:

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

    });
});

Comme vous pouvez le voir, tout est assez classique, au niveau des exemples du SDK SAP UI5.
Maintenant, pour appeler cette boîte de dialogue, nous redéfinissons le gestionnaire de notre bouton principal comme suit:

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

C'est-à-dire que nous créons ici notre dialogue, en passant en paramètre la vue "principale" sur laquelle il sera situé, et la fonction de gestionnaire de confirmation.

Ici, nous sommes arrivés au plus intéressant.

Avant de retourner, revoyez le code du contrôleur de dialogue.

Vous voyez quelque chose d'étrange?

Non?

Eh bien, activez le mode trace et, après avoir sélectionné les deux premiers éléments, n'hésitez pas à cliquer sur le bouton.

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

Hourra, tout fonctionne! Il semblerait, quel est le piège? Il est possible que l'utilisateur devienne insatiable et veuille supprimer autre chose. Essayons de prédire ses actions: sélectionnez le troisième élément et cliquez sur «Allons-y!»:

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

Pourquoi [1,2], demandez-moi. C'est bon pour vous, il y a quelqu'un à demander.

Et la première fois que j'ai vu un comportement aussi laid de l'interprète, j'ai commencé à douter silencieusement de moi-même. Je travaille avec différents frameworks depuis longtemps, mais je n'ai jamais rien rencontré de tel: il est connu que const ne garantit pas l'immuabilité des objets et des tableaux tout au long de son existence - mais aSelected n'est même pas mentionné ailleurs. Il s'est donc présenté, s'est approprié, et maintenant il a été transféré vers un rappel.

Mais je ne te tourmenterai pas longtemps.

Tout dépend des contextes et des fermetures que tous les programmeurs js adorent.

En fait, lors de la première exécution de handleConfirmBtn, nous avons toujours un lien vers son gestionnaire avec l'ensemble du contexte (y compris aSelected). Et avec les confirmations d'effacement ultérieures, c'est elle qui est appelée.

La manière de corriger l'erreur n'était pas si simple et sans ambiguïté. Il ne suffisait pas de déplacer la déclaration oFragmentController (après le premier appel, le contexte était perdu). Et ce qui suit était la manière la plus concise (je ne donnerai que le code de la méthode ouverte):

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

Faites attention aux 4 dernières lignes exécutables: de cette façon, je "roule" le pointeur vers le gestionnaire actuel avec le contexte correct.

Dans l'ensemble, seules quelques lignes ont dû être ajoutées à la version originale.

Il a été possible de rencontrer un
oDialog.setBeginButton(new sap.m.Button({ text: '', press: this.__fnConfirmBtn }));

mais ici, vous savez, chaque fois que vous ouvrez une boîte de dialogue, un nouveau bouton est créé, ce n'est pas ok.

J'ai également envisagé la possibilité de mettre à jour le rappel via oDialog.getBeginButton (). AttachPress, mais il bloque simplement un gestionnaire supplémentaire, et il était dégoûtant de supprimer tous les éléments disponibles via .detachPress sur une seule ligne.

Ici, une telle aventure est sortie presque à l'improviste (et loin de la première ... ah, ui5!)

PS Passer une fonction à la fonction constructeur de l'objet, dans laquelle l'une des méthodes a un objet contrôleur qui contient des références aux fonctions de gestionnaire. Il semblerait que ce qui pourrait mal tourner?

Dans l'ensemble, la situation décrite est une caractéristique du langage, et le cadre UI5 ​​ne permet tout simplement pas de le résoudre magnifiquement.

All Articles