EDUCAÇÃO E TECNOLOGIA

Custom Card on OVP app with OData Binding

In this article I will show the OData integration with custom developed OVP card.

I had a scenario where I had to show multiple data points in a single card. The problem is that it is not possible to do so with any out-of-the-box solution. So, I had to build my own. There are numerous articles available which show the process of building a custom card, some of them are mentioned below:

https://blogs.sap.com/2017/05/21/create-your-own-custom-card-in-a-sap-fiori-overview-page/

https://blogs.sap.com/2018/10/11/overview-page-ovp-custom-cards/

https://sapui5.hana.ondemand.com/sdk/#/topic/6d260f7708ca4c4a9ff45e846402aebb.html

But none of them shows how to integrate OData service and that too dynamically. Meaning that any OData service can be bound to the new card template and it will show the data according to the annotation. Though I didn’t create this card exclusively dynamic, but it will not be so hard to do so once you know the concept.

Let me show what I wanted to achieve

Requirement and explanation: there are 32 total orders having some kind of block, either planning, execution or invoice. There could be order that might have more than one block, eg., one order has both execution as well as invoice block.

How to create custom card is very well explained in the aforementioned articles, Lets jump into integrating it with OData service.

First, the OData service. I created a CDS view with four (or more) data points, which is nothing but four fields with some default aggregation (#SUM in my case).

CDS%20cube%20view%20with%20multiple%20data%20points

CDS cube view with multiple data points

Now, I will add them in my annotation, either as data point or data field. It is up to you to either add them as data point or data field, but I wanted keep the distinction on what goes on header and what on items and so I added one as Data point and rest as Data field. I also wanted to navigate to a list report on pressing either the header of the card or by pressing on any of the item. If any item is pressed, the list report would only show the relevant filtered record. For this, the OData service has three different flags to filter the records out.

File structure on OVP project

Local%20annotations%20in%20the%20SAP%20OVP%20app

Local annotations in the SAP OVP app

Lets now build the card.

I followed this link to create the necessary files in my OVP project. The file structure looks like below:

File%20Structure%20on%20OVP%20project

File Structure on OVP project

My Component.js file contains the metadata for the card and it looks like below:

The blurred part in the above image is namespace.appname. 

the headerExtensionFragment is added to show the total value on the card header and contains the following XML code

<core:FragmentDefinition xmlns="sap.m" xmlns:core="sap.ui.core"> <FlexBox class="sapOvpHeaderCountMainDiv"> <NumericContent value="{BlockCounts>/blockedOrderCount}" valueColor="Neutral"/> </FlexBox>
</core:FragmentDefinition>

Since, I wanted to show items in a list manner, I added sap.m.table for it and I kept the column headings blank. by the way, any control can be added here and it just depends on how you want to visualize the data. I am going for a tabular content Here is the XML

<core:FragmentDefinition xmlns:core="sap.ui.core" xmlns="sap.m" xmlns:l="sap.ui.layout"> <Table id="idFOBlockedCardTable" inset="false" items="{path: 'BlockCounts>/KPICounts'}" itemPress="_onNavigationbyItem"> <columns> <Column width="9em"/> <Column/> </columns> <items> <ColumnListItem visible="{BlockCounts>visible}"> <cells> <ObjectIdentifier title="{BlockCounts>Text}" text="{BlockCounts>Count}"/> <ProgressIndicator class="sapUiSmallMarginBottom" percentValue="{BlockCounts>Pecentage}" state="{BlockCounts>Color}"/> </cells> </ColumnListItem> </items> </Table>
</core:FragmentDefinition>

BlockCounts is a json data model I used.

Now add the card in the manifest.json file with all annotations and template for the card which is this custom one.

Now, the most important and heart of operation for this to work, the Controller. The first thing to do in the controller is to read all the annotation configurations. I did this in onInit method of the controller

 var sLineItemAnnotationPath = oCardModelData.annotationPath, sIdentificationAnnotationPath = oCardModelData.identificationAnnotationPath, sSelectioinVariant = oCardModelData.selectionAnnotationPath, sSelectFieldsString = ""; this.entitySetname = oCardModelData.entitySet; // first get the Semantic object and action this.sSemanticObject = oCardModelData.entityType[sIdentificationAnnotationPath][0].SemanticObject.String; this.sAction = oCardModelData.entityType[sIdentificationAnnotationPath][0].Action.String; var aLineItems = oCardModelData.entityType[sLineItemAnnotationPath]; for (var i = 0; i < aLineItems.length; i++) { if (aLineItems[i].RecordType === "com.sap.vocabularies.UI.v1.DataField") { sSelectFieldsString = aLineItems[i].Value.Path + "," + sSelectFieldsString; } else if (aLineItems[i].RecordType === "com.sap.vocabularies.UI.v1.DataFieldForAnnotation") { sSelectFieldsString = oCardModelData.entityType[aLineItems[i].Target.AnnotationPath.substring(1)].Value.Path + "," + sSelectFieldsString; } }
this.selectFields = sSelectFieldsString.slice(0, -1);

Now I have the entityset to read, the fields to select along with the semantic object and action for navigation.

I have also added a on click event on the card header to navigate to list report:

this.getView().byId("ovpCardHeader").attachBrowserEvent("click", this._onNavigationbyCardHeader);

in case if there is a global filter bar present, then you can subscribe to the “OVPGlobalFilterSeacrhfired” event in case if the user adds a filter criteria. The following code can help to do this:

this.GloabalEventBus = sap.ui.getCore().getEventBus();
this.GloabalEventBus.subscribe("OVPGlobalfilter", "OVPGlobalFilterSeacrhfired", this.onGlobalfilterApply.bind(this));

onGlobalfilterApply is the fallback method when the event is fired.

Since I also want to navigate on click event on the items as well, I need to set template type as active for the sap.m.table, like below

this.getView().byId("idFOBlockedCardTable").getBindingInfo("items").template.setType("Active");

Selection of data from OData service can be done in either “onInit” or “onAfterRendering”. I did it on the latter.

I get the model directly from the view (of this card, mentioned in the manifest file)

 onAfterRendering: function () { var oModel = this.getView().getModel(); var sEntitySet = "/" + this.entitySetname; var oParamater = { urlParameters: { "$select": this.selectFields }, filters: this.aFilter, success: this._onSuccess.bind(this) }; oModel.read(sEntitySet, oParamater); }

in my case, I just created a json model and add it to the view

 _onSuccess: function (data) { var nBlockedPlanningCount = 0, nBlockedExecutionCount = 0, nBlockedInvoiceCount = 0, nBlockedOrderCount = 0, nPlanning = 0, nExecution = 0, nInvoice = 0; if (data.results.length > 0) { nBlockedPlanningCount = data.results[0].BlockedPlanningCount; nBlockedExecutionCount = data.results[0].BlockedExecutionCount; nBlockedInvoiceCount = data.results[0].BlockedInvoiceCount; nBlockedOrderCount = data.results[0].BlockedOrderCount; nPlanning = nBlockedPlanningCount / nBlockedOrderCount * 100; nExecution = nBlockedExecutionCount / nBlockedOrderCount * 100; nInvoice = nBlockedInvoiceCount / nBlockedOrderCount * 100; } var aValues = [{ "Text": "Planning Block", "Count": nBlockedPlanningCount, "Pecentage": nPlanning, "Field": "PlanningBlock", "Value": true, "Color": "None", "visible": nBlockedPlanningCount == 0 ? false : true }, { "Text": "Execution Block", "Count": nBlockedExecutionCount, "Pecentage": nExecution, "Field": "ExecBlock", "Value": true, "Color": "Success", "visible": nBlockedExecutionCount == 0 ? false : true }, { "Text": "Invoice Block", "Count": nBlockedInvoiceCount, "Pecentage": nInvoice, "Field": "FSDBlock", "Value": true, "Color": "Information", "visible": nBlockedInvoiceCount == 0 ? false : true }]; var oObject = { "blockedOrderCount": nBlockedOrderCount, "KPICounts": aValues }; var oModel = new sap.ui.model.json.JSONModel(oObject); this.getView().setModel(oModel, "BlockCounts");

Handle global filter bar searched fired event:

When the event is fired, the callback function / listener gets three arguments, Channel ID, event name and the array of filters. Handling of this event is quite easy and straightforward

onGlobalfilterApply: function (sChannelID, sEventName, aFilters) { var oModel = this.getView().getModel(); var sEntitySet = "/" + this.entitySetname; var oParamater = { urlParameters: { "$select": this.selectFields }, filters: aFilters, success: this._onSuccess.bind(this) }; oModel.read(sEntitySet, oParamater); },

The above code is exactly same as what we did in “onAfterRendering” with an exception of filters.

Last thing is navigation.

for navigation from header, we already have an event lister we added in the “onInit” hook method.

We just need to create an instance of “CrossApplicationNavigation” and fire the navigation. In case if there are some filters, they can be transferred through “appstate”. Something like below:

 _onNavigationbyCardHeader: function (oEvent) { var oComponent = this.getParent().getParent().getParent().getParent().getComponentData().appComponent; // get the cross app navigation service var oCrossAppNavigator = sap.ushell.Container.getService("CrossApplicationNavigation"); // get the initial app state var oAppState = oCrossAppNavigator.createEmptyAppState(oComponent); // get the filter freom the controller instance var aFilter = this.getParent().getParent().getParent().getController().aFilter; oAppState.setData({ "customFilter": aFilter }); // object of values needed to be restored oAppState.save(); // change the hash var oHashChanger = sap.ui.core.routing.HashChanger.getInstance(); var sOldHash = oHashChanger.getHash(); var sNewHash = sOldHash + "?" + "sap-iapp-state=" + oAppState.getKey(); oHashChanger.replaceHash(sNewHash); // semantic object and action details var sSemanticObject = this.getParent().getParent().getParent().getController().sSemanticObject; var sAction = this.getParent().getParent().getParent().getController().sAction; var hash = (oCrossAppNavigator && oCrossAppNavigator.hrefForExternal({ target: { semanticObject: sSemanticObject, action: sAction }, appStateKey: oAppState.getKey() })); oCrossAppNavigator.toExternal({ target: { shellHash: hash } }); },

For navigation through item, we added the callback function on event “itemPress” (mentioned in the fragment definition of the property contentFrangment). The code is more or less the same as above, but we will also pass the filters to the list report depending on which item is clicked from the table.

 _onNavigationbyItem: function (oEvent) { // get the cross app navigation service var oCrossAppNavigator = sap.ushell.Container.getService("CrossApplicationNavigation"); // get the initial app state var oAppState = oCrossAppNavigator.createEmptyAppState(this.getOwnerComponent()); // Build the Filter data var oObjectData = oEvent.getParameter("listItem").getBindingContext("BlockCounts").getObject(); if (oObjectData) { var aFilter = this.aFilter; aFilter.push(new sap.ui.model.Filter(oObjectData.Field, sap.ui.model.FilterOperator.EQ, "X")); } // set the app state oAppState.setData({ "customFilter": aFilter }); // object of values needed to be restored oAppState.save(); // change the hash var oHashChanger = sap.ui.core.routing.HashChanger.getInstance(); var sOldHash = oHashChanger.getHash(); var sNewHash = sOldHash + "?" + "sap-iapp-state=" + oAppState.getKey(); oHashChanger.replaceHash(sNewHash); var hash = (oCrossAppNavigator && oCrossAppNavigator.hrefForExternal({ target: { semanticObject: this.sSemanticObject, action: this.sAction }, appStateKey: oAppState.getKey() })); oCrossAppNavigator.toExternal({ target: { shellHash: hash } }); },

Reading the filter values in the list report is also easy, you can write code as below in the onInit  method to read the customFilter added in the appstate 

 var oNavigationHandler = new NavigationHandler(this); var oParseNavigationPromise = oNavigationHandler.parseNavigation(); oParseNavigationPromise.done(function (oAppData, oStartupParameters, sNavType) { var oRows = this.oTable.getBinding("rows"); if (oRows) { oRows.filter(oAppData.customFilter); } }.bind(this));

Conclusion:

From this article, you have learned how to integrate the OData service with the custom card. The OVP card can be easily customizable using annotations and those customizations can be easily read and manipulated in the card’s controller. There are no constraints on how to visualize data, I though it would be simple to show a table in the card, at least for me that was the requirement. But I think any other control can be easily integrated with the card, for example a chart of some kind. Nevertheless, I hope this gives a good explanation on how a custom OVP card with OData integration is made. 

I would really like have your feedback and thoughts on this post. Please, do comment. If there are questions, I would be very happy to answer.

You can also find answers and post questions on SAPUI5/Fiori Topic on the following community topic pages:

SAPUI5

field masking for SAPUI5 and SAP Fiori

SAP Fiori

Thank you for reading.