SAP UI5 and Confirmation Windows: Again About the Context

Miracles happen - any programmer will tell you that. (instead of the epigraph)

Good afternoon!

An example will be simple.

There is a view:

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

and the same "tricked out" controller:

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

});

To look and behave, all this will be quite expected:

image

By clicking on the button, the selected identifiers listed with a comma will pop up (we will leave the check for non-empty array and other ui-trifles outside the scope of the article).

But imagine that by clicking on the button "Let's go!" You need to not only display the identifiers of the selected elements, as is happening now, but delete them from the database.

Such things, of course, need to be done only after additional confirmation by the user.

Well, trivial matter - add a view fragment for the confirmation dialog:

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>

And the controller to it:

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

    });
});

As you can see, everything is quite classical, at the level of examples of the SAP UI5 SDK.
Now, to call this dialog box, we redefine the handler of our main button as follows:

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

That is, here we create our dialogue, passing as the parameter the "main" view on which it will be located, and the confirmation handler function.

Here we came to the most interesting.

Before flipping on, review the dialog controller code once more.

See something strange?

No?

Well then, turn on the trace mode and, having selected the first two elements, feel free to click the button.

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

Hooray, everything works! It would seem, what's the catch? It’s possible that the user gets insatiable and wants to delete something else. Let's try to predict its actions: select the third element and click “Let's Go!”:

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

Why [1,2], ask me you. It’s good for you, there is someone to ask.

And the first time I saw such an ugly behavior of the interpreter, I began to silently doubt myself. I have been working with various frameworks for a long time, but I have never encountered anything like it: it is known that const does not guarantee the immutability of objects and arrays throughout its existence - but aSelected is not even mentioned anywhere else. So he showed up, appropriated, and now he was transferred to a callback.

But I won’t torment you for a long time.

It's all about the contexts and closures that all js programmers love.

That is, in fact, at the first execution of handleConfirmBtn, we still have a link to its handler along with the entire context (including aSelected). And with subsequent confirmations of deletion, it is she who is called.

The way to correct the error was not so simple and unambiguous. It was not enough just to move the oFragmentController declaration (after the 1st call, the context was lost). And the following was the most concise way (I will give the code of the open method only):

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

Pay attention to the last 4 executable lines: in this way I "roll" the pointer to the current handler along with the correct context.

By and large, only a couple of lines had to be added to the original version.

It was possible to meet one
oDialog.setBeginButton(new sap.m.Button({ text: '', press: this.__fnConfirmBtn }));

but here, you know, every time you open a dialog, a new button would be created, this is not ok.

I also considered the option of updating the callback via oDialog.getBeginButton (). AttachPress, but it just hangs an additional handler, and it was disgusting to remove all available via .detachPress in one line.

Here such an adventure came out almost out of the blue (far from the first ... ah, ui5!)

PS Passing a function to the constructor function of the object, in which one of the methods has a controller object that contains references to handler functions. It would seem that what could go wrong?

By and large, the described situation is a feature of the language, and the UI5 ​​framework simply does not allow it to be beautifully solved.

All Articles