Technical Articles
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 cube view with multiple data points
File structure on OVP project
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 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:
field masking for SAPUI5 and SAP Fiori
Thank you for reading.