0
votes

I have created a Master Detail template app in Web IDE. (See image)

RSA Fiori App

I have an OData service (ZSV_SURVEY_SRV) with many EntitySets.

Master (left side) calls my EntitySet SurveySet and loads all Surveys found.

Detail (right side) calls my EntitySet QuestionSet and I've just currently got it so it loads all Questions found. What I really need to do is bring back only the Questions specific to the Survey selected in the Master section.

It's the first time I've used the Master Detail app template and I'm a little lost with to pull data from one EntitySet that is related to data in another EntitySet.

Tried different variations of binding, but just have it set back to read all from the QuestionSet for now.

Detail View:

<mvc:View xmlns="sap.m" xmlns:core="sap.ui.core" xmlns:mvc="sap.ui.core.mvc" xmlns:l="sap.ui.layout" xmlns:f="sap.ui.layout.form"
xmlns:semantic="sap.m.semantic" xmlns:footerbar="sap.ushell.ui.footerbar" controllerName="managesurveys.controller.Detail">
<semantic:DetailPage id="page" navButtonPress="onNavBack" showNavButton="{device>/system/phone}" title="{Name}" busy="{detailView>/busy}"
    busyIndicatorDelay="{detailView>/delay}">
    <semantic:content>
        <!-- Start of Survey Details Form -->
        <f:Form editable="true" class="sapUiSmallMarginTop">
            <f:formContainers>
                <!-- Survey Details -->
                <f:FormContainer>
                    <f:formElements>
                        <!-- Survey Name -->
                        <f:FormElement label="{i18n>surveyName}">
                            <f:fields>
                                <Input value="{Name}" width="100%" id="__inputSurveyName" liveChange="validateForm"/>
                            </f:fields>
                        </f:FormElement>
                        <!-- Description -->
                        <f:FormElement label="{i18n>description}">
                            <f:fields>
                                <TextArea value="{SurveyDesc}" id="__areaSurveyDescription"/>
                            </f:fields>
                        </f:FormElement>
                    </f:formElements>
                </f:FormContainer>
                <f:FormContainer>
                    <f:formElements>
                        <f:FormElement label="{i18n>reporting}">
                            <f:fields>
                                <RadioButtonGroup width="100%" selectedIndex="-1" id="__group0" columns="3">
                                    <buttons>
                                        <RadioButton selected="true" text="{i18n>onSubmission}" id="__reportingOnSubmission" select="onReportingSelected"/>
                                        <RadioButton text="{i18n>onDate}" id="__reportingOnDate" select="onReportingDateSelected"/>
                                        <RadioButton text="{i18n>none}" id="__reportingNo" select="onNoReportingSelected"/>
                                    </buttons>
                                </RadioButtonGroup>
                            </f:fields>
                        </f:FormElement>
                        <f:FormElement label="" id="__reportDateElement" visible="false">
                            <f:fields>
                                <DatePicker id="__reportDate" placeholder="{i18n>enterDate}" change="onReportingDateChange"/>
                            </f:fields>
                        </f:FormElement>
                    </f:formElements>
                </f:FormContainer>
            </f:formContainers>
            <f:layout>
                <f:ResponsiveGridLayout/>
            </f:layout>
        </f:Form>
        <!-- Categories -->
        <!--<IconTabBar items="{/CategorySet}" id="iconTabBar" enableTabReordering="true" class="sapUiResponsiveContentPadding" expandable="false">-->
            <!--<items>-->
                <!--<IconTabFilter text="{CategoryDesc}">-->
                    <List  id="list" mode="SingleSelectMaster" delete="handleDelete" items="{path:'/QuestionSet', templateShareable:true}"
                        includeItemInSelection="true" selectionChange="onQuestionSelect" headerText="{i18n>questions}">
                        <CustomListItem>
                            <l:Grid>
                                <Text text="ID ({SurveyId}) - Question ({QuestionDesc})"
                                    class="sapUiTinyMarginTop sapUiLargeMarginBegin">
                                    <layoutData>
                                        <l:GridData span="XL9 L9 M9 S9"/>
                                    </layoutData>
                                </Text>
                                <Button icon="sap-icon://navigation-up-arrow">
                                    <layoutData>
                                        <l:GridData span="XL1 L1 M1 S1"/>
                                    </layoutData>
                                </Button>
                                <Button icon="sap-icon://navigation-down-arrow">
                                    <layoutData>
                                        <l:GridData span="XL1 L1 M1 S1"/>
                                    </layoutData>
                                </Button>
                                <Button icon="sap-icon://delete" type="Reject">
                                    <layoutData>
                                        <l:GridData span="XL1 L1 M1 S1"/>
                                    </layoutData>
                                </Button>
                            </l:Grid>
                        </CustomListItem>
                    </List>
                <!--</IconTabFilter>-->
            <!--</items>-->
        <!--</IconTabBar>-->
    </semantic:content>
    <!-- Footer -->
    <semantic:positiveAction>
        <semantic:PositiveAction text="{i18n>btnSaveSurvey}" id="btnSurveySave" press="onSaveSurvey" enabled="false"/>
    </semantic:positiveAction>
