Show TOC

Step 8: Master ViewLocate this document in the navigation structure

Flushed with the success of seeing our SplitApp container on the screen, we'll add the first main view, Master, replacing the empty skeleton we used in the Model View Controller section.

View

Let's examine the view definition bit by bit.

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

We start with the view declaration itself, which also points to a corresponding controller, which we'll introduce shortly. Note that in all the views in this app, we're specifying sap.m as the default namespace (this means that elements that don't have an XML namespace prefix belong to the sap.m library). The displayBlock="true" declaration is to prevent scrollbars appearing.

	<Page
		id="page"
		title="{i18n>masterTitle}">
		<subHeader>
			<Bar id="searchBar">
				<contentMiddle>
					<SearchField
						id="searchField"
						showRefreshButton="{device>/isNoTouch}"
						search="onSearch"
						tooltip="{i18n>masterSearchTooltip}"
						width="100%">
					</SearchField>
				</contentMiddle>
			</Bar>
		</subHeader>

The Master view contains a single top-level control - an sap.m.Page. As a UI unit this makes sense especially being within the context of the SplitApp's masterPages aggregation. The page's title is determined via our Internationalization mechanism, and has a subHeader aggregation, which is a single aggregation that expects an sap.m.Bar. In the middle of the Bar we have an sap.m.SearchField control. The SearchField should appear slightly different depending on whether the device on which the app is running has touch capabilities. There's a boolean value controlling the display of the refresh button, and this is taken from the device model - the named model "device" in the property binding for showRefreshButton. The device model was introduced briefly in the Component section of this part, and is covered in more detail in the Device Model section. We'll examine the handler for the Search Field shortly, when we look at the Master controller.

<content>
			<List
				id="list"
				items="{/Products}"
				mode="{device>/listMode}"
				noDataText="{i18n>masterListNoDataText}"
				select="onSelect"
				growing="true"
				growingScrollToLoad="true">
				<items>
					<ObjectListItem
						type="{device>/listItemType}"
						press="onSelect"
						title="{Name}"
						number="{
							path: 'Price',
							formatter: 'sap.ui.demo.tdg.util.Formatter.currencyValue'
						}"
						numberUnit="USD">
					</ObjectListItem>
				</items>
			</List>
		</content>

The Page's default aggregation is the 'content' aggregation, and contains an sap.m.List control. The List is bound to the "/Products" entity set in our domain model (more on this in the Data Sources section), and uses growing features to provide paging facilities. The List's selection mode is dependent on whether the device is a smartphone or not - again, covered by the device model (see later).

With an aggregation binding, a template is required that is used to present each element of the aggregation - in this case each "Product" entity in the domain model's "/Products" entity set. We declare this in the "items" aggregation declaration - specifying an sap.m.ObjectListItem. Each Product will be presented using a separate ObjectListItem instance in the List.

Like the List's selection mode, the ObjectListItem's behaviour (via the "type" property) is also device-dependent. We use some of the ObjectListItem's basic properties to display information for each Product's name and price properties. Because the Northwind service that is being used for the domain model doesn't have currency information, we're just showing "USD" for this demo.

The Product's price itself needs to be subject to special formatting if it is to look presentable in the app - we would like the values to always appear with two decimal places. For this, we use a formatter "currencyValue" in the Formatter.js file in our app's util folder. See the "Custom Utilities" section for more details on how this formatter works. But here, note how the formatter is used, in a complex property binding for the ObjectListItem's number property: the binding is still introduced with the braces, but is specified as a JavaScript object with a 'path' key pointing to the model property, and a 'formatter' key pointing to the formatter function.

	<footer>
			<Bar>
				<contentRight>
					<Button
						icon="sap-icon://add"
						tooltip="{i18n>masterFooterAddButtonTooltip}"
						press="onAddProduct" />
				</contentRight>
			</Bar>
		</footer>
	</Page>
</mvc:View>

Finally we have an sap.m.Bar in the Page's footer aggregation. In this Bar we have an "add" button, on the right, which, when pressed, is to bring up the AddProduct view for creating a new product. You can see this in the app screenshot in the Introduction section of this part.

Controller

Now we've got the view declaration done, we need to look at the related controller view/Master.controller.js.

jQuery.sap.require("sap.ui.demo.tdg.util.Formatter");

