Show TOC

Step 9: Detail ViewLocate this document in the navigation structure

We'll now add the Detail view, replacing the skeleton placeholder we had up until now.

View

Here's what the view declaration looks like.

<mvc:View
	controllerName="sap.ui.demo.tdg.view.Detail"
	xmlns:mvc="sap.ui.core.mvc"
	xmlns:core="sap.ui.core"
	xmlns="sap.m">

Like the Master view we specify that this view has a controller, which we'll get to shortly, and that the default namespace is sap.m. While the "mvc" namespace is for the outermost View element itself, unlike the Master view we also have a "core" namespace (sap.ui.core), which is needed in this view because we're going to pull in a couple of XML fragments using sap.ui.core.Fragment.

<Page
		showNavButton="{device>/isPhone}"
		navButtonPress="onNavBack"
		class="sapUiFioriObjectPage"
		title="{i18n>detailTitle}">
		<content>

Also like the Master view, this view's main control is a Page. This time, however, we may or may not need a back button. Remember that we may be running on a smartphone, or a tablet, or a desktop.

If we're running on a tablet or a desktop, the app will look like what you see in the screenshot, with the master and detail parts of the SplitApp visible at the same time. So the user has no difficulty navigating to another Product in the master list. But on a smartphone, only either the master or the detail part is displayed at any given time, meaning that if the user is viewing a detail page and wants to select another product, they first need to get to the master. This is what the navigation button is for - to go back. And we only want it to be shown if the device is a smartphone, so we use the Device Model and a boolean flag to set on the showNavButton property of the Page.

A Page control's default aggregation is 'content'; while we therefore don't need to specify the <content> tag here, we should do as it's good practice.

			<ObjectHeader
				title="{Name}"
				number="{
					path: 'Price',
					formatter: 'sap.ui.demo.tdg.util.Formatter.currencyValue'
				}"
				numberUnit="USD">
				<attributes>
					<ObjectAttribute text="{i18n>detailFromDate} {
						path: 'ReleaseDate',
						type: 'sap.ui.model.type.Date'
						}" />
					<ObjectAttribute text="{Description}" />
				</attributes>
				<firstStatus>
					<ObjectStatus
						text="{
							path: 'DiscontinuedDate',
							formatter: 'sap.ui.demo.tdg.util.Formatter.discontinuedStatusValue'
						}"
						state="{
							path: 'DiscontinuedDate',
							formatter: 'sap.ui.demo.tdg.util.Formatter.discontinuedStatusState'
						}" />
				</firstStatus>
			</ObjectHeader>

The detail consists of an sap.m.ObjectHeader control at the top. This has a couple of aggregations that are being used - attributes and firstStatus. For all of the properties in the controls that are aggregated (such as sap.m.ObjectAttribute and sap.m.ObjectStatus), as well as some of the basic properties (such as 'number') we use the extended binding syntax again to enable us to use formatter and type specifications.

While the formatter functions were introduced in the Custom Utilities section, it's worth highlighting here that formatter functions are not just for returning formatted versions of the values that are passed to them. They can be used, as here in the state property of the ObjectStatus control, to return any value, in this case one of the enumerated states in sap.ui.core.ValueState, to highlight a discontinued product status with a semantic color - red, via the Error state.

			<IconTabBar
				select="onDetailSelect"
				id="idIconTabBar">
				<items>
					<IconTabFilter
						key="supplier"
						text="{i18n>iconTabFilterSupplier}"
						icon="sap-icon://supplier">
						<content>
							<!--core:Fragment fragmentName="sap.ui.demo.tdg.view.SupplierAddressForm" type="XML" /-->
						</content>
					</IconTabFilter>
					<IconTabFilter
						key="category"
						text="{i18n>iconTabFilterCategory}"
						icon="sap-icon://hint">
						<content>
							<!--core:Fragment fragmentName="sap.ui.demo.tdg.view.CategoryInfoForm" type="XML" /-->
						</content>
					</IconTabFilter>
				</items>
			</IconTabBar>

The other main part of the detail view's content is an sap.m.IconTabBar, to display information about the selected Product entity's Supplier and Category information.

The two sap.m.IconTabFilter controls that are contained in the IconTabBar reference XML fragments where the bulk of the information layout is declared. We'll come to the content of these XML fragments later. For now, the references ("core:Fragment...") are temporarily commented out, because if we include them now without adding the actual XML fragment files, we'll get an error and our progress will take a few steps backwards. Note the key properties of each IconTabFilter - we'll be using these to control the navigation to the appropriate information display and data binding in the RouteMatched handler in the controller.

</content>
		<footer>
			<Bar>
			</Bar>
		</footer>
	</Page>
</mvc:View>

To finish off the Page, we have an sap.m.Bar in the footer aggregation, so that the detail view matches the master visually.

Controller

The bulk of the detail view's controller is for handling the navigation events that are initiated through the Router.

