Урок 8. SapUI5: Модели. Взаимодействие интерфейса с моделью. Биндинг.
Продолжим знакомство с SapUI5 и перейдем к одной из важнейших тем — взаимодействие интерфейса (представления) и модели в SapUI5.
Тестовый пример вы всегда можете посмотреть и импортировать к себе в webIDE тут https://github.com/kannade/DataBinding_sapui5.
Модель (Model) предоставляет доступ к данным!
Какие бывают модели в SapUI5?
В SapUI5 может быть использовано 4 типа модели:
- XML модель (https://sapui5.hana.ondemand.com/#/api/sap.ui.model.xml.XMLModel). Клиентская модель (модель полностью содержится в xml файле). Пример XML модели — http://www.cbr.ru/scripts/XML_daily.asp.
- JSON модель (https://sapui5.hana.ondemand.com/#/api/sap.ui.model.json.JSONModel). Клиентская модель (модель полностью содержится в json файле). JSON модель мы использовали в прошлом тестовом примере, подробней в 4 уроке.
- oData модель (https://sapui5.hana.ondemand.com/#/api/sap.ui.model.odata.v2). Серверная модель (чтобы получить доступ к частям модели, необходимо отправлять на сервер запрос).
Open Data Protocol (OData) — это открытый веб-протокол для запроса и обновления данных. Протокол позволяет выполнять операции с ресурсами, используя в качестве запросов HTTP-команды, и получать ответы в форматах XML или JSON.
Пример oData модели — http://services.odata.org/Northwind/Northwind.svc/.
- Resource модель (https://sapui5.hana.ondemand.com/#/api/sap.ui.model.resource.ResourceModel). Resource модель в нашем примере это файл i18n, с которым мы работали на прошлом уроке.
Как забиндить модель в SapUI5?
Выделим 4 способа:
- Установить модель напрямую для элемента:
/DataBinding_sapui5/blob/master/webapp/controller/Main.controller.js 1 2
this.getOwnerComponent().byId("MainViewId").byId("__text4").setModel(oMdl); sap.ui.getCore().byId("__component0---MainViewId--__text4").setModel(oMdl);
- Установить модель для всего представления:
/DataBinding_sapui5/blob/master/webapp/controller/Main.controller.js 1
this.getView().setModel(oMdl);
- Установить модель глобально для всего приложения:
/DataBinding_sapui5/blob/master/webapp/controller/Main.controller.js 1
sap.ui.getCore().setModel(oMdl);
- Установить модель глобально, но с определенным именем:
/DataBinding_sapui5/blob/master/webapp/controller/Main.controller.js 1
sap.ui.getCore().setModel(oMdl, "globalData");
Как получить модель в SapUI5?
- Получим модель, обратившись напрямую к элементу:
/DataBinding_sapui5/blob/master/webapp/controller/Main.controller.js 1 2
var oElem = this.getOwnerComponent().byId("MainViewId").byId("__text4"); var oResData = oElem.getModel().getData();
- Получим модель, обратившись напрямую к представлению:
/DataBinding_sapui5/blob/master/webapp/controller/Main.controller.js 1
var oView = this.getView().getModel().getData();
- Получим модель по имени, обратившись к ядру:
/DataBinding_sapui5/blob/master/webapp/controller/Main.controller.js 1 2
var oMdl3 = sap.ui.getCore().getModel("globalData"); var oResVar3 = oMdl3.getData();
Хорошо, давайте пройдемся по каждой и посмотрим как можно объявить модель и использовать её в своем sapui5 приложении!
В index.html определяем контейнер для приложения:
1 2 3 4 5 6 7 8 9 10 | sap.ui.getCore().attachInit(function() { new sap.m.Shell({ app: new sap.ui.core.ComponentContainer({ async : true, height : "100%", name : "alfa.DataBindingExample", propagateModel : true }) }).placeAt("content"); }); |
propagateModel : true — для корректной работы моделей!
XML модель.
XML модели редко где используются, но бывают разные ситуации, разные сервисы и разные задачи.
Поместим всю модель в файл model/products.xml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | <root> <stores> <store name="Ленинский проспект"> <info> <employees>45</employees> <manager>Кузнецова Ирина Семеновна</manager> </info> <product count="123" price="54.99" weight="23.79">Яблоки</product> <product count="54" price="103.99" weight="15.17">Груши</product> <product count="95" price="65" weight="197">Апельсины</product> <product count="4" price="165" weight="9">Манго</product> <product count="144" price="85" weight="932">Грейпфруты</product> </store> <store name="Академическая"> <info> <employees>25</employees> <manager>Петрова Елена Ивановна</manager> </info> <product count="93" price="45" weight="23.79">Груши</product> <product count="23" price="120" weight="321">Апельсины</product> <product count="900" price="100" weight="153">Яблоки</product> </store> </stores> </root> |
Корневой элемент root, затем идет stores а затем у нас два элемента store со своей внутренней иерархией.
Подключим xml модель:
Процедура подключения моделей более подробна описана в 4 уроке.
1 2 3 4 5 | onInit: function() { var oMdl = new sap.ui.model.xml.XMLModel(); // определяем модель oMdl.loadData("model/products.xml"); //загружаем данные this.getView().setModel(oMdl); //устанавливаем модель для данного представления } |
В представлении выводим XML модель:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <mvc:view displayBlock="true" xmlns:core="sap.ui.core" xmlns:mvc="sap.ui.core.mvc" xmlns="sap.m" xmlns:html="http://www.w3.org/1999/xhtml" controllerName="alfa.DataBindingExample.controller.XmlModelView" xmlns:z="sap.ui.layout"> <panel class="panelStyle" headerText="XML модель" expandable="true" expanded="false"> <content> <z:verticallayout> <text text="Название магазина 2: {/stores/store/1/@name}"></text> <text text="Количество продукта 2: {/stores/store/1/product/1/@count}"></text> </z:verticallayout> <list headerText="Магазины" noDataText="Drop list items here" items="{/stores/store/}"> <standardlistitem type="Navigation" title="{@name}" description="{info/manager}" icon="sap-icon://picture" counter="{ path: 'info/employees/text()', formatter : '.formatter.formatToInt' }"></standardlistitem> </list> </content> </panel> </mvc:view> |
Обратите внимание как мы это делаем!
- Корневой элемент root игнорируется! Это важно помнить!
- /stores/store/1/@name: обращаемся к /stores/store/1 ко ВТОРОМУ (цифра 1, так как нумерация начинается с 0) элементу и выводим name из атрибута: store name=»Академическая» при помощи такого синтаксиса @name.
- items=»{/stores/store/}» — указали массив магазинов, состоящий, в данном конкретном случае, из двух элементов.
- Внутри List, к которому задали атрибут items используем не полный путь для биндинга, а относительный (относительно /stores/store/), например, info/manager — вывели содержимое тега manager, info/employees/text() — вывели содержимое тега employees.
Все довольно просто, посмотрите сами на примере!
Подробней про XML Model — https://openui5.hana.ondemand.com/#/topic/a53e71d85fae4d0887a8b58431197a27.html
JSON модель.
С JSON моделью мы уже сталкивались и использовали ее в предыдущих уроках.
Поместим всю модель в файл model/prod.json:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | { "stores": [{ "name" : "Ленинский проспект", "info": { "employees": 45, "manager": "Кузнецова Ирина Семеновна" }, "products": [{ "name": "Яблоки", "count": "123", "price": 54.99 }, … ] }, { "name" : "Академическая", "info": { "employees": 25, "manager": "Петрова Елена Ивановна" }, "products": [{ "name": "Апельсины", "count": "93", "price": 45 }, … ] }] } |
Подключим json модель:
1 2 3 4 5 6 | onInit: function() { var oMdl = new sap.ui.model.json.JSONModel(); //определяем модель oMdl.loadData("model/prod.json"); //загружаем модель this.getView().setModel(oMdl); //устанавливаем модель this.changeTableBinding(); }, |
В представлении выводим json модель:
1 2 3 4 5 6 | <text text="Магазин 2: {/stores/1/name}"></text> <text text="Количество продукта 2: {/stores/1/products/1/count}"></text> <list mode="SingleSelectMaster" headerText="Магазины" noDataText="Drop list items here" id="__list0" items="{/stores}" selectionChange="onSelList"> <standardlistitem type="Navigation" title="{name}" description="{info/manager}" counter="{info/employees}" icon="sap-icon://picture" id="__item0"></standardlistitem> </list> |
А вот тут нас ждет один очень интересный момент.
Сделаем так, что при нажатии на магазин, мы получали список продуктов в нем:
В List мы определили метод selectionChange=»onSelList», то есть при нажатии на элемент из List (магазин) будет отрабатывать метод onSelList, который мы определяем в контроллере:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | onSelList: function(oEvt) { //нажали на элемент из List this.changeTableBinding(); //вызвали метод changeTableBinding }, changeTableBinding: function() { var oList = this.getView().byId("__list0"); // получили ссылку на List var oItem = oList.getSelectedItem(); // взяли выбранный элемент // взяли oItem.getBindingContext().sPath и определили на какой магазин нажали (смотрите картинку ниже) var i = oItem ? oItem.getBindingContext().sPath.split("/")[2] : 0; var oTbl = this.getView().byId("table0"); //получили ссылку на таблицу var oBindInfo = oTbl.getBindingInfo("items"); //получаем информацию о биндинге таблицы if (!oBindInfo) { return; } oTbl.bindItems({ // биндим новые строки в зависимости от нажатого магазина (i) //Таблица будет забиндина либо на /stores/0/products, либо на /stores/1/products path: "/stores/" + i + "/products", template: oBindInfo.template }); } |
Обратим внимание на представление, таблица биндиться пустым значением items=»{}»:
1 2 | <table id="table0" headerText="Продукты" items="{}"> </table> |
Подробней про JSON модель — https://sapui5.hana.ondemand.com/#/topic/70ef981d350a495b940640801701c409.
oData модель.
oData — самая предпочтительная, сложная и удобная модель!
В данном примере мы используем уже готовый сервис, предоставляющий нам модель Northwind.
Чтобы наш пример работал корректно, нам необходимо сообщить WebIDE где брать данную модель.
Для этого идем в SAP cloud platform cockpit:
В меню слева выбираем Connectivity -> Destinations.
Там ищем кнопку New Destination и создаем новый destination для northwind:
URL — http://services.odata.org/V2/Northwind/Northwind.svc/.
В файле neo-app.json у нас есть запись:
1 2 3 4 5 6 7 8 | { "path": "/Northwind/Northwind.svc/", "target": { "type": "destination", "name": "northwind" }, "description": "northwind service" } |
которая говорит, что если WebIDE нашел путь /Northwind/Northwind.svc/, то он будет отсылать браузер по ссылке, которую мы указали выше в создании destination с именем «northwind» («name»: «northwind»).
И сразу посмотрим как мы определяем oData модель:
1 2 3 4 5 6 | onInit: function() { var oMdl = new sap.ui.model.odata.v2.ODataModel("/Northwind/Northwind.svc/", { defaultCountMode: CountMode.Inline, useBatch: false }); this.getView().setModel(oMdl); |
Не забываем подключать необходимые библиотеки для работы:
1 2 3 4 5 6 7 8 9 10 | sap.ui.define([ "sap/ui/core/mvc/Controller", "./../model/formatter", "sap/m/GroupHeaderListItem", "sap/ui/model/Sorter", "sap/ui/model/Filter", "sap/ui/model/odata/CountMode", "sap/ui/model/FilterOperator"], function(Controller, Formatter, GroupHeaderListItem, Sorter, Filter, CountMode, FilterOperator) { "use strict"; |
Откроем и посмотрим что собой представляет Northwind, добавив мета тег metadata — http://services.odata.org/V2/Northwind/Northwind.svc/$metadata.
EntityType — описание сущности, тип.
Возьмем, например, Employee:
В EntityType содежится описание всех полей, которые содержит сущность, описанная на данном EntityType.
А чуть ниже описали EntitySet с именем Employees:
<EntitySet Name=»Employees» EntityType=»NorthwindModel.Employee»/>
Employees — это то имя, к которому мы будем обращаться, чтобы получить данные и отобразить их.
Сущность Employees (EntitySet) описана на типе Employee (EntityType)!
Обратимся к сущности Employees — http://services.odata.org/V2/Northwind/Northwind.svc/Employees.
В ответ oData сервис вернет XML. Но я хочу получить данные в JSON формате! Поэтому укажу это сервису, добавив мета тег ?$format=json к url:
http://services.odata.org/V2/Northwind/Northwind.svc/Employees?$format=json.
Ура, мы получили ответ в JSON. Теперь я хочу посмотреть заказы (Orders) первого человека из массива:
http://services.odata.org/V2/Northwind/Northwind.svc/Employees(1)/Orders?$format=json.
Или, я хочу получить всех людей, но только отсортировать массив по полю EmployeeID в порядке убывания, $orderby=EmployeeID desc:
http://services.odata.org/V2/Northwind/Northwind.svc/Employees?$format=json&$orderby=EmployeeID%20desc.
А теперь из полученного массива взять только 5 человек, начиная с третьего, $skip=3 $top=5:
http://services.odata.org/V2/Northwind/Northwind.svc/Employees?$format=json&$orderby=EmployeeID%20desc&$skip=3&$top=5.
Хочу посмотреть тех, кто из Лондона, $filter=City eq ‘London’:
http://services.odata.org/V2/Northwind/Northwind.svc/Employees?$format=json&$orderby=EmployeeID%20desc&$filter=City%20eq%20%27London%27.
И выведу только два поля LastName и FirstName, $select=LastName,FirstName
http://services.odata.org/V2/Northwind/Northwind.svc/Employees?$format=json&$orderby=EmployeeID%20desc&$filter=City%20eq%20%27London%27&$select=LastName,FirstName.
На этих примерах понятно, что в нашем распоряжении есть вполне рабочий язык для составления разнообразных выборок данных.
Подробней — http://www.odata.org/documentation/odata-version-2-0/uri-conventions/.
Посмотрите, как можно прочитать oData модель применив к ней определенные фильтры (к работе нашего примера данный кусок кода не имеет никакого отношения, написано только для примера чтения):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | oMdl.read("/Employees", { urlParameters: { "$expand": "Orders", "$select": "EmployeeID,LastName,FirstName,Orders/OrderDate,Orders/ShipName", "$orderby": "EmployeeID desc", "$inlinecount": "allpages", "$skip": "5", "$top": "2" }, filters: [ new sap.ui.model.Filter({ path: "EmployeeID", operator: "GE", value1: "1" }) ], success: function(oEvt) { if (oEvt) { //тут мы должны получить ответ в параметре и забиндить результат на элемент } }, error: function(oEvt) { if (oEvt) { } } }); }, |
В методе success мы должны будем получить ответ (массив results) и забиндить его:
1 2 3 4 5 6 | if (oEvt) { var oJsonModel = new sap.ui.model.json.JSONModel(oEvt); //oResults); //выберем представление <em>idMain</em> -> представление <em>ListingView</em> (находиться в <em>idMain</em>) -> таблицу <em>files</em> и установим для таблицы модель с именем <em>model2</em> sap.ui.getCore().byId("idMain").byId("ListingView").byId("files").setModel(oJsonModel, "model2"); } |
Вернемся к примеру и…
Выведем oData модель:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <table selectionChange="onTblSel" mode="SingleSelectMaster" id="table0" headerText="Сотрудники" items="{ path: '/Employees', parameters: { select : 'Photo, EmployeeID,LastName,FirstName,City,HireDate' }, sorter : { path : 'City', descending: false, group: '.getGroup' }, groupHeaderFactory: '.getGroupHeader' }"> <!--parameters: { select : 'Photo, EmployeeID,LastName,FirstName,City,HireDate' },--> <items> <columnlistitem id="firstItem" press="onTblDetail"> <cells> <image src="{ path: 'Photo', formatter: '.formatter.formatImg' }"></image> <text text="{LastName}"></text> <text text="{FirstName}"></text> <datepicker dateValue="{HireDate}" displayFormat="dd.MM.yyyy" editable="false"></datepicker> </cells> </columnlistitem> </items> </table> |
Посмотрим на вывод данных на странице, все люди группируются по полю City (в биндинге модели мы задали условия группировки):
group: ‘.getGroup’ — условие группировки данных
groupHeaderFactory: ‘.getGroupHeader’ — заголовок:
1 2 3 4 5 6 7 8 9 10 | getGroup: function(oContext) { return oContext.getProperty('City'); }, getGroupHeader: function(oGroup) { return new GroupHeaderListItem({ title: oGroup.key, upperCase: false }); }, |
Сделаем так, чтобы по нажатию на строку в таблице ниже отображалась информация о заказах конкретного человека:
Посмотрите код представления выше, для строк у нас определено событие press=»onTblDetail»:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | onTblSel: function() { this.changeTableBinding(); //Вызываем метод changeTableBinding }, changeTableBinding: function() { var oTbl = this.getView().byId("table0"); //получаем ссылку на таблицу var oItem = oTbl.getSelectedItem(); //получаем ссылку на выбранную строку if (!oItem) { return; } var oBndCnt = oItem.getBindingContext(); // getBindingContext() от выбранной строки if (!oBndCnt) { return; } var oSelObj = oBndCnt.getObject(); // выбранный элемент var oTblDtl = this.getView().byId("tableDtl"); //Получаем ссылку на таблицу, в которую будем выводить var oBindInfo = oTbl.getBindingInfo("items"); if (oBindInfo) { oTblDtl.bindRows({ path: "/Employees(" + oSelObj.EmployeeID + ")/Orders", // oBindInfo.path, template: oBindInfo.template, parameters: { select: "OrderID, ShipPostalCode, ShipCountry, ShipCity, ShipAddress, ShipName, OrderDate, ShippedDate, Freight" } }); } }, |
Красиво, не правда ли? 🙂
Нажмите правой кнопкой мыши на первую колонку:
:
Посмотрите как устанавливаются такие фильтры (https://openui5.hana.ondemand.com/#/api/sap.ui.table.Column):
1 2 3 | <t:column id="orderIdCol" filterProperty="OrderID" sortProperty="OrderID" hAlign="Center" width="5rem"></t:column> … <t:column id="addrCol" filterProperty="ShipAddress" sortProperty="ShipPostalCode, ShipCountry, ShipCity" hAlign="Center" width="20rem"></t:column> |
Resource модель
Определим Resource модель:
1 2 3 4 5 6 7 8 | onInit: function() { // Resource Model i18n var oResourceModel = new sap.ui.model.resource.ResourceModel({ bundleName: "alfa.DataBindingExample.i18n.i18n" }); // Устанавливаем для ядра с именем i18n sap.ui.getCore().setModel(oResourceModel, "i18n"); |
Сама модель:
1 2 3 4 5 6 | // коммент firstName=Имя lastName=Фамилия midName=Отчество errorPernr=При обработке табельных номеров {0} {1} {2} {3} {4} {5} {6} {7} {8} {9} {10} {11} {12} {13} {14} {15} произошла ошибка … |
НО! Путь до модели мы указываем вида bundleName: «alfa.DataBindingExample.i18n.i18n», а файл i18n_ru_RU.properties.
SapUI5 сам понимает какой языковой файл загрузить в зависимости от языка региона, в котором вы находитесь.
Можно задать язык загрузки, добавив параметр sap-ui-language=DE в url.
SapUI5 будет искать языковой файл с уже другим префиксом:
Выведем Resource модель:
1 2 3 4 5 6 7 8 9 | <panel class="panelStyle" headerText="{i18n>panels.TitleRes}" expandable="true" expanded="false"> <content> <l:verticallayout width="100%"> <l:content> <button text="Отобразить сообщение" press="onShowMessage"></button> </l:content> </l:verticallayout> </content> </panel> |
В представлении нарисовали кнопку с событием press=»onShowMessage»:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | onShowMessage: function() { var aP = []; for (var i = 0; i < = 20; i++) { aP.push("0000000" + i); } //Сделали массив вида: [00000001, 00000002, 00000003, 00000004, 00000005…] //Создаем текстовое сообщение. Берем маску errorPernr из модели i18n/i18n_ru_RU.properties и используем ее с массивом var sMsg = sap.ui.getCore().getModel("i18n").getResourceBundle().getText("errorPernr", aP); //Глобальная передача данных. Об этом чуть ниже jQuery.sap.getObject("sap.alfa.dataBinding", 0); if (sap.alfa.dataBinding) { sMsg += " " + sap.alfa.dataBinding.key1; } sap.m.MessageToast.show(sMsg); //выводим сообщение } |
SapUI5 взял сообщение из модели errorPernr, а в качестве параметров {0} {1} {2} {3}… использовал массив aP.
Глобальная передача данных в SapUI5.
В SapUI5 есть один забавный способ передать данные глобально. То есть получить к определенным данным доступ из любого контроллера.
Устанавливаем объект глобально:
1 2 3 4 5 | // Использование глобальных данных jQuery.sap.getObject("sap.alfa.dataBinding", 0); sap.alfa.dataBinding = { key1: "Эта строка из глобального объекта" }; |
Используем глобальный объект:
1 | sMsg += " " + sap.alfa.dataBinding.key1; |
Взаимодействие модели и интерфейса
Существует 3 типа взаимодействия:
- One-Way — при изменении модели, изменения на представлении происходить не будут.
- Two-Way — при изменении модели, изменения на представлении применяются сразу.
- One-Time — модель запрашивается только во время инициализации приложения.
Посмотрим наш пример.
Зададим модель и установим режим для модели OneWay:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | onInit: function() { // Ввод данных One-Way Data Binding vs Two-Way Data Binding var oData = { firstName: "Иван", lastName: "Иванов", midName: "Петрович" }; var oMdl = new sap.ui.model.json.JSONModel(oData); // значение считывается из модели единственный раз // oMdl.setDefaultBindingMode(sap.ui.model.BindingMode.OneTime); // из модели данные передаются на view oMdl.setDefaultBindingMode(sap.ui.model.BindingMode.OneWay); // из модели данные передаются на view и из view обновляется модель // oMdl.setDefaultBindingMode(sap.ui.model.BindingMode.TwoWay); this.getView().setModel(oMdl, "inputData"); }, |
Есть представление, в котором отображаются поля модели:
1 2 3 4 5 6 7 8 9 | <label text="{i18n>firstName}" width="100%"/> <input id="fName" width="100%" value="{inputData/>/firstName}" valueLiveUpdate="true"/> </label><label text="{i18n>lastName}" width="100%"/> <input id="lName" width="100%" value="{inputData/>/lastName}" valueLiveUpdate="true"/> </label><label text="{i18n>midName}" width="100%"/> <input id="mName" width="100%" value="{inputData/>/midName}" valueLiveUpdate="true"/> <text width="100%" text="Введенное ФИО: {inputData>/lastName} {inputData>/firstName} {inputData>/midName}"/> <button text="Обновить значение модели из view" press="onRefreshMdl"></button> </text></label> |
Если мы начнем менять значения в полях вода, то значения в Text меняться не будут. Модель обновиться, только если нажать на кнопку и запустить соответствующий метод для обновления модели:
1 2 3 4 5 6 7 8 9 10 11 | onRefreshMdl: function() { var oMdl = this.getView().getModel("inputData"); var sFName = this.getView().byId("fName").getValue(); var sLName = this.getView().byId("lName").getValue(); var sMName = this.getView().byId("mName").getValue(); oMdl.setData({ firstName: sFName, lastName: sLName, midName: sMName }); } |
Если поменяем oMdl.setDefaultBindingMode(sap.ui.model.BindingMode.OneTime); на oMdl.setDefaultBindingMode(sap.ui.model.BindingMode.TwoWay); и запустим приложение, то при редактирования поля ввода, текст из Text будет сразу меняться.
Такие дела :).