</semantic:DetailPage>

Detail Controller:

sap.ui.define([
"managesurveys/controller/BaseController",
"sap/ui/model/json/JSONModel",
"managesurveys/model/formatter",
"sap/m/MessageBox",
"sap/m/MessageToast",
"managesurveys/libs/underscore"

], function(BaseController, JSONModel, formatter, MessageBox, MessageToast, UnderScoreJS) { "use strict";

return BaseController.extend("managesurveys.controller.Detail", {

    formatter: formatter,

    /* =========================================================== */
    /* lifecycle methods                                           */
    /* =========================================================== */

    onInit: function() {
        // Model used to manipulate control states. The chosen values make sure,
        // detail page is busy indication immediately so there is no break in
        // between the busy indication for loading the view's meta data
        var oViewModel = new JSONModel({
            busy: false,
            delay: 0,
            questions: [{
                order: 1,
                title: "",
                criteria: "",
                category: "",
                competency: "",
                showScore: false,
                addImage: false,
                questionType: 0,
                minimumCommentsLength: 10,
                additionalQuestionTitle: "1",
                additionalQuestionDesc: "2",
                additionalQuestionType: 1,
                numberOfAnswers: 3,
                answers: [{
                    sequence: 1,
                    label: "Yes",
                    points: 2,
                    imageFlag: false,
                    image: "/images/happy-1.svg",
                    mandatoryComments: false,
                    additionalQuestion: false
                }, {
                    sequence: 2,
                    label: "No",
                    points: 0,
                    imageFlag: false,
                    image: "/images/sceptic.svg",
                    mandatoryComments: false,
                    additionalQuestion: false
                }, {
                    sequence: 3,
                    label: "N/A",
                    points: -2,
                    imageFlag: false,
                    image: "/images/angry-2.svg",
                    mandatoryComments: false,
                    additionalQuestion: false
                }]
            }]
        });


        this.getRouter().getRoute("object").attachPatternMatched(this._onObjectMatched, this);

        this.setModel(oViewModel, "detailView");

        this.getOwnerComponent().getModel().metadataLoaded().then(this._onMetadataLoaded.bind(this));
    },

    /* =========================================================== */
    /* begin: internal methods                                     */
    /* =========================================================== */

    /**
     * Binds the view to the object path and expands the aggregated line items.
     * @function
     * @param {sap.ui.base.Event} oEvent pattern match event in route 'object'
     * @private
     */
    _onObjectMatched: function(oEvent) {
        var sObjectId = oEvent.getParameter("arguments").objectId;
        this.getModel().metadataLoaded().then(function() {
            var sObjectPath = this.getModel().createKey("SurveySet", {
                SurveyId: sObjectId
            });
            this._bindView("/" + sObjectPath);
        }.bind(this));
    },

    /**
     * Binds the view to the object path. Makes sure that detail view displays
     * a busy indicator while data for the corresponding element binding is loaded.
     * @function
     * @param {string} sObjectPath path to the object to be bound to the view.
     * @private
     */
    _bindView: function(sObjectPath) {
        // Set busy indicator during view binding
        var oViewModel = this.getModel("detailView");

        // If the view was not bound yet its not busy, only if the binding requests data it is set to busy again
        oViewModel.setProperty("/busy", false);

        this.getView().bindElement({
            path: sObjectPath,
            events: {
                change: this._onBindingChange.bind(this),
                dataRequested: function() {
                    oViewModel.setProperty("/busy", true);
                },
                dataReceived: function() {
                    oViewModel.setProperty("/busy", false);
                }
            }
        });
    },

    _onBindingChange: function() {
        var oView = this.getView(),
            oElementBinding = oView.getElementBinding();

        // No data for the binding
        if (!oElementBinding.getBoundContext()) {
            this.getRouter().getTargets().display("detailObjectNotFound");
            // if object could not be found, the selection in the master list
            // does not make sense anymore.
            this.getOwnerComponent().oListSelector.clearMasterListSelection();
            return;
        }

        var sPath = oElementBinding.getPath(),
            oResourceBundle = this.getResourceBundle(),
            oObject = oView.getModel().getObject(sPath),
            sObjectId = oObject.SurveyId,
            sObjectName = oObject.SurveyDesc,
            oViewModel = this.getModel("detailView");

        this.getOwnerComponent().oListSelector.selectAListItem(sPath);

        oViewModel.setProperty("/saveAsTileTitle", oResourceBundle.getText("shareSaveTileAppTitle", [sObjectName]));
        oViewModel.setProperty("/shareOnJamTitle", sObjectName);
        oViewModel.setProperty("/shareSendEmailSubject",
            oResourceBundle.getText("shareSendEmailObjectSubject", [sObjectId]));
        oViewModel.setProperty("/shareSendEmailMessage",
            oResourceBundle.getText("shareSendEmailObjectMessage", [sObjectName, sObjectId, location.href]));
    },

    _onMetadataLoaded: function() {
        // Store original busy indicator delay for the detail view
        var iOriginalViewBusyDelay = this.getView().getBusyIndicatorDelay(),
            oViewModel = this.getModel("detailView");

        // Make sure busy indicator is displayed immediately when
        // detail view is displayed for the first time
        oViewModel.setProperty("/delay", 0);

        // Binding the view will set it to not busy - so the view is always busy if it is not bound
        oViewModel.setProperty("/busy", true);
        // Restore original busy indicator delay for the detail view
        oViewModel.setProperty("/delay", iOriginalViewBusyDelay);
    },

    // Question radio button selected, ready for delete question 
    onQuestionSelect: function(oEvent) {
        console.log("Question Selected");
        this.getView().byId("btnSurveySave").setEnabled(true);

        var oRouter = sap.ui.core.UIComponent.getRouterFor(this);
        oRouter.navTo("questiondetails");


        //var oContext = oEvent.getParameters().listItem.getBindingContext(this.MODEL_SITE);
        //var oData = oContext.getModel().getProperty(oContext.getPath());
        //this.getRouter().navTo("QuestionDetails", {QuestionId: oData.QuestionId});
    },

    // Delete Question Message Box  
    onDeleteQuestion: function() {
        var that = this;
        var oViewModel = this.getModel("masterView");
        MessageBox.show("Are you sure you want to delete this Question?", {
            icon: MessageBox.Icon.ERROR,
            title: "Delete Question",
            actions: [MessageBox.Action.YES, MessageBox.Action.NO],
            onClose: function(oAction) {
                if (oAction === MessageBox.Action.YES) {
                    that.getModel().remove("/SurveySet", {
                        SurveyId: oViewModel.SurveyId
                    }, {
                        success: function() {
                            MessageToast.show("Survey successfully deleted", {
                                duration: 2000
                            });
                        },
                        error: function() {
                            MessageToast.show("Error deleting Survey", {
                                duration: 2000
                            });
                        }
                    });
                }
            }
        });
    },

    // Save Survey
    onSaveSurvey: function() {
        MessageToast.show("Survey successfully saved", {
            duration: 2000
        });
    }

});

});