In order to be able to reference and use the currencyValue formatter function (in our util/Formatter.js) for each Product's price property, we need to declare a requirement for the module. We do it at the top of the view's controller file.

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

	onInit : function() {
		this.oUpdateFinishedDeferred = jQuery.Deferred();

		this.getView().byId("list").attachEventOnce("updateFinished", function() {
			this.oUpdateFinishedDeferred.resolve();
		}, this);

In the init event, we create a jQuery Deferred object to be able to know when the update of our List control is finished - in other words, when data has been loaded. We set the Deferred object to resolved in a handler that we attach to the "updateFinished" event which signals that the binding has been updated.

		sap.ui.core.UIComponent.getRouterFor(this).attachRouteMatched(this.onRouteMatched, this);
	

We then set a handler to handle the routing event.

	onRouteMatched : function(oEvent) {

		var oList = this.getView().byId("list");
		var sName = oEvent.getParameter("name");
		var oArguments = oEvent.getParameter("arguments");

In the routing handler, we retrieve and store the event arguments passed, which are the name of the route (the "name" parameter) and the arguments from the placeholders (in the "arguments") parameter.

		// Wait for the list to be loaded once
		jQuery.when(this.oUpdateFinishedDeferred).then(jQuery.proxy(function() {
			var aItems;

			// On the empty hash select the first item
			if (sName === "main") {
				this.selectDetail();
			}

			// Try to select the item in the list
			if (sName === "product") {

				aItems = oList.getItems();
				for (var i = 0; i < aItems.length; i++) {
					if (aItems[i].getBindingContext().getPath() === "/" + oArguments.product) {
						oList.setSelectedItem(aItems[i], true);
						break;
					}
				}	
			}	

		}, this));
	},

It's only when the list has been updated with data do we want to properly handle the route. There are two cases relating to whether a product has been selected via the hash or not. The "main" route will match where there's no hash, and the "product" route will match with a hash containing product (and optional supplier or category tab) information. If there's no product selected, we just select the first item. Otherwise we try to find the selected product in the list and set that to be visibly selected.

	selectDetail : function() {
		if (!sap.ui.Device.system.phone) {
			var oList = this.getView().byId("list");
			var aItems = oList.getItems();
			if (aItems.length && !oList.getSelectedItem()) {
				oList.setSelectedItem(aItems[0], true);
				this.showDetail(aItems[0]);
			}
		}
	},

The selectDetail function checks to see if an item has already been selected (this could have happened based on the route match handling above) and if so, makes sure that the item's details are shown in the Detail view, via the showDetail function.

	onSearch : function() {
		// add filter for search
		var filters = [];
		var searchString = this.getView().byId("searchField").getValue();
		if (searchString && searchString.length > 0) {
			filters = [ new sap.ui.model.Filter("Name", sap.ui.model.FilterOperator.Contains, searchString) ];
		}

		// update list binding
		this.getView().byId("list").getBinding("items").filter(filters);
	},

This function is the handler for the SearchField in the Master view ( <SearchField ... search="onSearch" ... /> ). This is where some of the real power of the ODataModel is to be seen. We retrieve the search text entered, and create a "Contains" filter, in the form of an sap.ui.model.Filter, to search for that text. And by applying this Filter (within an array of possible Filters) to the List's "items" binding, an OData QUERY operation is performed automatically via the ODataModel mechanism. Here's an example of what that request might look like, with a search term of "Lemon":

GET
http://<host>:<port>/uilib-sample/proxy/http/services.odata.org/V2/(S(sapuidemotdg))/OData/OData.svc/Products?$skip=0&$top=1&$filter=substringof(%27Lemon%27,Name)

Note that the $top=1 was included as it the number of entities containing "Lemon" in the name was determined as being 1, according to an also automatic request for a count:

GET 
http://<host>:<port>/uilib-sample/proxy/http/services.odata.org/V2/(S(sapuidemotdg))/OData/OData.svc/Products?$count?$filter=substringof(%27Lemon%27,Name)

This automatic request for a count is based on the fact that this domain model is specified as supporting the OData $count by default. You can turn this off with sap.ui.model.odata.ODataModel.setCountSupported function.

	onSelect : function(oEvent) {
		// Get the list item, either from the listItem parameter or from the event's
		// source itself (will depend on the device-dependent mode).
		this.showDetail(oEvent.getParameter("listItem") || oEvent.getSource());
	},

At some stage, the user will select an item, either via the List itself, or via one of the aggregated ObjectListItems. Which one the user will select, depends on the device model settings. We handle both events (one on the List, the other on the ObjectListItem) with the onSelect function.

We're looking for the control instance that has the appropriate data context. If the event is triggered from the List, then the event parameterlistItem is made available, and that's the context. Otherwise the context is the control itself that's raising the event - i.e. the individual ObjectListItem bound to the specific entity in the Products entity set. In both cases we pass this object to the showDetail function.

showDetail : function(oItem) {
		// If we're on a phone, include nav in history; if not, don't.
		var bReplace = jQuery.device.is.phone ? false : true;
		sap.ui.core.UIComponent.getRouterFor(this).navTo("product", {
			from: "master",
			product: oItem.getBindingContext().getPath().substr(1),
			tab: "supplier"
		}, bReplace);
	},

The showDetail function is called from the onSelect event handler to show the detail of the selected item in this Master view. It does this by using the Router to navigate to the "product" view (this is the name from the routing configuration in the Component).

Note how we pass the information as to which product detail is to be shown: the two parameter names "product" and "tab" from the pattern " {product}/:tab: " in the "product" subroute definition are specified with appropriate values:

  • "product" gets the value of the path of the selected item's binding context, minus the leading slash (e.g. "/Products(7)" -> "Products(7)")
  • "tab" gets the default value "supplier" so that the initial entry in the Detail view's IconTabBar is pre-selected
	onAddProduct : function() {
		sap.ui.core.UIComponent.getRouterFor(this).myNavToWithoutHash({
			currentView : this.getView(),
			targetViewName : "sap.ui.demo.tdg.view.AddProduct",
			targetViewType : "XML",
			transition : "slide"
		});
	}

});

Finally we must handle the "add" button in the Page's footer Bar. In this case we just need to get the Router to put the AddProduct view into place. This uses our custom routing function sap.ui.demo.tdg.MyRouter.myNavToWithoutHash to

  • load the view
  • add it to the appropriate aggregation in the SplitApp (masterPages or detailPages)
  • navigate to it
Progress Check

Now our app folder content now looks like this:

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

We now have something to show that's starting to look like our app!