sap.ui.core.mvc.Controller.extend("sap.ui.demo.tdg.view.Detail", {

	onInit : function() {
		var oView = this.getView();

		sap.ui.core.UIComponent.getRouterFor(this).attachRouteMatched(function(oEvent) {
			// when detail navigation occurs, update the binding context
			if (oEvent.getParameter("name") === "product") {

				var sProductPath = "/" + oEvent.getParameter("arguments").product;

				oView.bindElement(sProductPath);

				// Check that the product specified actually was found
				oView.getElementBinding().attachEventOnce("dataReceived", jQuery.proxy(function() {
					var oData = oView.getModel().getData(sProductPath);
					if (!oData) {
						sap.ui.core.UIComponent.getRouterFor(this).myNavToWithoutHash({
							currentView : oView,
							targetViewName : "sap.ui.demo.tdg.view.NotFound",
							targetViewType : "XML"
						});
					}
				}, this));


				// Make sure the master is here
				var oIconTabBar = oView.byId("idIconTabBar");
				oIconTabBar.getItems().forEach(function(oItem) {
					oItem.bindElement(sap.ui.demo.tdg.util.Formatter.uppercaseFirstChar(oItem.getKey()));
				});

				// Which tab?
				var sTabKey = oEvent.getParameter("arguments").tab || "supplier";
				if (oIconTabBar.getSelectedKey() !== sTabKey) {
					oIconTabBar.setSelectedKey(sTabKey);
				}
			}
		}, this);

	},

This RouteMatched event handler has been covered already in the Navigation and Routing section, but it's worth revisiting and looking at a couple of details.

We determine the path of the chosen Product (in sProductPath) and perform an element binding to this on the detail view. Further, we bind each IconTabFilter (the items in the IconTabBar) to the entity that directly relates to the key of that IconTabFilter, i.e. Supplier or Category.

Note that we also attach a handler to the dataReceived event to check that data was actually received for this binding - and if not we ask the Router to navigate to the NotFound view.

We're not making an explicit call to navigate from the selected Product to the associated Supplier or Category, the Supplier and Category details are not contained within the Product entity, and we haven't tried to use the OData $expand in the original Product entity set binding to pull each associated entity in with the original call (that would be retrieving too much data perhaps unnecessarily). So what's happening here?

What's happening is that the sap.ui.model.odata.ODataModel mechanism is handling the navigation from entity to entity for us automatically, following the navigation properties that are described in the OData service metadata thus:

<EntityType Name="Product">
  <Key>
    <PropertyRef Name="ProductID" />
  </Key>
  <Property Name="ProductID" ... />
  ...
  <NavigationProperty Name="Category" ... />
  <NavigationProperty Name="Supplier" ... />
</EntityType>

The automatic navigation also causes the appropriate OData operations to be invoked implicitly. You can see this in the browser's developer console in the Network tab: two GET requests are made:

GET 
http://localhost:8080/uilib-sample/proxy/http/services.odata.org/V2/(S(sapuidemotdg))/OData/OData.svc/Products(2)/Supplier
GET 
http://localhost:8080/uilib-sample/proxy/http/services.odata.org/V2/(S(sapuidemotdg))/OData/OData.svc/Products(2)/Supplier

So - you don't need to "drive" the ODataModel to navigate your service; embrace it and let it do the hard work for you!

onNavBack : function() {
		// This is only relevant when running on phone devices
		sap.ui.core.UIComponent.getRouterFor(this).myNavBack("main");
	},

	onDetailSelect : function(oEvent) {
		sap.ui.core.UIComponent.getRouterFor(this).navTo("product",{
			product : oEvent.getSource().getBindingContext().getPath().slice(1),
			tab: oEvent.getParameter("selectedKey")
		}, true);

	}

});

There are a couple of events that will be raised in the UI, and they need to be handled.

The first is when the Page's navigation button (if shown) is pressed. In this case, the onNavBack function is invoked, and this uses the Router to navigate back to the Master view via the "main" route (defined earlier - see the Component section).There's also an event that's raised when an sap.m.IconTabBar item is selected. In this case we just use the Router again to move us to the appropriate sap.m.IconTabFilter; in calling the Router's navTo function we pass the name of the Product (from the binding context path) and the key of the IconTabFilter that was selected.

Progress Check

With our skeleton Detail.view.xml file replaced, and a corresponding controller added, our app folder content now looks like this:

tdg/
  |
  +-- i18n/
  |     |
  |     +-- messageBundle.properties
  |
  +-- util/
  |     |
  |     +-- Formatter.js
  |
  +-- view/
  |     |
  |     +-- App.view.xml
  |     +-- Detail.controller.js
  |     +-- Detail.view.xml
  |     +-- Master.controller.js
  |     +-- Master.view.xml
  |
  +-- Component.js
  +-- index.html
  +-- MyRouter.html

When a Product is selected, this is how it appears:

The IconTabFilters for the Supplier and Category information are empty, but that's just because we haven't got round to including the XML fragments that describe what goes in there. Let's get to that next.