Show TOC

Step 5: Adding Actions to the WorklistLocate this document in the navigation structure

Now we can easily spot shortages on our stock, but we also would like to take action and resolve it. Either we can decide to remove the product until the shortage is resolved or order new items of the product. In this step, we will add these actions to the footer of the worklist table.

Preview
Figure 1: Actions are now available in the footer bar
Coding

You can view and download all files in the Explored app in the Demo Kit under Worklist App - Step 5.

webapp/view/Worklist.view.xml

...
<Table
   id="table"
   busyIndicatorDelay="{worklistView>/tableBusyDelay}"
   class="sapUiResponsiveMargin sapUiNoMarginTop"
   growing="true"
   growingScrollToLoad="true"
   noDataText="{worklistView>/tableNoDataText}"
   updateFinished="onUpdateFinished"
   width="auto"
   mode="MultiSelect"

   items="{
      path: '/Products',
      sorter: {
         path: 'ProductName',
         descending: false
      },
      parameters: {
         'expand': 'Supplier'
      }
   }">
...

We change the table mode to MultiSelect. This allows to select multiple items in the table. Below, we will add two buttons to the footer bar of the screen. The first button will add to the UnitsInStock property, and the second will remove the selected products.

webapp/view/Worklist.view.xml

<mvc:View
   controllerName="myCompany.myApp.controller.Worklist"
   xmlns:mvc="sap.ui.core.mvc"
   xmlns:semantic="sap.m.semantic"
   xmlns="sap.m">
   <semantic:FullscreenPage
      id="page"
      navButtonPress="onNavBack"
      showNavButton="true"
      title="{i18n>worklistViewTitle}">
      <semantic:content>
         ...
      </semantic:content>
      <semantic:sendEmailAction>
         <semantic:SendEmailAction
            id="shareEmail"
            press="onShareEmailPress"/>
      </semantic:sendEmailAction>
      <semantic:positiveAction>
         <semantic:PositiveAction text="{i18n>TableProductsReorder}" press="onUpdateStockObjects"/>
      </semantic:positiveAction>
      <semantic:negativeAction>
         <semantic:NegativeAction text="{i18n>TablePorductsUnlist}" press="onUnlistObjects"/>
      </semantic:negativeAction>

   </semantic:FullscreenPage>
</mvc:View>
...

Now we add the buttons to the footer bar of the page. The two semantic actions Negative and Positive will automatically be positioned in the footer bar. The first button will order new items of the selected products and the second one will remove them. The corresponding event handlers will be implemented in the controller.

webapp/controller/Worklist.controller.js

