SAP UI5 y Windows de confirmación: nuevamente sobre el contexto

Los milagros suceden, cualquier programador te lo dirá. (en lugar del epígrafe) ¡

Buenas tardes!

Un ejemplo será simple.

Hay una vista:

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

y el mismo controlador "engañado":

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 ver y comportarse, todo esto será bastante esperado:

imagen

al hacer clic en el botón, aparecerán los identificadores seleccionados que se enumeran con una coma (dejaremos la verificación de la matriz no vacía y otros trifles ui fuera del alcance del artículo).

Pero imagine eso haciendo clic en el botón "¡Vamos!" no solo necesita mostrar los identificadores de los elementos seleccionados, como está sucediendo ahora, sino eliminarlos de la base de datos.

Tales cosas, por supuesto, deben hacerse solo después de una confirmación adicional por parte del usuario.

Bueno, cuestión trivial: agregue un fragmento de vista para el diálogo de confirmación:

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>

Y el controlador para ello:

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 puede ver, todo es bastante clásico, a nivel de ejemplos del SDK de SAP UI5.
Ahora, para llamar a este cuadro de diálogo, redefinimos el controlador de nuestro botón principal de la siguiente manera:

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

Es decir, aquí creamos nuestro diálogo, pasando como parámetro la vista "principal" en la que se ubicará y la función de controlador de confirmación.

Aquí llegamos a lo más interesante.

Antes de encender, revise el código del controlador de diálogo una vez más.

¿Ves algo extraño?

¿No?

Entonces, active el modo de rastreo y, después de haber seleccionado los dos primeros elementos, no dude en hacer clic en el botón.

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

¡Hurra, todo funciona! Parece, ¿cuál es el problema? Es posible que el usuario se vuelva insaciable y quiera eliminar otra cosa. Intentemos predecir sus acciones: seleccione el tercer elemento y haga clic en "¡Vamos!":

        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 qué [1,2], pregúntame tú. Es bueno para ti, hay alguien a quien preguntar.

Y la primera vez que vi un comportamiento tan feo del intérprete, comencé a dudar en silencio de mí mismo. He estado trabajando con varios marcos durante mucho tiempo, pero nunca me he encontrado con algo así: se sabe que const no garantiza la inmutabilidad de objetos y matrices a lo largo de su existencia, pero aSelected ni siquiera se menciona en ningún otro lugar. Entonces apareció, se apropió, y ahora fue transferido a una devolución de llamada.

Pero no te atormentaré por mucho tiempo.

Se trata de los contextos y cierres que aman todos los programadores de js.

Es decir, de hecho, durante la primera ejecución de handleConfirmBtn, todavía tenemos un enlace a su controlador junto con todo el contexto (incluido aSelected). Y con las posteriores confirmaciones de eliminación, es ella quien se llama.

La forma de corregir el error no fue tan simple e inequívoca. No fue suficiente solo mover la declaración oFragmentController (después de la primera llamada, se perdió el contexto). Y la siguiente fue la forma más concisa (solo daré el código del método abierto):

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 atención a las últimas 4 líneas ejecutables: de esta manera "ruedo" el puntero al controlador actual junto con el contexto correcto.

En general, solo se tuvieron que agregar un par de líneas a la versión original.

Fue posible conocer a uno
oDialog.setBeginButton(new sap.m.Button({ text: '', press: this.__fnConfirmBtn }));

pero aquí, ya sabes, cada vez que abres un cuadro de diálogo, se crea un nuevo botón, esto no está bien.

También consideré la opción de actualizar la devolución de llamada a través de oDialog.getBeginButton (). AttachPress, pero solo cuelga un controlador adicional, y fue desagradable eliminar todos los disponibles a través de .detachPress en una línea.

Aquí, tal aventura salió casi de la nada (lejos de la primera ... ¡ah, ui5!)

PD Pasar una función a la función constructora del objeto, en la que uno de los métodos tiene un objeto controlador que contiene referencias a funciones de controlador. Parece que lo que podría salir mal?

En general, la situación descrita es una característica del lenguaje, y el marco UI5 simplemente no permite que se resuelva maravillosamente.

All Articles