1
If you're not familiar with the whole Master Detail concept just search for tutorials and examples in the web. There are plenty of it and this site is not for tutorials. E.g. this tutorial: blogs.sap.com/2016/11/28/… Also I don't think that you have multiple odata services. You just have two EntitySets? You're specific problem could possibly be caused by wrong data binding. If you post a code snipped of your detail view people can also help you betterThomas L.
Hi Thomas, yes my bad I meant two entity sets, was in a rush to post this before going to lunch :) I'll update it with some example code also.Scott
You should have only one model for both master and detail screens: SurveySet?$expand=QuestionSet. As Thomas said, there are plenty of tutorials online for doing this.Chris Neve
$expand would fetch all questions from all surveys which represents an unnecessary overloadfabiopagoti

1 Answers

0
votes

Your question is very common and it is not related only with a Master/Detail app.

Actually, the solution needed is as simple as fixing your UI5 code but a mix of fixed in your oData model (so, you will have to adapt your SAP Gateway project in SEGW transaction) + service implementation + UI5 code.

oData part

You need to create an association + navigation property between your entity types.

Assuming your entity type names are Survey and Question.

  1. Create an association between Survey and Question entity types. As a survey might have multiple questions in your model, it's very important to set the cardinality to 1:n
  2. Create an association set between SurveySet and QuestionSet entity sets
  3. Create a navigation property in Survey entity type which is linked with the association you created. Let's assume your navigation property is called "ToQuestions"

This fixes your odata model

Your oData calls will be something like:

Get all surveys
GET /sap/opu/odata/sap/ZSV_SURVEY_SRV/SurveySet

Get a specific survey
GET /sap/opu/odata/sap/ZSV_SURVEY_SRV/SurveySet(1)

Get all questions from a survey
GET /sap/opu/odata/sap/ZSV_SURVEY_SRV/SurveySet(1)/ToQuestions

ABAP part

Assuming you have used Gateway Service Generation to implement your project, you will have to add a mandatory importing parameter in the function module which fetches your questions (the one mapped with the GetEntitySet operation of QuestionSet). This parameter will represent the ID of a survey. You will have also to map the SurveyId property with the imporing parameter. So you will have two rows in your mappings for SurveyId Propety. One row will have an arrow pointing to the right (for the importing parameter), and the other will have an arrow pointing to the left (for the survey id column of the exported internal table)

Assuming you have not used service generation but service implementation (when you manually develop your ABAP code in DPC_EXT class) you will need to read the survey id using the following method call:

io_tech_request_context->get_converted_keys(
  IMPORTING
    es_key_values = ls_converted_keys ).

UI5 part

You will also have to do a minor fix in your UI5 code.

Apart from using your second entity set in the list aggregation binding, like this...

<List  items="{path:'/QuestionSet', templateShareable:true}" >

You will have to use the exact name of your navigation property

<List  items="{path:'/ToQuestions' }" >

or just

<List  items="{/ToQuestions}" >

I bet templateShareable is not needed in your scenario.

So, the detail page will be binded with '/SurveySet(1)' because you already have a call to .bindElement method

this._bindView("/" + sObjectPath);

So relative to this binding, your question table will be binded to /ToQuestions navigation property.