sap.ui.define([
   "myCompany/myApp/controller/BaseController",
   "sap/ui/model/json/JSONModel",
   "myCompany/myApp/model/formatter",
   "sap/ui/model/Filter",
   "sap/ui/model/FilterOperator",
   "sap/m/MessageToast",
   "sap/m/MessageBox"

], function(BaseController, JSONModel, formatter, Filter, FilterOperator, MessageToast, MessageBox) {
   "use strict";
   return BaseController.extend("myCompany.myApp.controller.Worklist", {
      formatter: formatter,
      ...
      /**
       * Displays an error message dialog. The displayed dialog is content density aware.
       * @param {String} sMsg The error message to be displayed
       * @private
       */
      _showErrorMessage: function(sMsg) {
         MessageBox.error(sMsg, {
            styleClass: this.getOwnerComponent().getContentDensityClass()
         });
      },

      ...
      /**
       * Error and success handler for the unlist action.
       * @param sProductId the product id for which this handler is called
       * @param bSuccess true in case of a success handler, else false (for error handler)
       * @param iRequestNumber the counter which specifies the position of this request
       * @param iTotalRequests the number of all requests sent
       * @param oData forwarded data object received from the remove/update OData API
         * @param oResponse forwarded response object received from the remove/update OData API
         * @private
         */
      _handleUnlistActionResult : function (sProductId, bSuccess, iRequestNumber, iTotalRequests, oData, oResponse){
         // create a counter for successful and one for failed requests
         // ...
         // however, we just assume that every single request was successful and display a success message once
         if (iRequestNumber === iTotalRequests) {
            MessageToast.show(this.getModel("i18n").getResourceBundle().getText("StockRemovedSuccessMsg", [iTotalRequests]));
         }
      },
      /**
       * Error and success handler for the reorder action.
       * @param sProductId the product id for which this handler is called
       * @param bSuccess true in case of a success handler, else false (for error handler)
       * @param iRequestNumber the counter which specifies the position of this request
       * @param iTotalRequests the number of all requests sent
       * @param oData forwarded data object received from the remove/update OData API
       * @param oResponse forwarded response object received from the remove/update OData API
       * @private
       */
      _handleReorderActionResult : function (sProductId, bSuccess, iRequestNumber, iTotalRequests, oData, oResponse){
         // create a counter for successful and one for failed requests
         // ...
         // however, we just assume that every single request was successful and display a success message once
         if (iRequestNumber === iTotalRequests) {
            MessageToast.show(this.getModel("i18n").getResourceBundle().getText("StockUpdatedSuccessMsg", [iTotalRequests]));
         }
      },
      /**
       * Event handler for the unlist button. Will delete the
       * product from the (local) model.
       * @public
       */
      onUnlistObjects: function() {
         var aSelectedProducts, i, sPath, oProduct, oProductId;
         aSelectedProducts = this.byId("table").getSelectedItems();
         if (aSelectedProducts.length) {
            for (i = 0; i < aSelectedProducts.length; i++) {
               oProduct = aSelectedProducts[i];
               oProductId = oProduct.getBindingContext().getProperty("ProductID");
               sPath = oProduct.getBindingContextPath();
               this.getModel().remove(sPath, {
                  success : this._handleUnlistActionResult.bind(this, oProductId, true, i+1, aSelectedProducts.length),
                  error : this._handleUnlistActionResult.bind(this, oProductId, false, i+1, aSelectedProducts.length)
               });
            }
         } else {
            this._showErrorMessage(this.getModel("i18n").getResourceBundle().getText("TableSelectProduct"));
         }
      },
      /**
       * Event handler for the order button. Will reorder the
       * product by updating the (local) model
       * @public
       */
      onUpdateStockObjects: function() {
         var aSelectedProducts, i, sPath, oProductObject;
         aSelectedProducts = this.byId("table").getSelectedItems();
         if (aSelectedProducts.length) {
            for (i = 0; i < aSelectedProducts.length; i++) {
               sPath = aSelectedProducts[i].getBindingContextPath();
               oProductObject = aSelectedProducts[i].getBindingContext().getObject();
               oProductObject.UnitsInStock += 10;
               this.getModel().update(sPath, oProductObject, {
                  success : this._handleReorderActionResult.bind(this, oProductObject.ProductID, true, i+1, aSelectedProducts.length),
                  error : this._handleReorderActionResult.bind(this, oProductObject.ProductID, false, i+1, aSelectedProducts.length)
               });
            }
         } else {
            this._showErrorMessage(this.getModel("i18n").getResourceBundle().getText("TableSelectProduct"));
         }
      }

   });
});

Let’s have a look at the implementation of the event handlers for the new actions. We first load the sap.m.MessageToast control as a new dependency to display a success message for the unlist and reorder actions.

Both actions are similar from an implementation perspective and the details are described below. They both loop over the selected items in the table and trigger a model update or deletion on the selected path. After that, a success message with the number of products processed is displayed. The table is updated automatically by the model change.

  • Order

    For each of the selected items the binding path in the model is retrieved by calling the helper method getBindingContextPath on the selected item. Additionally, the data object from the model is fetched by calling getBindingContext().getObject() on the item. We update the data object and simply add 10 items to the stock to keep things simple in this example. Then we call the update function on the model with the product path and the new object. This will trigger an OData update request to the back end and a refresh of the model afterwards (multiple requests are handled together in batch mode). When the model refreshes, the table will be updated as well because of its binding.

  • Remove

    For each of the selected items the binding path in the model is retrieved by calling the helper method getBindingContextPath on the selected item. Then, we call the remove function on the model with the product path. This triggers an OData delete request to the back end and a refresh of the OData model afterwards. Again, when the model is refreshed, the table will be updated as well because of its binding. The ODataModel v2 collects all these requests and only sends one batch request (this default behavior can be changed).

For each action we register both a success handler and an error handler. The success handler and error handler for each action is the same, but the function is called with different parameters. This allows us to use the same handler function for both the error and success case. Inside the corresponding handlers we simply display a success message once by comparing the current request number with the total number of requests. Furthermore, we assume that all of our requests always succeed.

In a real scenario, you could have a counter for error responses, and one for success responses. Finally, you could implement you own business logic for error and success cases, like displaying the number of failed and succeeded requests together with the corresponding product identified by the product ID parameter of the handlers. We don’t do this to keep things simple.

Note

In our example, the remove or order actions are only applied to items that are visible in the table, even if the Select All checkbox of the table is selected. Keep in mind that there may be more data on the back end which is currently not loaded, and therefore it is neither displayed and nor can it be selected by the user.

If you want to change this behavior, you might need to change both back-end and front-end code.

webapp/controller/Object.controller.js

...
/*global location*/
sap.ui.define([
   "myCompany/myApp/controller/BaseController",
   "sap/ui/model/json/JSONModel",
   "sap/ui/core/routing/History",
   "myCompany/myApp/model/formatter"
], function(BaseController, JSONModel, History, formatter) {
   "use strict";
   return BaseController.extend("myCompany.myApp.controller.Object", {
      formatter: formatter,
      ...
      /**
       * Event handler  for navigating back.
       * It checks if there is a history entry. If yes, history.go(-1) will happen.
       * If not, it will replace the current entry of the browser history with the worklist route.
       * Furthermore, it removes the defined binding context of the view by calling unbindElement().

       * @public
       */
      onNavBack: function() {
         var oHistory = History.getInstance();
         var sPreviousHash = oHistory.getPreviousHash();
         this.getView().unbindElement();

         if (sPreviousHash !== undefined) {
            // The history contains a previous entry
            history.go(-1);
         } else {
            // Otherwise we go backwards with a forward history
            var bReplace = true;
            this.getRouter().navTo("worklist", {}, bReplace);
         }
      },
      ...
});

When we navigate to the detail page of a product the view is bound to selected Product entity of our OData model. Choosing the navigation button on the detail page will navigate back to the worklist page, the start page of our app. If we would now remove the same product which we just saw in the detail page we discover a strange behavior: Our NotFound page is displayed.

This happens because we are removing the entity from the model and the back end, but there is still an existing binding for that product. The HTTP request still gets the product, but it is not available on the back end anymore. Therefore the back end returns an HTTP 404 response, which fires a BindingChange event. This event is still handled in our webapp/controller/Object.controller.js file, even though the object page is currently not displayed. Because the product was deleted, there is no data available. Therefore the event handler simply displays the objectNotFound target.

To prevent this, we simply call unbindElement() on the view whenever the user chooses the Back button on the detail page.

webapp/i18n/i18n.properties

...
#text of the button for Products reordering
TableProductsReorder=Order

#text for the button for Products unlisting
TablePorductsUnlist=Remove

#Text for no product selected
TableNoProductsSelected=No product selected

#Product successfully deleted
StockRemovedSuccessMsg=Product removed

#Product successfully updated
StockUpdatedSuccessMsg=Product stock level updated

#~~~ Object View ~~~~~~~~~~~~~~~~~~~~~~~~~~
...

Add the missing texts for the buttons and the message toast.

Save the changes and run the application again. Try the Order and Remove buttons with one or more products selected. The stock value will be increased or the product will be (temporarily) removed from the worklist table. As all of our changes happen on a local mock server, we can simply reload the app to reset the data again.