抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

之前学习Fiori时根据官方的帮助文档实现的一个小练习,上传上来以供后续学习参考使用。

Walkthrough 练习笔记

一、应用程序的容器

1
<Shell></Shell>

该标签可以用作应用程序的一个容器,通过在桌面屏幕上引入一个所谓letterbox。Shell负责根据设备屏幕大小对应用程序进行视觉调整。

1
<content></content>

该标签也是一个容器标签、

二、Fiori标准类

以下记录的是可以直接使用的标准类,主要是用于设置各组件之间布局的标准类。SAP UI5是一个重量级的框架,也就是说它定制了很多CSS的样式以及自定义的一些组件,开发时不需要我们额外编写这些CSS属性,为开发节省了很多时间。这里就记录一下系统CSS的使用。预设CSS主要是针对于不同尺寸的Margin和Padding的一个设定。

1.margin

margin常用的有以下四个属性:

  • Full Margin:控件四周的距离
  • Single-sided:上下左右单一方向距离
  • Two-sided:同时两个方向的距离值
  • Response Margin:响应式Margin
BOX布局
(1)Full Margins
  • sapUiTinyMargin:8px
  • sapUiSmallMargin:16px
  • sapUiMediumMargin:32px
  • sapUiLargeMarin:48px
(2)Single-Sided Margins
8px 16px 32px 48px
sapUiTinyMarginTop sapUiSmallMarginTop sapUiMediumMarginTop sapUiLargeMarinTop
sapUiTinyMarginBottom sapUiSmallMarginBottom sapUiMediumMarginBottom sapUiLargeMarinBottom
sapUiTinyMarginBegin sapUiSmallMarginBegin sapUiMediumMarginBegin sapUiLargeMarinBegin
sapUiTinyMarginEnd sapUiSmallMarginEnd sapUiMediumMarginEnd sapUiLargeMarinEnd
(3)Two-Sided Margins
8px 16px 32px 48px
sapUiTinyMarginBeginEnd sapUiSmallMarginBeginEnd sapUiMediumMarginBeginEnd sapUiLargeMarginBeginEnd
sapUiTinyMarginBeginBottom sapUiSmallMarginBeginBottom sapUiMediumMarginBeginBottom sapUiLargeMarginBeginBottom
(4)Responsive Margins

类名:

sapUiNoMarginTop;

sapUiNoMarginBottom;

sapUiNoMarginBegin;

sapUiNoMarginEnd;

sapUiResponsiveMargin针对不同尺寸进行调整,不包括sap.m.SplitApp。

  • 小于600px时,这时在下边距有一个16像素的边距,上左右的margin都为0
边距图片1
  • 600~1023px时,这时在上下左右边距有一个16像素的边距
边距图片2
  • 大于1023px时,这时在上下左右边距有一个32像素的边距
边距图片3
(5)注意事项

使用预设CSS标准类时不要将width设置成100%,而是要设置成auto。若设置为100%则会超出页面边距。

可以根据设备的屏幕大小进行调整,边距也是会对应减小的。这个设置是响应式的。

页边距可以添加到各种控件中,有许多不同的选项。我们甚至可以通过向按钮添加类sapuismallmargined,在按钮和输入字段之间添加空间。

2.Padding
  • sapUiNoContentPadding:移除padding
  • sapUiContentPadding:16px(1 rem)
  • sapUiResponsiveContentPadding: (no padding,16px,32*16px)
1
<Page class=”sapUiResponsivePadding”>

[https://blog.csdn.net/carawangc/article/details/78790101]: “参考资料”

三、Fiori的CSS

1.使用场景

有时我们需要定义一些更精细的布局,这时我们可以利用CSS的灵活性,向控件添加自定义样式类,并根据自己的喜好设置样式。

2.注意事项

正如兼容性规则中所述,SAPUI5生成的HTML和CSS不是公共API的一部分,可能会在补丁和小版本中更改。如果决定覆盖样式,则有义务在每次更新SAPUI5时测试和更新修改。这样做的前提是,您可以控制正在使用的SAPUI5版本,例如在独立场景中。在SAP Fiori launchpad中运行应用程序时,这是不可能的,因为SAP Fiori launchpad集中加载所有应用程序的SAPUI5。因此,SAP Fiori launchpad应用程序不应覆盖样式。

即使用自定义样式时不要随意覆盖样式,如果覆盖了样式则需要在每次更新UI5时对其进行测试和调试。

创建与自定义名称空间类相结合的自定义类。这样可以确保这些样式只应用于我们应用程序中使用的控件。

对于从右向左(rtl)语言,如阿拉伯语,当应用程序显示反转时,可以设置左边距并重置右边距。如果你只使用标准的SAPUI5控件,你不需要关心这一点,在我们使用自定义CSS的情况下,必须添加这些信息。

1
2
3
4
5
6
7
8
9
10
11
12
html[dir="ltr"] .myAppDemoWT .myCustomButton.sapMBtn {
margin-right: 0.125rem
}

html[dir="rtl"] .myAppDemoWT .myCustomButton.sapMBtn {
margin-left: 0.125rem
}

.myAppDemoWT .myCustomText {
display: inline-block;
font-weight: bold;
}

sapThemeHighlight-asColor:自适应所选主题的颜色设置

四、嵌套视图

1.使用场景

我们的面板内容变得越来越复杂,现在是时候将面板内容移动到单独的视图中了。通过这种方法,应用程序结构更容易理解,应用程序的各个部分也可以重用。

2.嵌套步骤
(1)在主视图中嵌入如下的内容

webapp/view/View1.view.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<mvc:View controllerName="TestCaseTwo.controller.View1" 
xmlns:html="http://www.w3.org/1999/xhtml"
xmlns:mvc="sap.ui.core.mvc" "<!--关键内容1-->"
displayBlock="true" xmlns="sap.m">
<Shell>
<App class="myAppDemoWT">
<pages>
<Page title="{i18n>title}">
<content>
<!--关键内容2-->
<mvc:XMLView viewName="TestCaseTwo.view.HelloPanel"/>
<!--关键内容2-->
</content>
</Page>
</pages>
</App>
</Shell>
</mvc:View>

关键内容1:可以理解为导入Fiori的工具包并起别名“mvc”。

关键内容2:此处就是嵌入视图的地方。

  • TestCaseTwo:项目的名称,与最上面的controllerName相同;
  • view:视图文件夹的名称;
  • HelloPanel:嵌入的视图名称。

三者联系起来的意思是指在TestCaseTwo这个项目下的view文件夹中的名字叫HelloPanel的视图。

(2)创建嵌套视图

在webapp/view下新建一个名字叫HelloPanel的视图

webapp/view/HelloPanel.view.xml

1
2
3
4
5
6
7
8
9
10
11
12
<mvc:View 
controllerName="TestCaseTwo.controller.HelloPanel"
xmlns:mvc="sap.ui.core.mvc"
displayBlock="true" xmlns="sap.m">
<Panel headerText="{i18n>helloPanelTitle}" class="sapUiResponsiveMargin" width="auto">
<content>
<Button text="{i18n>showHelloButtonText}" press="onWhowHello" class="myCustomButton"/>
<Input value="{/recipient/name}" valueLiveUpdate="true" width="60%"/>
<FormattedText htmlText="Hello {/recipient/name}" class="sapUiSmallMargin sapThemeHighlight-asColor myCustomText"/>
</content>
</Panel>
</mvc:View>
(3)创建嵌套视图控制器文件

在webapp/controller的文件下新建嵌套视图的js文件

webapp/controller/HelloPanel.controller.js

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
31
32
33
34
sap.ui.define([
"sap/ui/core/mvc/Controller",
"sap/m/MessageToast",
"sap/ui/model/json/JSONModel",
"sap/ui/model/resource/ResourceModel"
], function(Controller, MessageToast, JSONModel, ResourceModel) {
"use strict";

return Controller.extend("TestCaseTwo.controller.HelloPanel", {
onInit : function() {
var oData = {
recipient : {
name : "World"
}
};
var oModel = new JSONModel(oData);
this.getView().setModel(oModel);

// set i18n model on view
var i18nModel = new ResourceModel({
bundleName:"TestCaseTwo.i18n.i18n"
});
this.getView().setModel(i18nModel,"i18n");
},
onWhowHello : function() {
//read msg from i18n model
var oBundle = this.getView().getModel("i18n").getResourceBundle();
var sRecipient = this.getView().getModel().getProperty("/recipient/name");
var sMsg = oBundle.getText("helloMsg",[sRecipient]);
//show message
MessageToast.show(sMsg);
}
});
});
(4)主视图控制器文件

webapp/controller/View1.controller.js

1
2
3
4
5
6
7
8
sap.ui.define([
"sap/ui/core/mvc/Controller"
], function(Controller) {
"use strict";

return Controller.extend("TestCaseTwo.controller.View1", {
});
});

webapp/i18n/i18n.properties

1
2
3
4
5
6
title=SAPUI5 Walkthrough
appTitle = App Title
appDescription=App Description
showHelloButtonText=Say Hello
helloMsg=Hello {0}
helloPanelTitle=Hello World
(5)效果图展示
知识点四效果展示图

五、片段(Dialogs and Fragments )

片段是轻量级的UI部件(UI子树),可以重用,但没有任何控制器。这意味着,无论何时,当您想要定义UI的某个部分在多个视图中可重用,或者当您想要在特定情况下(不同的用户角色、编辑模式与只读模式)交换视图的某些部分时,片段都是一个很好的候选对象,尤其是在不需要额外控制器逻辑的情况下。

一个片段可以由1到n个控件组成。在运行时,放置在视图中的片段的行为类似于“正常”视图内容,这意味着片段中的控件在渲染时只会包含在视图的DOM中。当然,有些控件不是为成为视图的一部分而设计的,例如对话框。

但即使对于这些控件,片段也可能特别有用,稍后您将看到这一点。

现在,我们将在应用程序中添加一个对话框。对话框是特殊的,因为它们是在常规应用程序内容之上打开的,因此不属于特定的视图。这意味着对话框必须在控制器代码中的某个地方实例化,但由于我们希望坚持声明性方法,并创建尽可能灵活的可重用构件,而且由于对话框不能指定为视图,因此我们将创建一个包含该对话框的XML片段。毕竟,一个对话框可以在应用程序的多个视图中使用。

若使用ABAP的知识进行理解,可类比数据库和Include。有5个数据库都使用了相同的维护结构,那么这些维护结构可以使用一个Include来代替。此时片段就相当于是5个数据库的共用结构Include,而数据库则可以理解成是视图。

1.创建片段步骤
(1)添加触发片段的按钮

webapp/view/HelloPanel.view.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
25
26
27
28
<mvc:View 
controllerName="TestCaseTwo.controller.HelloPanel"
xmlns:mvc="sap.ui.core.mvc"
displayBlock="true"
xmlns="sap.m">
<Panel headerText="{i18n>helloPanelTitle}" class="sapUiResponsiveMargin" width="auto">
<content>
<!--新增内容-->
<Button
id="helloDialogButton"
text="{i18n>openDialogButtonText}"
press="onOpenDialog"
class="sapUiSmallMarginEnd"/>
<!--新增内容-->
<Button
text="{i18n>showHelloButtonText}"
press="onWhowHello"
class="myCustomButton"/>
<Input
value="{/recipient/name}"
valueLiveUpdate="true"
width="60%"/>
<FormattedText
htmlText="Hello {/recipient/name}"
class="sapUiSmallMargin sapThemeHighlight-asColor myCustomText"/>
</content>
</Panel>
</mvc:View>

我们在视图中添加一个新按钮以打开对话框。它只是在面板内容视图的控制器中调用事件处理程序函数。

将helloWorldButton这样的唯一ID设置为应用程序的关键控件是一种很好的做法,这样可以轻松识别。如果未指定属性’id’,OpenUI5运行时将为控件生成唯一但不断变化的id,如u button23。在浏览器中检查应用程序的DOM元素以查看差异。

(2)创建片段代码

webapp/view/HelloDialog.Fragment.xml

1
2
3
4
5
6
7
8
9
<core:FragmentDefinition
xmlns="sap.m"
xmlns:core="sap.ui.core">

<Dialog
id="helloDialog"
title="Hello {/recipient/name}"/>

</core:FragmentDefinition>

我们添加一个新的XML文件,以声明方式在片段中定义我们的对话框。片段资产位于核心名称空间中,因此我们在FragmentDefinition标记中为其添加了一个xml名称空间。

语法类似于视图,但由于片段没有控制器,因此缺少该属性。此外,片段在应用程序的DOM树中没有任何足迹,并且片段本身没有控件实例(只有包含的控件)。它只是一组重用控件的容器。

(3)编写触发片段的按钮逻辑

webapp/controller/HelloPanel.controller.js

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
sap.ui.define([
"sap/ui/core/mvc/Controller",
"sap/m/MessageToast",
"sap/ui/model/json/JSONModel",
"sap/ui/model/resource/ResourceModel",
"sap/ui/core/Fragment" //新增内容
], function(Controller, MessageToast, JSONModel, ResourceModel,Fragment) {
"use strict";

return Controller.extend("TestCaseTwo.controller.HelloPanel", {
onInit : function() {
var oData = {
recipient : {
name : "World",
text : "ok"
}
};
var oModel = new JSONModel(oData);
this.getView().setModel(oModel);

// set i18n model on view
var i18nModel = new ResourceModel({
bundleName:"TestCaseTwo.i18n.i18n"
});
this.getView().setModel(i18nModel,"i18n");
},
onWhowHello : function() {
//read msg from i18n model
var oBundle = this.getView().getModel("i18n").getResourceBundle();
var sRecipient = this.getView().getModel().getProperty("/recipient/name");
var sMsg = oBundle.getText("helloMsg",[sRecipient]);
//show message
MessageToast.show(sMsg);
},
//新增内容
onOpenDialog : function(){
// create dialog lazily
var oView = this.getView();
var oModel = this.getView().getModel();
if (!this.pDialog) {
this.pDialog = Fragment.load({
id:oView.getId(),
name: "TestCaseTwo.view.HelloDialog",
controller:this
});
}
this.pDialog.then(function(oDialog) {
oDialog.setModel(oModel);
oDialog.open();
});
}
//新增内容
});
});

此处如果参照样例去编写会有两个问题:

  • 读取Fragment对象时,样例使用的是this.loadFragment的方式,但是该方式使用的方法是在1.93版本的里的方法,低于该版本的Fiori无法使用该方法。所以使用相对较低的方式如代码所示;
  • 该问题可能的原因还是版本问题,我们在实例化Dialog对象时,需要将数据模型一起传递过去,但是在样例中却没有类似的操作,所以需要我们手动补齐。
2.创建片段回调步骤
(1)创建响应关闭弹窗的按钮

webapp/view/HelloDialog.fragment.xml

1
2
3
4
5
6
7
8
9
10
11
12
<core:FragmentDefinition xmlns="sap.m" xmlns:core="sap.ui.core">
<Dialog id="helloDialog" title="Hello {/recipient/name}">
<!--新增内容-->
<beginButton>
<Button
text="{/recipient/text}"
press=".onCloseDialog"
class="sapThemeHighlight-asColor"/>
</beginButton>
</Dialog>
<!--新增内容-->
</core:FragmentDefinition>

新增一个按钮标签,添加点击触发的逻辑方法onCloseDialog(),设置文本来自传递过来的数据模型。Class中代表对应版本的字体颜色。

标签<beginButton>是一个聚合。在这两个聚合中放置按钮可以确保在UI上,beginButton放在endButton之前。然而,before的意思取决于当前语言的文本方向。因此,我们使用begin和end作为“left”和“right”的同义词。在具有从左到右方向的语言中,beginButton将呈现为left,endButton位于对话框页脚右侧;在特定语言的从右到左模式中,顺序将被切换。

(2)设置关闭方法

webapp/controller/HelloPanel.controller.js

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
sap.ui.define([
"sap/ui/core/mvc/Controller",
"sap/m/MessageToast",
"sap/ui/model/json/JSONModel",
"sap/ui/model/resource/ResourceModel",
"sap/ui/core/Fragment"
], function(Controller, MessageToast, JSONModel, ResourceModel,Fragment) {
"use strict";

return Controller.extend("TestCaseTwo.controller.HelloPanel", {
onInit : function() {
var oData = {
recipient : {
name : "World",
text : "ok" //新增内容
}
};
var oModel = new JSONModel(oData);
this.getView().setModel(oModel);

// set i18n model on view
...
},
onWhowHello : function() {
...
},
onOpenDialog : function(){
// create dialog lazily
var oView = this.getView();
var oModel = this.getView().getModel();
if (!this.pDialog) {
this.pDialog = Fragment.load({
id:oView.getId(),
name: "TestCaseTwo.view.HelloDialog",
controller:this //新增内容
});
}
this.pDialog.then(function(oDialog) {
oDialog.setModel(oModel);
oDialog.open();
});
},
//新增内容
onCloseDialog : function(){
// note: We don't need to chain to the pDialog promise, since this event-handler
// is only called from within the loaded dialog itself.
this.byId("helloDialog").close();
}
//新增内容
});
});

如前所述,片段是纯UI重用工件,没有控制器。但是,可以将控制器对象传递给片段。加载API。对于我们的对话框,我们参考HelloPanel控制器。然而,第三个参数不一定是控制器,但可以是任何对象。只是别忘了这个关键词。

事件处理函数被放入同一个控制器文件中,它通过访问返回对话框的内部帮助函数来关闭对话框。

(3)展示效果图
知识点五展示效果图
3.通过组件重用片段
(1)实例化对话框组件 添加进组件的声明周期

webapp/Component.js

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
sap.ui.define([
"sap/ui/core/UIComponent",
"sap/ui/Device",
"TestCaseTwo/model/models",
"sap/ui/model/json/JSONModel",
"sap/ui/model/resource/ResourceModel",
"./controller/HelloDialog"
], function(UIComponent, Device, models,JSONModel,ResourceModel,HelloDialog) {
"use strict";

return UIComponent.extend("TestCaseTwo.Component", {

metadata: {
manifest: "json"
},

/**
* The component is initialized by UI5 automatically during the startup of the app and calls the init method once.
* @public
* @override
*/
init: function() {
// call the base component's init function
UIComponent.prototype.init.apply(this, arguments);

// set the device model
this.setModel(models.createDeviceModel(), "device");

var oData = {
recipient : {
name : "World",
text : "ok"
}
};
var oModel = new JSONModel(oData);
this.getView().setModel(oModel);

// set i18n model on view
var i18nModel = new ResourceModel({
bundleName:"TestCaseTwo.i18n.i18n"
});
this.getView().setModel(i18nModel,"i18n");

// set dialog
this._helloDialog = new HelloDialog(this.getRootControl());
//
},
exit : function(){
this._helloDialog.destroy();
delete this._helloDialog;
},

openHelloDialog : function(){
this._helloDialog.open();
}
});
});

此处的this指代的UIComponent(UI组件),在初始化时声明了一个UI的属性 _helloDialog 。该属性是一个对象,由HelloDialog构造方法创建的对话框对象。因为我们要将重用对话框连接到应用程序根视图的生命周期,因此我们将根视图的一个实例传递给构造函数。可以通过调用组件的getRootControl方法来检索它。

为了能够从其他控制器打开对话框,我们实现了一个重用函数openHelloDialog,它调用我们的助手对象的open方法。通过这样做,我们还将重用对话框的实现细节与应用程序编码分离。

到目前为止,我们向组件添加了新属性_helloDialog,并为其分配了helloDialog对象的一个实例。我们希望确保当组件被销毁时,为这个helper对象分配的内存被释放。否则,我们的应用程序可能会导致内存泄漏。

为此,我们使用出口挂钩。SAPUI5框架在销毁组件时调用分配给退出的函数。我们调用HelloDialog的destroy函数来清理helper类并结束其生命周期。尽管如此,实例本身仍将存在于浏览器内存中。因此,我们通过调用delete this来删除对HelloDialog实例的引用_helloDialog和浏览器的垃圾收集可以清理其内存。

简言之,我们将对话框的实例化放到了UI组件的声明周期中,在Fiori开始时就将对话框组件实例化,当Fiori生命周期结束时,对话框组件也同样被释放掉。

  • 将重用的对话框组件实例化到UI组件的声明周期中;
  • 将对话框组件绑定为UIComponent的一个属性;
  • 重用了一个调用对话框组件的函数。
(2)对话框组件的实例化代码

注意该文件的命名不能加controller 命名似乎有限制,添加后系统会报错找不到对应的文件

webapp/controller/HelloDialog.js (New)

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
31
32
33
34
35
36
37
38
39
40
41
42
sap.ui.define([
"sap/ui/base/ManagedObject",
"sap/ui/core/Fragment"
], function (ManagedObject,Fragment ) {
"use strict";

return ManagedObject.extend("TestCaseTwo.controller.HelloDialog",{
constructor : function(oView){
this._oView = oView;
},

exit : function() {
delete this._oView;
},

open : function() {
var oView = this._oView;

// 创建对话框
if (!this.pDialog){
var oFragmentController = {
onCloseDialog : function() {
oView.byId("helloDialog").close();
}
};
// 加载异步XML片段
this.pDialog = Fragment.load({
id : oView.getId(),
name : "TestCaseTwo.view.HelloDialog",
controller : oFragmentController
}).then(function (oDialog) {
// 将对话框连接到此组件的根视图(模型、生命周期)
oView.addDependent(oDialog);
return oDialog;
});
}
this.oDialog.then(function(oDialog) {
oDialog.open();
});
}
});
});

open:open方法是打开对话框的方法,先判断对话框是否实例化。若没有实例化,则先关闭对话框,在实例化对话框组件并添加进视图的声明周期。

HelloDialog重用对象的实现扩展了sap.ui.base.ManagedObject继承SAPUI5的一些核心功能。

我们的open方法是从HelloPanel控制器重构而来的,并像前面的步骤一样实例化我们的对话框片段。

我们不会将控制器作为第三个参数传递给函数片段。Fragment.load的本地助手对象,该对象包含片段所需的事件处理程序函数onCloseDialog。
open方法现在包含我们的对话框实例化。第一次调用open方法时,对话框被实例化。此方法的oView参数用于将当前视图连接到对话框。稍后我们将在控制器中调用该对象的open方法。

onCloseDialog事件处理程序只需从HelloPanel控制器移动到重用对象。

我们还添加了一个退出函数,就像我们在组件中所做的那样,当对象被销毁时会自动调用该函数。为了释放helper对象中所有分配的内存,我们删除了保存视图引用的属性。视图本身将被组件破坏,所以我们不需要注意这一点。

(3)修改触发弹出框的逻辑

webapp/controller/HelloPanel.controller.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
sap.ui.define([
"sap/ui/core/mvc/Controller",
"sap/m/MessageToast",
"sap/ui/model/json/JSONModel",
"sap/ui/model/resource/ResourceModel",
"sap/ui/core/Fragment"
], function(Controller, MessageToast, JSONModel, ResourceModel,Fragment) {
"use strict";

return Controller.extend("TestCaseTwo.controller.HelloPanel", {
onWhowHello : function() {
//read msg from i18n model
var oBundle = this.getView().getModel("i18n").getResourceBundle();
var sRecipient = this.getView().getModel().getProperty("/recipient/name");
var sMsg = oBundle.getText("helloMsg",[sRecipient]);
//show message
MessageToast.show(sMsg);
},
onOpenDialog : function(){
this.getOwnerComponent().openHelloDialog();
}
});
});

OnPendialog方法现在通过调用helper方法getOwnerComponent来访问其组件。当调用重用对象的open方法时,我们在当前视图中传递,以将其连接到对话框。

(4)创建另一个触发对话框的事件源

webapp/view /View1.view.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<mvc:View controllerName="TestCaseTwo.controller.View1" xmlns:html="http://www.w3.org/1999/xhtml" xmlns:mvc="sap.ui.core.mvc"
displayBlock="true" xmlns="sap.m">
<Shell>
<App class="myAppDemoWT">
<pages>
<Page title="{i18n>title}">
<headerContent>
<Button
icon="sap-icon://hello-world"
press=".onOpenDialog"/>
</headerContent>
<content>
<mvc:XMLView viewName="TestCaseTwo.view.HelloPanel"/>
</content>
</Page>
</pages>
</App>
</Shell>
</mvc:View>

我们在应用程序视图的标题区域添加了一个按钮,以显示hello world对话框的重用。按下按钮时,对话框将与我们之前在面板中创建的按钮一样打开。

(5)编写主视图对应的打开对话框方法

webapp/controller/View1.controller.js

1
2
3
4
5
6
7
8
9
10
11
sap.ui.define([
"sap/ui/core/mvc/Controller"
], function(Controller) {
"use strict";

return Controller.extend("TestCaseTwo.controller.View1", {
onOpenDialog : function () {
this.getOwnerComponent().openHelloDialog();
}
});
});

我们还将onOpenDialog方法添加到应用程序控制器中,这样对话框将打开并引用当前视图。

(6)展示效果
组件重用效果展示

六、图标

SAP Fiori的图标库有500多个图标可供选择。本次在按钮和弹出框的中心设置图标用于展示。

(1)给按钮添加图标属性

webapp/view/HelloPanel.view.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
25
26
27
<mvc:View 
controllerName="TestCaseTwo.controller.HelloPanel"
xmlns:mvc="sap.ui.core.mvc"
displayBlock="true"
xmlns="sap.m">
<Panel headerText="{i18n>helloPanelTitle}" class="sapUiResponsiveMargin" width="auto">
<content>
<Button
id="helloDialogButton"
icon="sap-icon://world" <!--新增内容-->
text="{i18n>openDialogButtonText}"
press="onOpenDialog"
class="sapUiSmallMarginEnd"/>
<Button
text="{i18n>showHelloButtonText}"
press="onWhowHello"
class="myCustomButton"/>
<Input
value="{/recipient/name}"
valueLiveUpdate="true"
width="60%"/>
<FormattedText
htmlText="Hello {/recipient/name}"
class="sapUiSmallMargin sapThemeHighlight-asColor myCustomText"/>
</content>
</Panel>
</mvc:View>

sap icon://协议指示应加载图标字体中的图标。

(2)在弹出框中添加图标

webapp/view/HelloDialog.fragment.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<core:FragmentDefinition xmlns="sap.m" xmlns:core="sap.ui.core">
<Dialog id="helloDialog" title="Hello {/recipient/name}">
<!--新增内容-->
<content>
<core:Icon
src="sap-icon://hello-world"
size="8rem"
class="sapUiMediumMargin"/>
</content>
<!--新增内容-->
<beginButton>
<Button
text="{/recipient/text}"
press=".onCloseDialog"
class="sapThemeHighlight-asColor"/>
</beginButton>
</Dialog>
</core:FragmentDefinition>

<content>在第一个知识点中有提到过,是一个容器标签,用于将内容限制在容器中

(3)效果展示图
知识点六效果展示图

七、聚合绑定(Aggregation Binding)

现在我们已经为我们的应用程序建立了一个良好的结构,是时候添加更多功能了。通过添加一些JSON格式的发票数据,我们开始探索数据绑定的更多功能,这些数据显示在面板下方的列表中。

(1)创建测试数据文件

webapp/Invoices.json (New)

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
{
"Invoices": [
{
"ProductName": "Pineapple",
"Quantity": 21,
"ExtendedPrice": 87.2000,
"ShipperName": "Fun Inc.",
"ShippedDate": "2015-04-01T00:00:00",
"Status": "A"
},
{
"ProductName": "Milk",
"Quantity": 4,
"ExtendedPrice": 9.99999,
"ShipperName": "ACME",
"ShippedDate": "2015-02-18T00:00:00",
"Status": "B"
},
{
"ProductName": "Canned Beans",
"Quantity": 3,
"ExtendedPrice": 6.85000,
"ShipperName": "ACME",
"ShippedDate": "2015-03-02T00:00:00",
"Status": "B"
},
{
"ProductName": "Salad",
"Quantity": 2,
"ExtendedPrice": 8.8000,
"ShipperName": "ACME",
"ShippedDate": "2015-04-12T00:00:00",
"Status": "C"
},
{
"ProductName": "Bread",
"Quantity": 1,
"ExtendedPrice": 2.71212,
"ShipperName": "Fun Inc.",
"ShippedDate": "2015-01-27T00:00:00",
"Status": "A"
}
]
}

发票文件只包含五张JSON格式的发票,我们可以使用它们在应用程序中绑定控件。JSON是一种非常轻量级的数据存储格式,可以直接用作SAPUI5应用程序的数据源。

(2)配置测试数据模型

webapp/manifest.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
{

"sap.ui5": {
"rootView": "sap.ui.demo.walkthrough.view.App",
[]
"models": {
"i18n": {
"type": "sap.ui.model.resource.ResourceModel",
"settings": {
"bundleName": "sap.ui.demo.walkthrough.i18n.i18n",
"supportedLocales": [""],
"fallbackLocale": ""
}
}
// 新增内容
,
"invoice": {
"type": "sap.ui.model.json.JSONModel",
"uri": "Invoices.json"
}
// 新增内容
}
}
}

该操作将自动实例化一个别名为“invoice”的数据模型。因为是数据模型,所以在类型方面传递”sap.ui.model.json.JSONModel”。uri指定的是文件所在的路径。不过要注意的是这种方式创建的数据模型只能在版本1.30以上的版中使用,低于这个版本的需要使用在组件的init方法中手动实例化资源包和应用程序的Component.js文件。

(3)增加一个嵌套视图

webapp/view/View1.view.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<mvc:View controllerName="TestCaseTwo.controller.View1" xmlns:html="http://www.w3.org/1999/xhtml" xmlns:mvc="sap.ui.core.mvc"
displayBlock="true" xmlns="sap.m">
<Shell>
<App class="myAppDemoWT">
<pages>
<Page title="{i18n>title}">
<headerContent>
<Button
icon="sap-icon://hello-world"
press=".onOpenDialog"/>
</headerContent>
<content>
<mvc:XMLView viewName="TestCaseTwo.view.HelloPanel"/>
<mvc:XMLView viewName="TestCaseTwo.view.InvoiceList"/> <!--新增内容-->
</content>
</Page>
</pages>
</App>
</Shell>
</mvc:View>
(4)编写嵌套的视图的内容

webapp/view/InvoiceList.view.xml (New)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<mvc:View
xmlns="sap.m"
xmlns:mvc="sap.ui.core.mvc">
<List
headerText="{i18n>invoiceListTitle}"
class="sapUiResponsiveMargin"
width="auto"
items="{invoice>/Invoices}">
<items>
<ObjectListItem
title="{invoice>Quantity} x {invoice>ProductName}"/>
</items>
</List>
</mvc:View>

新视图显示了一个带有自定义标题文本的列表控件。列表的项聚合绑定到JSON数据的根路径。由于我们定义了一个命名模型,我们必须在每个绑定定义前面加上标识符invoice>。

在items聚合中,我们为列表定义模板,该模板将自动为测试数据的每个发票重复。更准确地说,我们使用ObjectListItem为items聚合的每个聚合子级创建一个控件。列表项的标题属性绑定到单个发票的属性。这是通过定义一个相对路径(没有/在开始时)来实现的。这是因为我们通过items={invoice>/Invoices}将items聚合绑定到发票。

(5)设置i18n文件
1
2
3
4
...

# Invoice List
invoiceListTitle=Invoices
(6)展示效果图
知识点七展示效果图

八、数据类型

接下来设置各项发票中的金额和单位。

(1)在列表中添加货币和货币单位

webapp/view/InvoiceList.view.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
25
<mvc:View
controllerName="TestCaseTwo.controller.InvoiceList"
xmlns="sap.m"
xmlns:mvc="sap.ui.core.mvc">
<List
headerText="{i18n>invoiceListTitle}"
class="sapUiResponsiveMargin"
width="auto"
items="{invoice>/Invoices}">
<items>
<ObjectListItem
title="{invoice>Quantity} x {invoice>ProductName}"
<!--新增内容-->
number="{
parts:[{path: 'invoice>ExtendedPrice'},{path: 'view>/currency'}],
type: 'sap.ui.model.type.Currency',
formatOpentions: {
showMeasure: false
}
}"
numberUnit="{view>/currency}"/>
<!--新增内容-->
</items>
</List>
</mvc:View>

通过设置了列表的number和numberUnit属性来设置列表中的金额和金额单位。因为金额是数字,所以设置显示类型是sap.ui.model.type.Currency。

如上所示,我们对ObjectListItem的number属性使用了一种特殊的绑定语法。这种绑定语法使用所谓的“计算字段”,允许将不同模型的多个属性绑定到控件的单个属性。从不同模型绑定的属性称为“零件”。在上面的示例中,控件的属性是number,从两个不同的模型检索到的绑定属性(“部件”)是invoice>ExtendedPrice和view>/currency。

我们希望以欧元显示价格,通常情况下,货币是后端数据模型的一部分。在我们的例子中,情况并非如此,因此我们需要直接在应用程序中定义它。因此,我们为发票列表添加了一个控制器,并使用currency属性作为绑定语法的第二部分。货币类型将根据货币代码为我们处理价格格式。在我们的例子中,价格以2位小数显示。

此外,我们还将格式选项showMeasure设置为false。这会在属性号中隐藏货币代码,因为它会作为单独的属性号RunIt传递给ObjectListItem控件。

(2)设置货币的单位数据模型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
sap.ui.define([
"sap/ui/core/mvc/Controller",
"sap/ui/model/json/JSONModel"
], function (Controller, JSONModel) {
"use strict";

return Controller.extend("TestCaseTwo.controller.InvoiceList",{
onInit : function(){
var oViewModel = new JSONModel({
currency: "EUR"
});
this.getView().setModel(oViewModel,"view");
}
});

});
(3)效果图展示
知识点八效果展示图

九、绑定表达式

有时,预定义的SAPUI5类型不够灵活,您需要在视图中进行简单的计算或格式化,这正是表达式真正有用的地方。我们使用它们根据数据模型中的当前数字来设置价格。

(1)添加表达式

webapp/view/InvoiceList.view.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
<mvc:View
controllerName="TestCaseTwo.controller.InvoiceList"
xmlns="sap.m"
xmlns:mvc="sap.ui.core.mvc">
<List
headerText="{i18n>invoiceListTitle}"
class="sapUiResponsiveMargin"
width="auto"
items="{invoice>/Invoices}">
<items>
<ObjectListItem
title="{invoice>Quantity} x {invoice>ProductName}"
number="{
parts:[{path: 'invoice>ExtendedPrice'},{path: 'view>/currency'}],
type: 'sap.ui.model.type.Currency',
formatOpentions: {
showMeasure: false
}
}"
numberUnit="{view>/currency}"
numberState="{= ${invoice>ExtendedPrice} > 50 ? 'Error' : 'Success' }"/>//新增内容
</items>
</List>
</mvc:View>

通过新增属性numberState并给属性的赋值中使用三元运算符,来进行属性的设置。当金额大于50时设置numberState为“Error”(红色),反之小于等于50的数据设置属性为“Success”(绿色)。

表达式仅限于帮助格式化数据的一组特定操作,如数学表达式、比较等。您可以在文档中查找可能的操作。

(2)效果图展示
知识点九效果展示图

十、自定义格式化程序

如果我们想对数据模型的属性进行更复杂的格式化逻辑,我们还可以编写一个自定义格式化函数。我们现在将添加一个带有自定义格式化程序的本地化状态,因为数据模型中的状态是一种相当技术性的格式。

(1)编写自定义格式化的工具JS

webapp/model/formatter.js (New)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
sap.ui.define([],function () {
"use strict";
return {
statusText: function (sStatus){
var resourceBundle = this.getView().getModel("i18n").getResourceBundle();
switch (sStatus){
case "A":
return resourceBundle.getText("invoiceStatusA");
case "B":
return resourceBundle.getText("invoiceStatusB");
case "C":
return resourceBundle.getText("invoiceStatusC");
default:
return sStatus;
}
}
};
});

创建工具JavaScript文件,里面主要是使用由调用函数的对象传递进来的参数进行判断,并将i18n数据模型中对应字段的值返回回来。

函数statusText从数据模型获取技术状态作为输入参数,并返回从resourceBundle文件读取的可读文本。

(2)加载自定义格式化工具代码

webapp/controller/InvoiceList.controller.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
sap.ui.define([
"sap/ui/core/mvc/Controller",
"sap/ui/model/json/JSONModel",
"./formatter" //新增内容
], function (Controller, JSONModel, formatter) {
"use strict";

return Controller.extend("TestCaseTwo.controller.InvoiceList",{
formatter: formatter, //新增内容
onInit : function(){
var oViewModel = new JSONModel({
currency: "EUR"
});
this.getView().setModel(oViewModel,"view");
}
});

});

要加载格式化程序函数,我们必须将其添加到InvoiceList.controller.js。在这个控制器中,我们首先向自定义格式化程序模块添加一个依赖项。控制器只是将加载的格式化程序函数存储在本地属性格式化程序中,以便能够在视图中访问它们。

(3)将自定义格式化的数据绑定视图位置

webapp/view/InvoiceList.view.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
25
26
27
28
29
30
31
32
33
<mvc:View
controllerName="TestCaseTwo.controller.InvoiceList"
xmlns="sap.m"
xmlns:mvc="sap.ui.core.mvc">
<List
headerText="{i18n>invoiceListTitle}"
class="sapUiResponsiveMargin"
width="auto"
items="{invoice>/Invoices}">
<items>
<ObjectListItem
title="{invoice>Quantity} x {invoice>ProductName}"
number="{
parts:[{path: 'invoice>ExtendedPrice'},{path: 'view>/currency'}],
type: 'sap.ui.model.type.Currency',
formatOpentions: {
showMeasure: false
}
}"
numberUnit="{view>/currency}"
numberState="{= ${invoice>ExtendedPrice} > 50 ? 'Error' : 'Success' }">
<!--新增内容-->
<firstStatus>
<ObjectStatus text="{
path: 'invoice>Status',
formatter: '.formatter.statusText'
}"/>
</firstStatus>
</ObjectListItem>
<!--新增内容-->
</items>
</List>
</mvc:View>

我们使用firstStatus聚合将一个状态添加到ObjectListItem,它将显示发票的状态。自定义格式化程序函数是使用绑定语法的保留属性格式化程序指定的。“A”格式化程序名称前面表示在当前视图的控制器中查找函数。在那里,我们定义了一个属性格式化程序,它保存格式化程序函数,因此我们可以通过.formatter.statusText的方式调用方法并传递参数。

(4)设置i18n的文本

webapp/i18n/i18n.properties

1
2
3
4
5
6
7
...

# Invoice List
invoiceListTitle=Invoices
invoiceStatusA=New
invoiceStatusB=In Progress
invoiceStatusC=Done

在工具formatter.js文件中,是通过读取数据模型i18n中的可读文本并将文本的值返回回去。

(5)效果图展示
知识点十效果展示图

十一、过滤器

在这一步中,我们为产品列表添加一个搜索字段,并定义一个表示搜索词的过滤器。搜索时,列表会自动更新,仅显示与搜索词匹配的项目。

(1)添加搜索框

webapp/view/InvoiceList.view.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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<mvc:View
controllerName="TestCaseTwo.controller.InvoiceList"
xmlns="sap.m"
xmlns:mvc="sap.ui.core.mvc">
<List
id="invoiceList" <!--新增内容-->
headerText="{i18n>invoiceListTitle}"
class="sapUiResponsiveMargin"
width="auto"
items="{invoice>/Invoices}">
<!--新增内容-->
<headerToolbar>
<Toolbar>
<Title text="{i18n>invoiceListTitle}"/>
<ToolbarSpacer/>
<SearchField width="50%" search=".onFilterInvoices"/>
</Toolbar>
</headerToolbar>
<!--新增内容-->
<items>
<ObjectListItem
title="{invoice>Quantity} x {invoice>ProductName}"
number="{
parts:[{path: 'invoice>ExtendedPrice'},{path: 'view>/currency'}],
type: 'sap.ui.model.type.Currency',
formatOpentions: {
showMeasure: false
}
}"
numberUnit="{view>/currency}"
numberState="{= ${invoice>ExtendedPrice} > 50 ? 'Error' : 'Success' }">
<firstStatus>
<ObjectStatus text="{
path: 'invoice>Status',
formatter: '.formatter.statusText'
}"/>
</firstStatus>
</ObjectListItem>
</items>
</List>
</mvc:View>

为发票表格扩展了一个搜索控件,并为列表控件添加一个ID invoiceList,如此便可以从搜索字段的FilterInvoices上的事件处理程序函数中识别的列表。搜索字段是列表标题的一部分,因此,对列表绑定的每次更改都将触发整个列表(包括搜索字段)的重新排序。

headerToolbar聚合替换了我们之前用于列表标题的简单标题属性。工具栏控件更加灵活,可以根据需要进行调整。使用了sap.m.Title 控制表格的标题。间隔符和在右侧的sap.m.SearchField。

(2)设置过滤框的响应逻辑

webapp/controller/InvoiceList.controller.js

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
31
32
33
34
35
36
37
sap.ui.define([
"sap/ui/core/mvc/Controller",
"sap/ui/model/json/JSONModel",
"./formatter",
//新增内容
"sap/ui/model/Filter",
"sap/ui/model/FilterOperator"
//新增内容
], function (Controller, JSONModel, formatter, Filter, FilterOperator) {
"use strict";

return Controller.extend("TestCaseTwo.controller.InvoiceList",{
formatter: formatter,
onInit : function(){
var oViewModel = new JSONModel({
currency: "EUR"
});
this.getView().setModel(oViewModel,"view");
},
//新增内容
onFilterInvoices : function(oEvent) {
// 构建过滤器阵列
var aFilter = [];
var sQuery = oEvent.getParameter("query");
if (sQuery){
aFilter.push(new Filter("ProductName",FilterOperator.Contains,sQuery));
}

// 过滤内容
var oList = this.byId("invoiceList");
var oBinding = oList.getBinding("items");
oBinding.filter(aFilter);
}
//新增内容
});

});

此处先构建了一个空数组变量aFilter,然后通过搜索帮助事件传递过来的事件对象获取搜索帮助框中传递过来的过滤内容到sQuery变量中。然后创建了一个过滤对象添加到数组中。

获取列表组件,然后在获取列表的items内容,再将添加了过滤条件的数组传递到列表的过滤函数中获取到过滤内容。并刷新列表,此处的数组aFilter可以理解成ABAP中的Rangle表。数组为空时全查,反之根据内表中的数据过滤数据。但是不同的一点是此处的查询时模糊查询。

为过滤加载了两个新的依赖项。filter对象将保存我们对filter操作的配置,FilterOperator是我们指定筛选器所需的助手类型。如果我们想搜索多个数据字段,我们还可以向数组中添加更多过滤器。在我们的示例中,我们只需在ProductName路径中搜索,并指定一个将搜索给定查询字符串的筛选器运算符。

使用我们在视图中指定的ID访问列表,因为控件自动以视图ID作为前缀,所以我们需要向视图请求带有帮助函数byId的控件。在列表控件上,我们访问聚合项的绑定,用新构造的过滤器对象对其进行过滤。这将根据我们的搜索字符串自动筛选列表,以便在触发搜索时仅显示匹配的项目。过滤器操作员过滤器操作员。Contains不区分大小写。

(3)效果图展示
知识点十一展示效果图

十二、排序和分组

为了让发票列表更加方便用户,我们按字母顺序对其排序,而不是仅仅显示数据模型中的顺序。此外,我们还引入了组,并添加了发货公司,以便更容易使用数据。

(1)修改Items设置排序字段名

webapp/view/InvoiceList.view.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
25
26
27
<mvc:View
controllerName="TestCaseTwo.controller.InvoiceList"
xmlns="sap.m"
xmlns:mvc="sap.ui.core.mvc">
<List
id="invoiceList"
headerText="{i18n>invoiceListTitle}"
class="sapUiResponsiveMargin"
width="auto"
// 新增内容
items="{
path: 'invoice>/Invoices',
sorter: {
path: 'ProductName',
descending: true // 设置倒叙排序
}
// 新增内容
}"
>
<headerToolbar>
...
</headerToolbar>
<items>
...
</items>
</List>
</mvc:View>

我们在绑定语法中添加了一个声明性排序器。将简单的绑定语法转换为对象表示法,指定数据的路径,现在添加一个额外的sorter属性。指定了发票项目排序的数据路径(字段名),其余的都是自动完成的。默认情况下,排序是升序的,当需要倒序排序时可以参考上面的例子设置“descending”属性值为true。

此时刷新执行程序可以发现列表中的数据时倒叙排序的。

知识点十二展示效果图1

(2)设置分组

webapp/view/InvoiceList.view.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
25
26
27
<mvc:View
controllerName="TestCaseTwo.controller.InvoiceList"
xmlns="sap.m"
xmlns:mvc="sap.ui.core.mvc">
<List
id="invoiceList"
headerText="{i18n>invoiceListTitle}"
class="sapUiResponsiveMargin"
width="auto"
// 新增内容
items="{
path: 'invoice>/Invoices',
sorter: {
path: 'ShipperName',
group: true
}
}"
// 新增内容
>
<headerToolbar>
...
</headerToolbar>
<items>
...
</items>
</List>
</mvc:View>

和设置排序类似,同样使用到了sorter属性,只不过该属性内部的属性由原来的倒序属性变成了组(group),并且对应的路径(字段)也变成了ShipperName。可以通过设置groupHeaderFactory属性来定义一个自定义的组件工厂。

知识点十二效果展示图2

十三、远程OData服务

到目前为止,我们已经使用了本地JSON数据,但现在我们将访问一个真正的OData服务来可视化远程数据。

在现实世界中,数据通常驻留在远程服务器上,并通过OData服务进行访问。我们将向清单中添加一个数据源配置,并用公开的Northwind OData服务替换发票模型的JSONModel类型,以可视化远程数据。你会惊讶地发现,为了让这一切顺利进行,几乎不需要做什么改变!

webapp/manifest.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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
{
"_version": "1.7.0",
"sap.app": {
"id": "TestCaseTwo",
"type": "application",
"i18n": "i18n/i18n.properties",
"applicationVersion": {
"version": "1.0.0"
},
...
},
// 新增内容
"dataSources": {
"invoiceRemote": {
"uri": "Northwind/V2/Northwind/Northwind.svc/",
"type": "OData",
"settings": {
"odataVersion": "2.0"
}
}
}
// 新增内容
},

"sap.ui": {
...
},

"sap.ui5": {
...
"models": {
"i18n": {
"type": "sap.ui.model.resource.ResourceModel",
"settings": {
"bundleName": "TestCaseTwo.i18n.i18n"
}
},
"invoice":{
"dataSource": "invoiceRemote" // 新增内容
}
},
...
}
}

通过该方式配置时因为CORS策略的原因无法访问由SAP官方提供的资源连接。详细内容可以参考样例26下面的CORS策略解决方式。

在描述符文件的sap.app部分,我们添加了一个数据源配置。使用invoiceRemote键,我们指定一个允许自动模型实例化的配置对象。我们指定服务的类型(OData)和模型版本(2.0)。在这一步中,我们希望使用位于https://services.odata.org/V2/Northwind/Northwind.svc/.因此,URI指向官方的Northwind OData服务。

为了避免下文所述的跨源资源共享问题,典型的程序是只维护一条路径,例如/V2/Northwind/Northwind。svc/,位于数据源的URI属性中。然而,如果实际的OData服务器位于不同的地址,这也使得使用代理成为必要。

在模型部分,我们替换发票模型的内容。当模型在组件初始化期间自动实例化时,此键仍用作模型名称。然而,dataSource键的invoiceRemote值是对我们在上面指定的数据源部分的引用。此配置允许组件在应用程序启动期间检索此型号的技术信息。

我们的组件现在自动创建一个sap.ui.model.odata.v2.ODataModel根据我们上面指定的设置,并将其作为名为invoice的模型提供。使用invoiceRemote数据源时,ODataModel会从真正的Northwind OData服务获取数据。我们从Northwind OData服务收到的发票与我们之前使用的JSON数据具有相同的属性(除了status属性,它在Northwind OData服务中不可用)。

CORS策略解决方式:

  • SAP Web IDE:如下所述配置目标(推荐)
  • 本地开发:配置本地代理
  • 解决方法:在本地测试中禁用浏览器中的同源策略(不推荐,仅适用于测试)
  • 在远程系统上设置CORS相关响应头(如果可能)https://www.jianshu.com/p/72c8c7863418

详细内容参考样例26中的CORS策略。

十四、模拟服务器配置

我们只是针对一个真正的服务运行我们的应用程序,但是为了开发和测试我们的应用程序,我们不想依赖“真正”服务的可用性,也不想给数据服务所在的系统增加额外的负载。

这个系统就是所谓的后端系统,我们现在将使用名为mock server的SAPUI5特性来模拟它。它为本地文件提供服务,但它模拟后端系统比只加载本地数据更现实。我们还将更改模型实例化部分,以便在描述符中配置模型,并由SAPUI5自动实例化。这样,我们就不需要在代码中处理模型实例化。

1.环境配置

注意事项:

想要使用本地代理的服务器需要配置相对应的配置文件,且配置完成之后或许仍然存在问题。先将配置方法记录如下。

(1)打开Webide安装目录
十四环境配置安装路径
(2)在当前路径后追加如下的路径

\config_master\service.destinations\destinations

追加回车后会进入destinations文件夹,里面应该是空的。

(3)创建配置文件

在上面进入到路径中创建一个名字为:Northwind的没有后缀名的文件。

十四环境配置创建配置文件
(4)编写文件内容

使用记事本打开文件在其中键入如下内容:

1
2
3
4
5
6
7
8
9
10
Description=Northwind
Type=HTTP
Authentication=NoAuthentication
WebIDEUsage=odata_gen
Name=northwind
WebIDEEnabled=true
CloudConnectorVersion=2
URL=https\://services.odata.org
ProxyType=Internet
WebIDESystem=Northwind

保存后重启Webide。

(5)对项目配置环境
十四环境配置项目环境配置

能进入下面的页面并下拉出Northwind选项就说明模拟服务器的环境配置好了,接下来就只需要将对应的模拟服务器数据模型导入到项目即可。

十四环境配置项目环境配置2

接下来对第三步的URL进行说明:

我们需要访问如下这个资源网址:https://services.odata.org/V2/Northwind/Northwind.svc/

然后我们打开刚刚配置的Northwind文件,可以看见其中的URL已被设置为:https://services.odata.org。所以在填写URL时只填写V2/Northwind/Northwind.svc/这一部分。

十四环境配置Northwind文件
(6)覆盖现有OData服务连接

在上一步中点击玩Next按钮后进入该页面,勾选Overwrite existing OData service connection选项。

十四环境配置OData链接

然后点击完成按钮。

(7)配置数据模型

继续在manifest.json文件中选择Models页签点击加号按钮

十四环境配置数据模型

在新弹出的框中左下角的选择框代表使用模型默认的名称,如果勾选了该项则在最上面的模型名称内容就可以不选了。在第二项模型来源中则可以从下拉框中查看到刚刚从资源网站中获取到的模型。

十四环境配置数据模型2

选择完成点击OK按钮后,可以发现在Models页签中多了一项Default数据模型。

十四环境配置最后一步

如此这样就配置好了模拟服务器的环境和项目所需要的数据模型。接下来配置项目中在模拟服务器中运行的文件。

2.模拟服务器文件
(1)创建模拟服务器的入口文件

webapp/test/mockServer.html (New)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>SAPUI5 Walkthrough - Test Page</title>
<script
id="sap-ui-bootstrap"
src="https://ui5.sap.com/resources/sap-ui-core.js"
data-sap-ui-theme="sap_belize"
data-sap-ui-libs="sap.m"
data-sap-ui-resourceroots='{
"TestCaseTwo": "../"
}'
data-sap-ui-oninit="module:TestCaseTwo/test/initMockServer"
data-sap-ui-compatVersion="edge"
data-sap-ui-async="true">
</script>
</head>
<body class="sapUiBody" id="content">
<div data-sap-ui-component data-name="TestCaseTwo" data-id="container" data-settings='{"id" : "TestCaseTwo"}'></div>

</body>
</html>

现在存在两个不同的入口页面:一个用于真正的“连接”应用程序(index.html),另一个用于本地测试(mockServer.html)。你可以自由决定下一步是对应用程序中的实际服务数据还是本地数据执行。

我们修改mockServer.html文件,并更改页面标题以将其与生产性起始页区分开来。在引导过程中,数据sap ui resourceroots属性也发生了更改。名称空间现在指向上方的文件夹(“../”),因为mockServer.html文件现在位于webapp文件夹的子文件夹中。我们现在调用脚本initMockServer.js,而不是直接加载应用程序组件。

(2)创建组件初始化生命周期文件

webapp/test/initMockServer.js (New)

1
2
3
4
5
6
7
8
9
10
11
sap.ui.define([
"../localService/mockserver"
], function (mockserver) {
"use strict";

// initialize the mock server
mockserver.init();

// initialize the embedded component on the HTML page
sap.ui.require(["sap/ui/core/ComponentSupport"]);
});

我们将要实现的mockserver依赖性是我们的本地测试服务器。在加载组件之前,会立即调用它的init方法。通过这种方式,我们可以捕获所有将进入“真实”服务的请求,并在使用mockServer.html启动应用程序时,通过测试服务器在本地处理它们文件。组件本身并不“知道”它现在将在测试模式下运行。

(3)创建本地数据模型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
30
31
32
33
34
35
36
37
38
39
40
41
42
[
{
"ProductName": "Pineapple",
"Quantity": 21,
"ExtendedPrice": 87.2000,
"ShipperName": "Fun Inc.",
"ShippedDate": "2015-04-01T00:00:00",
"Status": "A"
},
{
"ProductName": "Milk",
"Quantity": 4,
"ExtendedPrice": 9.99999,
"ShipperName": "ACME",
"ShippedDate": "2015-02-18T00:00:00",
"Status": "B"
},
{
"ProductName": "Canned Beans",
"Quantity": 3,
"ExtendedPrice": 6.85000,
"ShipperName": "ACME",
"ShippedDate": "2015-03-02T00:00:00",
"Status": "B"
},
{
"ProductName": "Salad",
"Quantity": 2,
"ExtendedPrice": 8.8000,
"ShipperName": "ACME",
"ShippedDate": "2015-04-12T00:00:00",
"Status": "C"
},
{
"ProductName": "Bread",
"Quantity": 1,
"ExtendedPrice": 2.71212,
"ShipperName": "Fun Inc.",
"ShippedDate": "2015-01-27T00:00:00",
"Status": "A"
}
]

模拟服务器访问的数据模型文件。

(4)创建元素据服务接口文件

webapp/localService/metadata.xml (New)

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
<edmx:Edmx Version="1.0" xmlns:edmx="http://schemas.microsoft.com/ado/2007/06/edmx">
<edmx:DataServices m:DataServiceVersion="1.0" m:MaxDataServiceVersion="3.0"
xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata">
<Schema Namespace="NorthwindModel" xmlns="http://schemas.microsoft.com/ado/2008/09/edm">
<EntityType Name="Invoice">
<Key>
<PropertyRef Name="ProductName"/>
<PropertyRef Name="Quantity"/>
<PropertyRef Name="ShipperName"/>
</Key>
<Property Name="ShipperName" Type="Edm.String" Nullable="false" MaxLength="40" FixedLength="false"
Unicode="true"/>
<Property Name="ProductName" Type="Edm.String" Nullable="false" MaxLength="40" FixedLength="false"
Unicode="true"/>
<Property Name="Quantity" Type="Edm.Int16" Nullable="false"/>
<Property Name="ExtendedPrice" Type="Edm.Decimal" Precision="19" Scale="4"/>
<Property Name="Status" Type="Edm.String" Nullable="false" MaxLength="1" FixedLength="false"
Unicode="true"/>
</EntityType>
</Schema>
<Schema Namespace="ODataWebV2.Northwind.Model" xmlns="http://schemas.microsoft.com/ado/2008/09/edm">
<EntityContainer Name="NorthwindEntities" m:IsDefaultEntityContainer="true" p6:LazyLoadingEnabled="true"
xmlns:p6="http://schemas.microsoft.com/ado/2009/02/edm/annotation">
<EntitySet Name="Invoices" EntityType="NorthwindModel.Invoice"/>
</EntityContainer>
</Schema>
</edmx:DataServices>
</edmx:Edmx>

元数据文件包含有关服务接口的信息,不需要手动编写。通过调用服务URL并在末尾添加$metadata(例如,在我们的例子中),可以直接从“real”服务访问它http://services.odata.org/V2/Northwind/Northwind.svc/$metadata)。模拟服务器将读取此文件以模拟真实的OData服务,并将以正确的格式返回本地源文件的结果,以便应用程序使用(XML或JSON格式)。

为了简单起见,我们从原始Northwind OData元数据文档中删除了场景中不需要的所有内容。我们还将状态字段添加到元数据中,因为它在真正的Northwind服务中不可用。

(5)效果图展示
知识点十四效果展示图

十五、用QUnit进行单元测试

现在我们在应用程序中有了一个测试文件夹,我们可以开始增加测试覆盖率了。

在test文件假下新建一个unit文件夹,再在unit文件夹下新建一个model文件夹。

1.步骤
(1)设置测试用JS文件

webapp/test/unit/model/formatter.js(New)

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/*global QUnit*/
sap.ui.define([
"sap/ui/demo/TestCaseTwo/controller/formatter",
"sap/ui/model/resource/ResourceModel"
],function (formatter,ResourceModel) {
"use strict";
QUnit.module("Formatting functions",{
beforeEach: function () {
this._oResourceModel = new ResourceModel({
bundleUrl: sap.ui.require.toUrl("sap/ui/demo/TestCaseTwo") + "/i18n/i18n.properties"
});
},
afterEach: function () {
this._oResourceModel.destroy();
}
});

QUnit.test("Should return the translated texts",function (assert) {
// Arrange
// this.stub() does not support chaining and always returns the right data
// even if a wrong or empty parameter is passed.
var oModel = this.stub();
oModel.withArgs("i18n").returns(this._oResourceModel);
var oViewStub = {
getModel: oModel
};
var oControllerStub = {
getView: this.stub().returns(oViewStub)
};

// System under test
var fnIsolatedFormatter = formatter.statusText.bind(oControllerStub);

// Assert
assert.strictEqual(fnIsolatedFormatter("A"), "New", "The long text for status A is correct");

assert.strictEqual(fnIsolatedFormatter("B"), "In Progress", "The long text for status B is correct");

assert.strictEqual(fnIsolatedFormatter("C"), "Done", "The long text for status C is correct");

assert.strictEqual(fnIsolatedFormatter("Foo"), "Foo", "The long text for status Foo is correct");

});
});

我们在webapp/test/unit/model下创建了一个新的formatter.js文件,其中实现了自定义格式化程序的单元测试。我们要测试的格式化程序文件作为依赖项加载。我们还需要对ResourceModel的依赖,因为我们想检查翻译的文本是否正确。

格式化程序文件只包含一个用于格式化程序功能的QUnit模块。它用beforeach函数中的本地化文本实例化我们的ResourceBundle,并在afterEach函数中再次销毁它。在执行每个测试之前和之后都会调用这些函数。

接下来是格式化程序函数的单元测试。在我们在设置自定义格式化步骤中创建的statusText函数的实现中,我们通过以下排队调用访问ResourceBundle:var resourceBundle = this.getView().getModel(“i18n”).getResourceBundle();

由于我们不想测试控制器、视图或模型功能,我们首先在SinonJS及其存根方法的帮助下,通过用空外壳替换这些调用来删除依赖项。这发生在单元测试的排列部分。SinonJS为所有对象注入一个存根方法,因此我们可以简单地调用它。stub()为我们需要模拟的任何行为创建一个新的stub。

然后通过调用JavaScript的bind函数将stub绑定到statusText格式化程序。当使用变量fnIsolatedFormatter调用函数时,this指针现在绑定到我们的控制器stub,并且我们仍然可以随心所欲地传入参数。这发生在测试的“测试中的系统”部分。

最后,我们执行我们的assertions。我们通过使用数据模型中预期的值(A、B、C和其他所有内容)调用独立的格式化程序函数来检查格式化程序逻辑的每个分支。我们严格比较格式化程序函数的结果和我们期望从资源包中得到的硬编码字符串,如果测试失败,我们会给出一条有意义的错误消息。我们在这里硬编码字符串,以确定资源包属性的问题。如果缺少一个属性,那么如果我们对照资源包中的实际值(两边都是空字符串)进行检查,测试仍然会成功。

(2)创建单元测试运行文件并加载单元测试的基本功能

webapp/test/unit/unitTests.qunit.html (New)

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
<!DOCTYPE html>
<html>
<head>
<title>Unit tests for SAPUI5 Walkthrough</title>
<meta charset="utf-8">

<script
id = "sap-ui-bootstrap"
src = "https://openui5.hana.ondemand.com/resources/sap-ui-core.js"
data-sap-ui-resourceroots = '{
"sap.ui.demo.TestCaseTwo": "../../"
}'
data-sap-ui-async = "true">
</script>

<link rel="stylesheet" type="text/css" href="https://openui5.hana.ondemand.com/resources/sap/ui/thirdparty/qunit-2.css">

<script src="https://openui5.hana.ondemand.com/resources/sap/ui/thirdparty/qunit-2.js"></script>
<script src="https://openui5.hana.ondemand.com/resources/sap/ui/qunit/qunit-junit.js"></script>
<script src="https://openui5.hana.ondemand.com/resources/sap/ui/qunit/qunit-coverage.js"></script>
<script src="https://openui5.hana.ondemand.com/resources/sap/ui/thirdparty/sinon.js"></script>
<script src="https://openui5.hana.ondemand.com/resources/sap/ui/thirdparty/sinon-qunit.js"></script>

<script src="unitTests.qunit.js"></script>
</head>
<body>
<div id="qunit"></div>
<div id="qunit-fixture"></div>
</body>
</html>

所谓的QUnit测试套件是一个HTML页面,用于触发应用程序的所有QUnit测试。大部分内容是生成结果页面的布局,您可以在预览中看到,我们不会进一步解释这些部分,而是将重点放在应用程序部分。

让我们从名称空间开始。由于我们现在在webapp/test/unit文件夹中,我们实际上需要升级两个级别才能再次获得src文件夹。这个名称空间可以在测试中用于加载和触发应用程序功能。

首先,我们通过脚本标签加载一些基本的QUnit功能。这里还可以添加其他QUnit测试。然后HTML页面加载另一个名为unitTests的脚本。昆特。js,我们将在下一步创建它。这个脚本将执行我们的格式化程序。

(3)创建单元测试入口程序加载的js文件

webapp/test/unit/unitTests.qunit.js (New)

1
2
3
4
5
6
7
8
9
10
11
12
/* global QUnit */
QUnit.config.autostart = false;

sap.ui.getCore().attachInit(function () {
"use strict";

sap.ui.require([
"sap/ui/demo/TestCaseTwo/test/unit/model/formatter"
], function () {
QUnit.start();
});
});

这个脚本加载并执行我们的格式化程序。如果我们现在打开webapp/test/unit/unitTests.qunit.html中,我们应该看到我们的测试正在运行并验证格式化程序逻辑。

(4)效果图展示
知识点十五展示效果图
2.注意事项
  • 所有单元测试都放在应用程序的webapp/test/unit文件夹中。
  • 测试套件中的文件以*.qunit.html.
  • unitTests.qunit.html文件触发应用程序的所有单元测试。
  • 应该为格式化程序、控制器逻辑和其他单个功能编写单元测试。
  • 所有依赖项都被stubs替换,以便只测试范围内的功能。

十六、路由和导航

到目前为止,我们已将所有应用程序内容放在一个页面上。随着我们添加越来越多的功能,我们希望分割内容并将其放在单独的页面上。

在这一步中,我们将使用SAPUI5导航功能加载并显示一个单独的详细信息页面,稍后可以使用该页面显示发票的详细信息。在前面的步骤中,我们直接在应用程序视图中定义了页面,以便在加载应用程序时显示页面。我们现在将使用SAPUI5路由器类来加载页面并自动更新URL。我们为应用程序指定路由配置,并为应用程序的每个页面创建单独的视图,然后通过触发导航事件连接视图。

1.路由设置
(1)配置路由

webapp/manifest.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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
{
"_version": "1.12.0",

"sap.ui5": {

"models": {

},
"routing": {
"config": {
"routerClass": "sap.m.routing.Router",
"viewType": "XML",
"viewPath": "TestCaseTwo.view",
"controlId": "app",
"controlAggregation": "pages",
"async": true
},
"routes": [
{
"pattern": "",
"name": "overview",
"target": "overview"
},
{
"pattern": "detail",
"name": "detail",
"target": "detail"
}
],
"targets": {
"overview": {
"viewId": "overview",
"viewName": "Overview",
"viewPath": "TestCaseTwo.view"
},
"detail": {
"viewId": "detail",
"viewName": "Detail",
"viewPath": "TestCaseTwo.view"
}
}
}
}
}

我们在描述符的sap.ui5部分添加了一个新的“路由”部分。有三个子部分定义了应用程序的路由和导航结构:

  • config
    • 本节包含适用于所有路由和目标的全局路由器配置和默认值。我们定义要使用的路由器类,以及视图在应用程序中的位置。为了自动加载和显示视图,我们还指定了用于显示页面的控件,以及显示新页面时应填充的聚合。
  • routes
    • 每条路线都定义了一个名称、一种模式以及一个或多个目标,以便在路线被选中时导航到这些目标。模式基本上是与路由匹配的URL部分,我们为应用程序定义了两条路由。第一个是默认路由,它将显示包含前面步骤内容的概览页面,第二个是包含URL模式细节的详细路由,它将显示新页面。
  • targets
    • 目标定义了显示的视图,它与一条或多条路线关联,也可以从应用程序中手动显示。无论何时显示目标,相应的视图都会加载并显示在应用程序中。在我们的应用程序中,我们只需使用与目标名称对应的视图名称定义两个目标。
(2)初始化激 活路由

webapp/Component.js

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
sap.ui.define([
"sap/ui/core/UIComponent",
"sap/ui/Device",
"TestCaseTwo/model/models",
"sap/ui/model/json/JSONModel",
"sap/ui/model/resource/ResourceModel",
"./controller/HelloDialog"
], function(UIComponent, Device, models,JSONModel,ResourceModel,HelloDialog) {
"use strict";

return UIComponent.extend("TestCaseTwo.Component", {

metadata: {
manifest: "json"
},

/**
* The component is initialized by UI5 automatically during the startup of the app and calls the init method once.
* @public
* @override
*/
init: function() {
// call the base component's init function
UIComponent.prototype.init.apply(this, arguments);

// set the device model
this.setModel(models.createDeviceModel(), "device");

var oData = {
recipient : {
name : "World",
text : "ok"
}
};
var oModel = new JSONModel(oData);
this.setModel(oModel);

// set i18n model on view
var i18nModel = new ResourceModel({
bundleName:"TestCaseTwo.i18n.i18n"
});
this.setModel(i18nModel,"i18n");

// set dialog
this._helloDialog = new HelloDialog(this.getRootControl());

// 新增内容
// create the views based on the url/hash
this.getRouter().initialize();
// 新增内容
},
exit : function(){
this._helloDialog.destroy();
delete this._helloDialog;
},

openHelloDialog : function(){
this._helloDialog.open();
}
});
});

在组件初始化方法中,我们现在添加一个调用来初始化路由器。我们不需要手动实例化路由器,它会根据AppDescriptor配置自动实例化并分配给组件。

初始化路由器将评估当前URL并自动加载相应的视图。这是在AppDescriptor中配置的路由和目标的帮助下完成的。如果路线被选中,则加载并显示其相应目标的视图。

(3)路由入口视图

webapp/view/Overview.view.xml (New)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<mvc:View
controllerName="TestCaseTwo.controller.View1"
xmlns="sap.m"
xmlns:mvc="sap.ui.core.mvc">
<Page title="{i18n>homePageTitle}">
<headerContent>
<Button
icon="sap-icon://hello-world"
press=".onOpenDialog"/>
</headerContent>
<content>
<mvc:XMLView viewName="TestCaseTwo.view.HelloPanel"/>
<mvc:XMLView viewName="TestCaseTwo.view.InvoiceList"/>
</content>
</Page>
</mvc:View>

我们将前面步骤的内容从应用程序视图移动到新的概览视图。为简单起见,我们不更改控制器,因为它只包含打开对话框的助手方法,这意味着我们重用了控制器sap.ui.demo.walkthrough.controller.App用于两种不同视图的应用程序(用于新概述和应用程序视图)。然而,该控制器的两个实例在运行时被实例化。通常,每个引用控制器的视图都会实例化一个控制器实例。

(4)修改主视图

webapp/view/View1.view.xml

1
2
3
4
5
6
7
8
9
<mvc:View 
controllerName="TestCaseTwo.controller.View1"
xmlns:mvc="sap.ui.core.mvc"
displayBlock="true"
xmlns="sap.m">
<Shell>
<App class="myAppDemoWT" id="app"/>
</Shell>
</mvc:View>

我们的应用程序视图现在只包含空的应用程序标签。路由器将自动将与当前URL对应的视图添加到应用程序控件中。路由器使用与AppDescriptor中的属性controlId:“app”对应的ID标识应用程序控件。

(5)路由跳转视图

webapp/view/Detail.view.xml (New)

1
2
3
4
5
6
7
8
9
<mvc:View
xmlns="sap.m"
xmlns:mvc="sap.ui.core.mvc">
<Page
title="{i18n>detailPageTitle}">
<ObjectHeader
title="Invoice"/>
</Page>
</mvc:View>

现在,我们为详图视图添加第二个视图。它目前只包含一个页面和一个ObjectHeader控件,用于显示静态文本发票。

(6)设置I8n中的视图标题

webapp/i18n/i18n.properties

1
2
3
4
...

# Detail Page
detailPageTitle=Walkthrough - Details
(7)设置列表点击切换视图事件

webapp/view/InvoiceList.view.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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<mvc:View
controllerName="TestCaseTwo.controller.InvoiceList"
xmlns="sap.m"
xmlns:mvc="sap.ui.core.mvc">
<List
id="invoiceList"
headerText="{i18n>invoiceListTitle}"
class="sapUiResponsiveMargin"
width="auto"
items="{
path: 'invoice>/Invoices',
sorter: {
path: 'ShipperName',
group: true
}
}"
>
<headerToolbar>
<Toolbar>
<Title text="{i18n>invoiceListTitle}"/>
<ToolbarSpacer/>
<SearchField width="50%" search=".onFilterInvoices"/>
</Toolbar>
</headerToolbar>
<items>
<ObjectListItem
title="{invoice>Quantity} x {invoice>ProductName}"
number="{
parts:[{path: 'invoice>ExTendedPrice'},{path: 'view>/currency'}],
type: 'sap.ui.model.type.Currency',
formatOpentions: {
showMeasure: false
}
}"
numberUnit="{view>/currency}"
numberState="{= ${invoice>ExtendedPrice} > 50 ? 'Error' : 'Success' }"
<!--新增内容-->
type="Navigation"
press="onPress">
<!--新增内容-->

<firstStatus>
<ObjectStatus text="{
path: 'invoice>Status',
formatter: '.formatter.statusText'
}"/>
</firstStatus>
</ObjectListItem>
</items>
</List>
</mvc:View>

在“发票列表”视图中,我们向列表项添加一个按下事件,并将项类型设置为“导航”,以便可以实际单击该项。

(8)设置列表路由跳转逻辑

webapp/controller/InvoiceList.controller.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
sap.ui.define([
"sap/ui/core/mvc/Controller",
"sap/ui/model/json/JSONModel",
"./formatter",
"sap/ui/model/Filter",
"sap/ui/model/FilterOperator"
], function (Controller, JSONModel, formatter, Filter, FilterOperator) {
"use strict";

return Controller.extend("TestCaseTwo.controller.InvoiceList",{
...
// 新增内容
onPress: function (oEvent) {
var oRouter = this.getOwnerComponent().getRouter();
oRouter.navTo("detail");
}
// 新增内容
});

});

我们将事件处理函数添加到发票列表的控制器中。现在是时候通过单击发票列表中的项目导航到详细信息页面了。我们通过调用助手方法getOwnerComponent()来访问应用程序的路由器实例。getRouter()。在路由器上,我们调用navTo方法来导航到我们在路由配置中指定的详细路由。

现在,当您单击发票列表中的一个项目时,您应该会看到详细信息页面。

(9)效果图展示
知识点十六效果展示图 知识点十六效果展示图1
2.带参数的路由

我们现在可以在概览和详细信息页面之间导航,但我们在概览中选择的实际项目尚未显示在详细信息页面上。我们的应用程序的一个典型用例是在详细信息页面上显示所选项目的附加信息。

(1)设置路由传递的参数

webapp/manifest.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
30
31
32
33
34
35
36
37
38
39
40
41
{
"_version": "1.7.0",
...
"sap.ui5": {
...
"resources": {
...
},
"routing": {
"config": {
"routerClass": "sap.m.routing.Router",
"viewType": "XML",
"viewPath": "TestCaseTwo.view",
"controlId": "app",
"controlAggregation": "pages",
"async": true
},
"routes": [{
"pattern": "",
"name": "overview",
"target": "overview"
}, {
"pattern": "detail/{invoicePath}", // 新增内容
"name": "detail",
"target": "detail"
}],
"targets": {
"overview": {
"viewId": "overview",
"viewName": "Overview",
"viewPath": "TestCaseTwo.view"
},
"detail": {
"viewId": "detail",
"viewName": "Detail",
"viewPath": "TestCaseTwo.view"
}
}
}
}
}

现在,我们将导航参数invoicePath添加到详细信息路由,以便将所选项目的信息移交给详细信息页面。导航参数必须用花括号定义。

(2)配置视图显示的数据内容

webapp/view/Detail.view.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
<mvc:View
controllerName="TestCaseTwo.controller.Detail" // 新增内容
xmlns="sap.m"
xmlns:mvc="sap.ui.core.mvc">
<Page
title="{i18n>detailPageTitle}">
<ObjectHeader
// 新增内容
intro="{invoice>ShipperName}"
title="{invoice>ProductName}"/>
// 新增内容
</Page>
</mvc:View>

将路由传递过来的数据模型内容展示在页面上。我们添加了一个控制器,负责在视图上设置项目的上下文,并将ObjectHeader的一些属性绑定到发票模型的字段。

(3)配置传递的路由参数

webapp/controller/InvoiceList.controller.js

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
31
32
33
34
35
36
sap.ui.define([
"sap/ui/core/mvc/Controller",
"sap/ui/model/json/JSONModel",
"./formatter",
"sap/ui/model/Filter",
"sap/ui/model/FilterOperator"
], function (Controller, JSONModel, formatter, Filter, FilterOperator) {
"use strict";

return Controller.extend("TestCaseTwo.controller.InvoiceList",{
formatter: formatter,
onInit : function(){
...
},
onFilterInvoices : function(oEvent) {
...
},
onPress: function (oEvent) {
// 获取列表的Item对象
var oItem = oEvent.getSource();//谁触发了这个方法就是指谁
// 获取路由对象
var oRouter = this.getOwnerComponent().getRouter();
// 获取数据模型对象,上下文对象
var oModel = oItem.getBindingContext("invoice");
// 获取鼠标点击选中的数据模型路径
var oPath = oModel.getPath();
// 去除掉通过路径获取到的数据模型格式,主要是去掉路劲获取到数据前的‘/’字符
var oObject = oPath.substr(1);
oRouter.navTo("detail",{
// 将选中数据模型的路径传递过去
invoicePath: window.encodeURIComponent(oObject)
});
}
});

});
知识点十六传递路由参数的设置

oModel对象中的获取上下文对象的方法虽然此处使用的是列表,但是一般表格和列表一类用来判断被点击选中的数据时都是使用该方法获取上下文对象然后通过getPath()方法获取到被选中数据的索引。substr(1)方法是字符串处理方法,去除掉字符串的第一个字符(因为传递的参数是1,当传递的参数是2或3则会取出开头的前2个或3个字符)。

与之交互的控件实例可以通过适用于所有SAPUI5事件的getSource方法访问。它将返回在本例中已单击的ObjectListItem。我们将使用它将单击的项目的信息传递到详细信息页面,以便相同的项目可以显示在那里。

在navTo方法中,我们现在添加一个配置对象,用项目的当前信息填充导航参数invoicePath。这将更新URL,同时导航到详细信息视图。在详细信息页面上,我们可以再次访问此上下文信息并显示相应的项。

为了识别我们选择的对象,我们通常会在后端系统中使用该项的键,因为它简短而精确。然而,对于我们的发票项目,我们没有简单的密钥,直接使用绑定路径来保持示例简短。项的路径是绑定上下文的一部分,绑定上下文是SAPUI5的帮助对象,用于管理控件的绑定信息。可以通过在任何绑定的SAPUI5控件上使用模型名调用getBindingContext方法来访问绑定上下文。我们需要通过调用从绑定路径中删除第一个/。字符串上的substr(1),因为这是URL中的一个特殊字符,不允许使用,所以我们将在详细信息页面上再次添加它。

(4)创建跳转页面的控制器文件

webapp/controller/Detail.controller.js (New)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
sap.ui.define([
"sap/ui/core/mvc/Controller"
], function (Controller) {
"use strict";

return Controller.extend("sap.ui.demo.TestCaseTwo.controller.Detail", {
onInit: function () {
var oRouter = this.getOwnerComponent().getRouter();
oRouter.getRoute("detail").attachPatternMatched(this._onObjectMatched, this);
},
_onObjectMatched: function (oEvent) {
this.getView().bindElement({
path: "/" + window.decodeURIComponent(oEvent.getParameter("arguments").invoicePath),
model: "invoice"
});
}
});
});

最后一块拼图是细节控制器。它需要设置我们在视图上用URL参数invoicePath传入的上下文,以便实际显示发票列表中已选择的项目,否则,视图将保持为空。

在控制器的onInit方法中,我们获取应用程序路由的实例,并通过调用通过其名称访问的路由上的attachPatternMatched方法来附加到详细路由。我们注册了一个内部回调函数_onObjectMatched,它将在点击路由时执行,方法是单击该项目,或者使用详细信息页面的URL调用应用程序。

在路由器触发的_onObjectMatched方法中,我们接收到一个事件,可以用来访问URL和导航参数。arguments参数将返回一个与路由模式中的导航参数相对应的对象。我们访问在invoice list控制器中设置的invoicePath,并调用视图上的bindElement函数来设置上下文。我们必须再次在路径前面添加根/作为URL参数在路径上传递时删除的路径。

知识点十六接收路由参数与路径URL 知识点十六接收路由参数与路径

bindElement函数正在为SAPUI5控件创建绑定上下文,并接收模型名以及配置对象中某个项的路径。这将触发与发票模型字段连接的UI控件的更新。现在,当您单击发票列表中的项目时,您应该可以在单独的页面上看到发票详细信息。

3.路由回逆

现在,我们可以导航到详细信息页面并显示发票,但我们还不能返回概览页面。我们将在详细信息页面中添加一个后退按钮,并实现一个再次显示概览页面的功能。

(1)添加回逆按钮

webapp/view/Detail.view.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<mvc:View
controllerName="TestCaseTwo.controller.Detail"
xmlns="sap.m"
xmlns:mvc="sap.ui.core.mvc">
<Page
title="{i18n>detailPageTitle}"
<!--新增内容-->
showNavButton = "true"
navButtonPress = ".onNavBack">
<!--新增内容-->
<ObjectHeader
intro="{invoice>ShipperName}"
title="{invoice>ProductName}"/>
</Page>
</mvc:View>

在详细信息页面上,我们通过将参数showNavButton设置为true来告诉控件显示后退按钮,并注册一个事件处理程序,当按下后退按钮时调用该事件处理程序。

(2)编写回逆按钮逻辑

webapp/controller/Detail.controller.js

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
31
32
33
34
35
36
37
38
39
40
sap.ui.define([
"sap/ui/core/mvc/Controller",
"sap/ui/core/routing/History" // 新增内容
], function (Controller, History) {
"use strict";

return Controller.extend("sap.ui.demo.TestCaseTwo.controller.Detail", {
onInit: function () {
var oRouter = this.getOwnerComponent().getRouter();
// 设置上下文对象
oRouter.getRoute("detail").attachPatternMatched(this._onObjectMatched, this);
},
_onObjectMatched: function (oEvent) {
var oPath = oEvent.getParameter("arguments").invoicePath;
this.getView().bindElement({
// 获取的是URL路径上传递过来的数据模型路径
path: "/" + window.decodeURIComponent(oPath),
model: "invoice"
});
},
// 新增内容
onNavBack: function () {
// 获取历史记录
var oHistory = History.getInstance();
var sPreviousHash = oHistory.getPreviousHash();

if (sPreviousHash !== undefined) {
// 浏览器历史记录,-1代表上一个记录
window.history.go(-1);
} else {
// 若历史记录为空则设置跳转的路由ID为overview进行跳转
var oRouter = this.getOwnerComponent().getRouter();
// 三个参数的含义
// 路由跳转的视图ID 数组 用新的历史状态替换当前的历史状态
oRouter.navTo("overview", {} ,true);
}
}
// 新增内容
});
});

我们加载一个新的依赖项,帮助我们从sap管理导航历史记录。用户界面。果心路由命名空间,并将事件处理程序的实现添加到我们的detail page控制器。

在事件处理程序中,我们访问导航历史,并尝试确定上一个哈希。与浏览器历史记录不同,只有在应用程序中的导航步骤已经完成时,我们才能得到有效的结果。然后,我们将简单地使用浏览器历史记录返回上一页。如果之前没有导航,我们可以告诉路由器直接进入我们的概览页面。第三个参数true告诉路由器用新的历史状态替换当前的历史状态,因为我们实际上是自己进行反向导航的。第二个参数是一个空数组({}),因为我们不向这个路由传递任何额外的参数。

对于我们的用例,这个实现比浏览器的后退按钮要好一点。即使我们在应用程序之外的另一个页面上,浏览器也会简单地回到历史上的一步。在应用程序中,我们总是想回到概览页面,即使我们来自另一个链接,或者直接用书签打开了详细信息页面。你可以直接在一个新选项卡中加载详细信息页面,然后点击应用程序中的后退按钮,它仍然会返回到概览页面。

(3)效果图展示
知识点十六路由回逆效果展示图

十七、自定义控件

在这一步中,我们将使用自定义控件扩展SAPUI5的功能。我们希望对细节页面上显示的产品进行评分,因此我们使用SAPUI5扩展机制创建了多个标准控件的组合,并添加了一些粘合代码,使它们能够很好地协同工作。这样,我们可以在整个应用程序中重用控件,并将所有相关功能保留在一个模块中。

(1)创建控件对象文件

webapp/control/ProductRating.js (New)

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
sap.ui.define([
"sap/ui/core/Control",
"sap/m/RatingIndicator",
"sap/m/Label",
"sap/m/Button"
], function (Control, RatingIndicator, Label, Button) {
"use strict";
return Control.extend("sap.ui.demo.TestCaseTwo.control.ProductRating", {
// 定义组件属性
metadata: {
properties: {
value: {type: "float", defaultValue: 0}
},
aggregations: {
_rating: {type: "sap.m.RatingIndicator", multiple: false, visibility: "hidden"},
_label: {type: "sap.m.Label", multiple: false, visibility: "hidden"},
_button: {type: "sap.m.Button", multiple: false,visibility: "hidden"}
},
events: {
change: {
parameters: {
value: {type : "int"}
}
}
}
},

init: function () {
this.setAggregation("_rating", new RatingIndicator({
value: this.getValue(),
iconSize: "2rem",
visualMode: "Half",
liveChange: this._onRate.bind(this)
}));

this.setAggregation("_label",new Label({
text: "{i18n>productRatingLabelInitial}"
}).addStyleClass("sapUiSmallMargin"));

this.setAggregation("_button", new Button({
text: "{i18n>productRatingButton}",
press: this._onSubmit.bind(this)
}).addStyleClass("sapUiTinyMarginTopBottom"));
},

setValue: function (fValue) {
this.setProperty("value", fValue, true);
this.getAggregation("_rating").setValue(fValue);
},


reset: function () {
var oResourceBundle = this.getModel("i18n").getResourceBundle();
this.setValue(0);
this.getAggregation("_label").setDesign("Standard");
this.getAggregation("_rating").setEnabled(true);
this.getAggregation("_label").setText(oResourceBundle.getText("productRatingLabelInitial"));
this.getAggregation("_button").setEnabled(true);
},

_onRate: function (oEvent) {
var oResourceBundle = this.getModel("i18n").getResourceBundle();
var fValue = oEvent.getParameter("value");

this.setProperty("value", fValue, true);

this.getAggregation("_label").setText(oResourceBundle.getText("productRatingLabelIndicator", [fValue, oEvent.getSource().getMaxValue()]));
this.getAggregation("_label").setDesign("Bold");
},

_onSubmit: function (oEvent) {
var oResourceBundle = this.getModel("i18n").getResourceBundle();

this.getAggregation("_rating").setEnabled(false);
this.getAggregation("_label").setText(oResourceBundle.getText("productRatingLabelFinal"));
this.getAggregation("_button").setEnabled(false);
this.fireEvent("change", {
value: this.getValue()
});
},

renderer: function (oRm, oControl) {
oRm.openStart("div", oControl);
oRm.class("myAppDemoWTProductRating");
oRm.openEnd();
oRm.renderControl(oControl.getAggregation("_rating"));
oRm.renderControl(oControl.getAggregation("_label"));
oRm.renderControl(oControl.getAggregation("_button"));
oRm.close("div");
}
});
});

创建一个控件的JS的文件。与我们的控制器和视图一样,自定义控件从SAPUI5基对象继承公共控件功能,对于控件,这是通过扩展基类sap.ui.core.Control.来完成的。

自定义控件是可以在应用程序中轻松创建的小型重用组件。由于其性质,它们有时也被称为“notepad”或“on the fly”控件。自定义控件是一个JavaScript对象,它有两个特殊部分(元数据和渲染器)和许多实现控件功能的方法。

元数据部分定义了数据结构,从而定义了控件的API。有了这些关于控件属性、事件和聚合的元信息,SAPUI5自动创建setter和getter方法以及其他可以在应用程序中调用的方便函数。

渲染器(css)定义HTML结构,每当控件在视图中实例化时,该结构将添加到应用程序的DOM树中。它通常最初由SAPUI5的核心调用,并且每当控件的属性发生更改时调用。render函数的参数oRM是SAPUI5 render manager,可用于将字符串和控件属性写入HTML页面。

init方法是一个特殊的函数,每当实例化控件时,SAPUI5内核都会调用它。它可用于设置控件并准备显示其内容。

现在,我们用所需的自定义功能增强了新的自定义控件。在我们的例子中,我们希望创建一个交互式产品评级,因此我们定义了一个值,并使用三个内部控件,这些控件会自动显示并更新。RatingIndicator控件用于收集用户对产品的输入,标签显示更多信息,按钮将评级提交给应用程序以存储。

因此,在元数据部分,我们定义了几个在实现中使用的属性:

  • 属性(Properties)

    • 值(value)

      我们定义了一个控件属性值,它将保存用户在评级中选择的值。该属性的Getter和setter函数将自动创建,如果愿意,我们还可以将其绑定到XML视图中的数据模型字段。

  • 聚合(Aggregations)

    如第一段所述,我们需要三个内部控制来实现我们的评级功能。因此,我们通过将visibility属性设置为hidden来创建三个“隐藏聚合”。通过这种方式,我们可以使用视图中设置的模型以及内部控件,SAPUI5将负责生命周期管理,并在不再需要控件时销毁它们。聚合也可以用来保存控件数组,但我们只希望每个聚合中有一个控件,所以我们需要通过将属性multiple设置为false来调整基数。

    • _rating:用于用户输入的sap.m.RatingIndicator 控件
    • _label:显示附加信息sap.m.Label。
    • _button:提交评级的sap.m.button。
  • 事件(Events)

    • 改变(Change)

      我们指定了一个更改事件,控件在提交评级时将触发该事件。它包含作为事件参数的当前值。应用程序可以注册到此事件,并处理类似于“常规”SAPUI5控件的结果,实际上,SAPUI5控件的构建类似于自定义控件。

每当一个新的控件实例被实例化时,SAPUI5就会自动调用init函数,在该函数中,我们设置了内部控件。我们实例化这三个控件,并通过调用从sap.ui.core.Control.继承的框架方法setAggregation将它们存储在内部聚合中。我们传递上面指定的内部聚合和新控件实例的名称。并指定一些控件属性,以使自定义控件看起来更好,并将liveChange事件注册到rating,将press事件注册到button。标签和按钮的初始文本来自我们的i18n模型。

每当一个新的控件实例被实例化时,SAPUI5就会自动调用init函数,在该函数中,我们设置了内部控件。我们实例化这三个控件,并通过调用从sap继承的框架方法setAggregation将它们存储在内部聚合中。用户界面。果心控制我们传递上面指定的内部聚合和新控件实例的名称。我们指定了一些控件属性,以使自定义控件看起来更好,并将liveChange事件注册到rating,将press事件注册到button。标签和按钮的初始文本来自我们的i18n型号。

让我们暂时忽略其他internal helper函数和事件处理程序,定义渲染器。在SAPUI5呈现管理器和作为引用传递的控件实例的帮助下,我们现在可以呈现控件的HTML结构。我们将外部

标记的开头呈现为<div,并调用helper方法writeControlData来呈现div标记内控件的ID和其他基本属性。接下来,我们添加一个自定义CSS类,以便稍后在CSS文件中为自定义控件定义样式规则。然后,通过调用renderer实例上的WriteClass来呈现视图中添加的这个CSS类和其他CSS类。然后,我们关闭周围的div标记,并通过将内部聚合的内容传递给render managers renderControl函数来呈现三个内部控件。这将调用控件的呈现程序,并将它们的HTML添加到页面中。最后,我们关闭周围的
标签。

setValue是一个重写的setter。SAPUI5将生成一个setter,在控制器中调用或在XML视图中定义时更新属性值,但我们还需要更新隐藏聚合中的内部评级控制,以正确反映状态。此外,我们还可以通过调用setProperty方法以true作为第三个参数来更新控件属性,从而跳过SAPUI5的重新排序,SAPUI5通常在控件的属性更改时触发。

现在我们为内部评级控制定义事件处理程序。每次用户更改评级时都会调用它。评级控制的当前值可以从sap.m.RatingIndicator的事件参数值中读取。使用我们调用重写设置器的值来更新控件状态,然后更新评级旁边的标签,以显示用户当前选择的值,并显示最大值。带有占位符值的字符串从自动分配给控件的i18n模型中读取。

接下来,我们有了提交评级的评级按钮的按下处理程序。我们假设对产品进行评级是一次性的,首先禁用评级和按钮,这样用户就不允许提交另一个评级。我们还更新了标签,以显示“感谢您的评分!”消息,然后我们触发控件的更改事件,并将当前值作为参数传递,以便侦听该事件的应用程序可以对评级交互做出反应。

我们定义了重置方法,以便能够将UI上控件的状态恢复到其初始状态,以便用户可以再次提交评级。

(2)添加自定义控件到视图

webapp/view/Detail.view.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<mvc:View
controllerName="TestCaseTwo.controller.Detail"
xmlns="sap.m"
xmlns:mvc="sap.ui.core.mvc"
xmlns:wt="TestCaseTwo.controller"> <!--新增内容-->
<Page
title="{i18n>detailPageTitle}"
showNavButton = "true"
navButtonPress = ".onNavBack">
<ObjectHeader
intro="{invoice>ShipperName}"
title="{invoice>ProductName}"/>
<!--新增内容-->
<wt:ProductRating id="rating" class="sapUiSmallMarginBeginEnd" change=".onRatingChange"/>
<!--新增内容-->
</Page>
</mvc:View>

在detail视图上定义了一个新的名称空间wt,以便我们可以在视图中轻松地引用自定义控件。然后,我们将ProductRating控件的一个实例添加到详细信息页面,并为更改事件注册一个事件处理程序。为了有一个合适的布局,我们还添加了一个边距样式类。

(3)设置新添加视图中的按钮逻辑

webapp/controller/Detail.controller.js

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
sap.ui.define([
"sap/ui/core/mvc/Controller",
"sap/ui/core/routing/History",
"sap/m/MessageToast" // 新增内容
], function (Controller, History, MessageToast) {
"use strict";

return Controller.extend("sap.ui.demo.TestCaseTwo.controller.Detail", {
...
_onObjectMatched: function (oEvent) {
this.byId("rating").reset(); // 新增内容
this.getView().bindElement({
// 获取的是URL路径上传递过来的数据模型路径
path: "/" + window.decodeURIComponent(oEvent.getParameter("arguments").invoicePath),
model: "invoice"
});
},
...
// 新增内容
onRatingChange: function (oEvent) {
var fValue = oEvent.getParameter("value");
var oResourceBundle = this.getView().getModel("i18n").getResourceBundle();

MessageToast.show(oResourceBundle.getText("ratingConfirmation", [fValue]));
}
// 新增内容
});
});

在Detail controller中,我们将依赖项加载到sap.m. MessageToast,因为我们只显示一条消息,而不是将评级发送到后端,以保持示例的简单性。事件处理程序onRatingChange读取提交评级时触发的自定义更改事件的值。然后,我们在MessageToast控件中显示一条带有该值的确认消息。

在OnObject Matched private方法中,我们调用重置方法,以便在显示不同项目的详细信息视图时,可以提交另一个评级。

(4)设置CSS渲染器

webapp/css/style.css

1
2
3
4
5
6
7
8
9
10
/* Enter your custom styles here */
...

/* ProductRating */
.myAppDemoWTProductRating {
padding: 0.75rem;
}
.myAppDemoWTProductRating .sapMRI {
vertical-align: initial;
}

为了布局我们的控件,我们在根类中添加了一点填充,以便在三个内部控件周围有一些空间,我们重写RatingIndicator控件的对齐方式,以便它与标签和按钮对齐。

我们也可以在渲染器中使用更多HTML来实现这一点,但这是最简单的方法,它只会应用于我们的自定义控件中。但是,请注意,自定义控件在您的应用程序中,可能需要在SAPUI5的未来版本中更改内部控件时进行调整。

(5)设置响应文本内容

webapp/i18n/i18n.properties

1
2
3
4
5
6
7
8
9
10

# Detail Page
detailPageTitle=Walkthrough - Details
ratingConfirmation=You have rated this product with {0} stars

# Product Rating
productRatingLabelInitial=Please rate this product
productRatingLabelIndicator=Your rating: {0} out of {1}
productRatingLabelFinal=Thank you for your rating!
productRatingButton=Rate

资源包通过确认消息和我们在自定义控件中引用的字符串进行扩展。现在,我们可以使用全新的控件在详细信息页面上对产品进行评级。

(6)效果图展示
知识点十七效果展示图

十八、响应性

在这一步中,我们提高了应用程序的响应能力。SAPUI5应用程序可以在手机、平板电脑和桌面设备上运行,我们可以对应用程序进行配置,以便在每个场景中充分利用屏幕状态。幸运的是,SAPUI5可以像sap.m.Table一样控制。已经提供了很多我们可以使用的功能。

(1)修改列表视图

webapp/view/InvoiceList.view.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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
<mvc:View
controllerName="TestCaseTwo.controller.InvoiceList"
xmlns="sap.m"
xmlns:mvc="sap.ui.core.mvc">
<Table <!--新增内容-->
id="invoiceList"
class="sapUiResponsiveMargin"
width="auto"
items="{
path: 'invoice>/Invoices',
sorter: {
path: 'ShipperName',
group: true
}
}"
>
<headerToolbar>
<Toolbar>
<Title text="{i18n>invoiceListTitle}"/>
<ToolbarSpacer/>
<SearchField width="50%" search=".onFilterInvoices"/>
</Toolbar>
</headerToolbar>
<!--新增内容-->
<columns>
<Column
hAlign="End"
minScreenWidth="Small"
demandPopin="true"
width="5em">
<Text text="{i18n>columnQuantity}"/>
</Column>
<Column>
<Text text="{i18n>columnName}"/>
</Column>
<Column
minScreenWidth="Small"
demandPopin="true">
<Text text="{i18n>columnStatus}"/>
</Column>
<Column
minScreenWidth="Tablet"
demandPopin="false">
<Text text="{i18n>columnSupplier}"/>
</Column>
<Column
hAlign="End">
<Text text="{i18n>columnPrice}"/>
</Column>
</columns>
<items>
<ColumnListItem
type="Navigation"
press=".onPress">
<cells>
<ObjectNumber number="{invoice>Quantity}" emphasized="false"/>
<ObjectIdentifier title="{invoice>ProductName}"/>
<Text text="{
path: 'invoice>Status',
formatter: '.formatter.statusText'
}"/>
<Text text="{invoice>ShipperName}"/>
<ObjectNumber
number="{
parts: [{path: 'invoice>ExtendedPrice'}, {path: 'view>/currency'}],
type: 'sap.ui.model.type.Currency',
formatOptions: {
showMeasure: false
}
}"
unit="{view>/currency}"
state="{= ${invoice>ExtendedPrice} > 50 ? 'Error' : 'Success' }"/>
</cells>
</ColumnListItem>
</items>
</Table>
<!--新增内容-->
</mvc:View>

我们只需将标签替换为

即可将列表与表交换。该表具有内置的响应功能,允许我们使应用程序更加灵活。表和列表共享同一组属性,因此我们可以简单地重用这些属性和分类器。

由于一个表的每行有多个单元格,我们必须为表定义列,并根据数据命名这些列。我们添加了五个 sap.m.Column 控件添加到列聚合中,并对每个控件进行稍微不同的配置:

  • Quantity
    此列将包含一个短数字,因此我们将对齐设置为End(在LTR语言中是“right”的意思),并将宽度设置为5em,这对于列描述来说足够长。作为描述文本,我们使用sap.m.Text引用资源束属性的控件。我们将属性minScreenWidth设置为Small,以表明此列在手机上不太重要。通过将属性demandPopin设置为true,我们将告诉表在主列下方显示此列。

  • Name

    我们的主栏有一个相当大的宽度来显示所有的细节。它将始终显示出来。

  • Status

    状态并不是那么重要,所以我们还可以通过将minScreenWidth设置为small,将demandPopin设置为true,在小屏幕上的name字段下方显示状态

  • Supplier

    我们将minScreenWidth设置为Tablet,demandPopin设置为false,从而完全隐藏手机设备上的供应商栏。

  • Price

    此列始终可见,因为它包含我们的发票价格。

我们不再使用之前的ObjectListItem,而是将信息拆分到与上面定义的列匹配的单元格中。因此,我们将其更改为具有相同属性的ColumnListItem控件,但现在使用cells聚合。在这里,我们创建五个控件来显示数据:

  • Quantity

    一个简单的sap.m.ObjectNumber控件,绑定到我们的数据字段。

  • Name

    指定名称的sap.m.ObjectIdentifier控件。

  • Status

    使用与以前相同的格式化程序的sap.m.Text控件。

  • Supplier

    一个简单的sap.m.Text控件。

  • Price

    一个ObjectNumber控件,其格式与前面步骤中的属性number和NumberRunIt相同。

(2)设置列名文本

webapp/i18n/i18n.properties

1
2
3
4
5
6
7
8
9
10
11
12
13
14
...
# Invoice List
invoiceListTitle=Invoices
invoiceStatusA=New
invoiceStatusB=In Progress
invoiceStatusC=Done
columnQuantity=Quantity
columnName=Name
columnSupplier=Supplier
columnStatus=Status
columnPrice=Price

# Detail Page
...

我们将列名和属性标题添加到i18n文件中。

当我们缩小浏览器的屏幕大小或在小型设备上打开应用程序时,我们可以看到结果。

(3)效果图展示
知识点十八效果展示图2 知识点十八效果展示图1

十九、设备适配

接下来设置页面的设备适配,页面会根据不同的设备进行相应的响应。

(1)设置可扩展属性

webapp/view/HelloPanel.view.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
25
26
27
28
<mvc:View 
controllerName="TestCaseTwo.controller.HelloPanel"
xmlns:mvc="sap.ui.core.mvc"
displayBlock="true"
xmlns="sap.m">
<Panel headerText="{i18n>helloPanelTitle}" class="sapUiResponsiveMargin" width="auto"
expandable="{device>/system/phone}" expanded="{= !${device>/system/phone} }"> <!--新增内容-->
<content>
<Button
id="helloDialogButton"
icon="sap-icon://world"
text="{i18n>openDialogButtonText}"
press="onOpenDialog"
class="sapUiSmallMarginEnd sapUiVisibleOnlyOnDesktop"/> <!--新增内容-->
<Button
text="{i18n>showHelloButtonText}"
press="onWhowHello"
class="myCustomButton"/>
<Input
value="{/recipient/name}"
valueLiveUpdate="true"
width="60%"/>
<FormattedText
htmlText="Hello {/recipient/name}"
class="sapUiSmallMargin sapThemeHighlight-asColor myCustomText"/>
</content>
</Panel>
</mvc:View>

在HelloPanel中添加了两个可以扩展的新属性,用户可以在手机等小屏幕设备上浏览网页。可扩展属性绑定到名为device的模型和path /system/phone。因此,该页面只能在手机设备上扩展。设备模型中填充了SAPUI5的sap.ui.Device 的API。扩展属性控制面板的状态,我们使用表达式绑定语法在电话设备上关闭面板,并在所有其他设备上扩展面板。

当我们设置像sapUiVisibleOnlyOnDesktop或sapUiHideOnDesktop这样的CSS类时,我们也可以按设备类型隐藏单个控件。我们只显示在桌面设备上打开对话框的按钮,而在其他设备上隐藏对话框。

sap.ui.Device API根据用户代理和设备的许多其他属性检测设备类型(电话、平板电脑、桌面)。因此,简单地减小屏幕大小不会改变设备类型。要测试此功能,您必须在浏览器中启用设备模拟或在真实设备上打开它。

(2)在程序组件中添加依赖项

webapp/Component.js

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
sap.ui.define([
"sap/ui/core/UIComponent",
"sap/ui/Device",
"sap/ui/model/json/JSONModel",
"sap/ui/model/resource/ResourceModel",
"./controller/HelloDialog" // 新增内容
], function(UIComponent, Device, JSONModel, ResourceModel, HelloDialog) {
"use strict";

return UIComponent.extend("TestCaseTwo.Component", {

metadata: {
manifest: "json"
},

/**
* The component is initialized by UI5 automatically during the startup of the app and calls the init method once.
* @public
* @override
*/
init: function() {
// call the base component's init function
UIComponent.prototype.init.apply(this, arguments);

var oData = {
recipient : {
name : "World",
text : "ok"
}
};
var oModel = new JSONModel(oData);
this.setModel(oModel);

// set i18n model on view
var i18nModel = new ResourceModel({
bundleName:"TestCaseTwo.i18n.i18n"
});
this.setModel(i18nModel,"i18n");

// 新增内容
// set device model
var oDeviceModel = new JSONModel(Device);
oDeviceModel.setDefaultBindingMode("OneWay");
this.setModel(oDeviceModel, "device");
// 新增内容

// set dialog
this._helloDialog = new HelloDialog(this.getRootControl());
// create the views based on the url/hash
this.getRouter().initialize();
},
exit : function(){
this._helloDialog.destroy();
delete this._helloDialog;
},

openHelloDialog : function(){
this._helloDialog.open();
}
});
});

在应用程序组件中,我们向BBB添加一个依赖项,并在init方法中初始化设备模型。我们可以简单地将加载的依赖项设备传递给JSONModel的构造函数。这将使SAPUI5设备API的大多数属性作为JSON模型提供。然后将该模型作为命名模型设置在组件上,以便我们可以在数据绑定中引用它,正如我们在上面的视图中看到的那样。

我们必须将绑定模式设置为单向,因为设备模型是只读的,并且我们希望避免在将控件的属性绑定到它时意外更改模型。默认情况下,SAPUI5中的模型是双向的(双向的)。属性更改时,绑定模型值也会更新。

(3)设置详情页面表格

webapp/view/Detail.view.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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<mvc:View
controllerName="TestCaseTwo.controller.Detail"
xmlns="sap.m"
xmlns:mvc="sap.ui.core.mvc"
xmlns:wt="TestCaseTwo.controller">
<Page
title="{i18n>detailPageTitle}"
showNavButton = "true"
navButtonPress = ".onNavBack">
<ObjectHeader
<!--新增内容-->
responsive="true"
fullScreenOptimized="true"
number="{
parts: [{path: 'invoice>ExtendedPrice'}, {path: 'view>/currency'}],
type: 'sap.ui.model.type.Currency',
formatOptions: {
showMeasure: false
}
}"
numberUnit="{view>/currency}"
<!--新增内容-->
intro="{invoice>ShipperName}"
title="{invoice>ProductName}">
<!--新增内容-->
<attributes>
<ObjectAttribute title="{i18n>quantityTitle}" text="{invoice>Quantity}"></ObjectAttribute>
<ObjectAttribute title="{i18n>dateTitle}" text="{
path: 'invoice>ShippedDate',
type: 'sap.ui.model.type.Date',
formatOptions: {
style: 'long',
source: {
pattern: 'yyyy-MM-ddTHH:mm:ss'
}
}
}"/>
</attributes>
<!--新增内容-->
</ObjectHeader>
<wt:ProductRating id="rating" class="sapUiSmallMarginBeginEnd" change=".onRatingChange"/>
</Page>
</mvc:View>

一些控件已经具有可配置的内置响应功能。ObjectHeader控件可以设置为更灵活的模式,方法是将属性responsive设置为true和fullScreenOptimized设置为true。这将根据设备大小在屏幕上的不同位置显示我们现在添加到视图中的数据。

我们还将前面步骤列表中的number和numberUnit字段添加到ObjectHeader中,并使用与前面步骤中相同的货币类型格式化程序。然后我们定义两个属性:发票数量和发货日期,这是数据模型的一部分。到目前为止,我们还没有使用发票JSON文件中的shippedDate字段,它包含一个典型字符串格式的日期。

我们现在使用日期类型,并在格式选项的源部分提供日期格式的模式。它将显示更具可读性的格式化日期文本,也适用于小屏幕设备。

(4)设置详情页面的单位数据模型

webapp/controller/Detail.controller.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
sap.ui.define([
"sap/ui/core/mvc/Controller",
"sap/ui/core/routing/History",
"sap/m/MessageToast",
"sap/ui/model/json/JSONModel" // 新增内容
], function (Controller, History, MessageToast, JSONModel) {
"use strict";

return Controller.extend("sap.ui.demo.TestCaseTwo.controller.Detail", {
onInit: function () {
// 新增内容
var oViewModel = new JSONModel({
currency: "EUR"
});
this.getView().setModel(oViewModel, "view");
// 新增内容

var oRouter = this.getOwnerComponent().getRouter();
// 设置上下文对象
oRouter.getRoute("detail").attachPatternMatched(this._onObjectMatched, this);
},
...
});
});

因为在详情页面有单位的内容需要显示,但是没并没有单位的数据,所以需要设置一个单位并使用数据模型传递过去。

(5)设置文本内容

webapp/i18n/i18n.properties

1
2
3
4
# Detail Page
...
dateTitle=Order date
quantityTitle=Quantity

因为无法测试,所以本案例不设置效果图

二十、内容密度

在本演练教程的这一步中,我们将根据用户的设备调整内容密度。SAPUI5包含不同的内容密度,允许您为支持触摸的设备显示更大的控件,并为通过鼠标操作的设备显示更小、更紧凑的设计。在我们的应用程序中,我们将检测设备并相应地调整密度。

(1)设置内容密度助手方法

webapp/Component.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
...
init: function () {
... },
...
getContentDensityClass : function () {
if (!this._sContentDensityClass) {
if (!Device.support.touch) {
this._sContentDensityClass = "sapUiSizeCompact";
} else {
this._sContentDensityClass = "sapUiSizeCozy";
}
}
return this._sContentDensityClass;
}

});
});

为了准备内容密度特性,我们还将添加一个助手方法getContentDensityClass。SAPUI5控件可以以多种尺寸显示,例如,以适合桌面和非触摸设备的紧凑尺寸显示,以及以适合触摸交互的舒适模式显示。这些控件在应用程序的HTML结构中查找特定的CSS类,以调整其大小。

此助手方法直接查询设备API以获得客户端的触摸支持,如果不支持触摸交互,则返回CSS类sapUiSizeCompact,对于所有其他情况,则返回sapUiSizeCozy。我们将在整个应用程序编码过程中使用它来设置适当的内容密度CSS类。

(2)设置视图上相应的样式类

webapp/controller/View1.controller.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
sap.ui.define([
"sap/ui/core/mvc/Controller"
], function(Controller) {
"use strict";

return Controller.extend("TestCaseTwo.controller.View1", {
// 新增内容
onInit : function () {
this.getView().addStyleClass(this.getOwnerComponent().getContentDensityClass());
},
// 新增内容
onOpenDialog : function () {
this.getOwnerComponent().openHelloDialog();
}
});
});

我们在app控制器上添加了一个方法onInit,在实例化app视图时调用该方法。在这里,我们查询应用程序组件上定义的helper函数,以在应用程序视图上设置相应的样式类,应用程序视图中的所有控件现在将自动调整为样式定义的紧凑或舒适大小。

(3)同步对话框的样式类与应用程序一致

webapp/controller/HelloDialog.js

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
sap.ui.define([
"sap/ui/base/ManagedObject",
"sap/ui/core/Fragment",
"sap/ui/core/syncStyleClass" // 新增内容
], function (ManagedObject,Fragment,syncStyleClass) {
"use strict";

return ManagedObject.extend("TestCaseTwo.controller.HelloDialog",{
constructor : function(oView){
this._oView = oView;
},

exit : function() {
delete this._oView;
},

open : function() {
var oView = this._oView;

// create dialog lazily
if (!this.pDialog){
var oFragmentController = {
onCloseDialog : function() {
oView.byId("helloDialog").close();
}
};
// load asynchronous XML fragment
this.pDialog = Fragment.load({
id : oView.getId(),
name : "TestCaseTwo.view.HelloDialog",
controller : oFragmentController
}).then(function (oDialog) {
// connect dialog to the root view of this component (models,lifecycle)
oView.addDependent(oDialog);
// 新增内容
// forward compact/cozy style into dialog
syncStyleClass(oView.getController().getOwnerComponent().getContentDensityClass(), oView, oDialog);
// 新增内容
return oDialog;
});
}
this.pDialog.then(function(oDialog) {
oDialog.open();
});
}
});
});

“Hello World”对话框不是应用程序视图的一部分,而是在DOM中名为“静态区域”的特殊部分打开的。对话框不知道应用程序视图中定义的内容密度类,因此我们手动将应用程序的样式类与对话框同步。

(4)设置应用程序的支持模式

webapp/manifest.json

1
2
3
4
5
6
7
8
9
10
11
12
...
"sap.ui5": {
...
"dependencies": {
...
},
"contentDensities": {
"compact": true,
"cozy": true
}

}

在sap的内容密度部分。在ui5命名空间中,我们指定应用程序支持的模式。SAP Fiori launchpad等容器允许根据这些设置切换内容密度。\

由于我们刚刚根据设备功能启用了应用程序以两种模式运行,因此我们可以在应用程序描述符中将这两种模式都设置为true。

二十一、可达性

提高应用程序的可访问性。

为了实现这一点,我们将添加ARIA属性。屏幕阅读器使用ARIA属性来识别应用程序结构并正确解释UI元素。通过这种方式,我们可以让我们的应用程序更容易被那些使用电脑受到限制的用户访问, for example visually impaired persons。这里的主要目标是让我们的应用程序能为尽可能多的人使用。

(1)设置ARIA角色和标签

webapp/view/Overview.view.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
25
26
27
<mvc:View
controllerName="TestCaseTwo.controller.View1"
xmlns="sap.m"
xmlns:mvc="sap.ui.core.mvc">
<Page title="{i18n>homePageTitle}">
<!--新增内容-->
<landmarkInfo>
<PageAccessibleLandmarkInfo
rootRole="Region"
rootLabel="{i18n>Overview_rootLabel}"
contentRole="Main"
contentLabel="{i18n>Overview_contentLabel}"
headerRole="Banner"
headerLabel="{i18n>Overview_headerLabel}"/>
</landmarkInfo>
<!--新增内容-->
<headerContent>
<Button
icon="sap-icon://hello-world"
press=".onOpenDialog"/>
</headerContent>
<content>
<mvc:XMLView viewName="TestCaseTwo.view.HelloPanel"/>
<mvc:XMLView viewName="TestCaseTwo.view.InvoiceList"/>
</content>
</Page>
</mvc:View>

我们使用sap.m.PageAccessibleLandmarkInfo为概览页面区域定义ARIA角色和标签。有关更多信息,请参阅API参考:sap。m、 PageAccessibleLandmarkInfo。

(2)设置工具栏标题

webapp/view/InvoiceList.view.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
25
26
27
28
29
30
31
32
33
34
35
36
<mvc:View
controllerName="TestCaseTwo.controller.InvoiceList" xmlns="sap.m" xmlns:mvc="sap.ui.core.mvc">
<Panel accessibleRole="Region">
<headerToolbar>
<Toolbar>
<Title text="{i18n>invoiceListTitle}"/>
<ToolbarSpacer/>
<SearchField
width="50%"
search=".onFilterInvoices"
ariaLabelledBy="searchFieldLabel"
ariaDescribedBy="searchFieldDescription"
placeholder="{i18n>searchFieldPlaceholder}"/>
</Toolbar>
</headerToolbar>
<Table
id="invoiceList"
class="sapUiResponsiveMargin"
width="auto"
items="{
path : 'invoice>/Invoices',
sorter : {
path : 'ShipperName',
group : true
}
}">
<columns>
<Column
hAlign="End"



</columns>
</Table>
</Panel>
</mvc:View>

我们在发票列表周围添加了一个sap.m.Panel,并将工具栏从表中移动到面板中,以便该区域可以将工具栏的标题作为自己的标题。这意味着它现在将成为我们地标性建筑中的一个区域。

webapp/view/HelloPanel.view.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<mvc:View
controllerName="TestCaseTwo.controller.HelloPanel"
xmlns:mvc="sap.ui.core.mvc"
displayBlock="true"
xmlns="sap.m">
<Panel
headerText="{i18n>helloPanelTitle}"
class="sapUiResponsiveMargin"
width="auto"
expandable="{device>/system/phone}"
expanded="{= !${device>/system/phone} }"
accessibleRole="Region">

</Panel>
</mvc:View>

在这个视图中,我们已经有了一个面板,所以我们只需添加accessibleRole属性。

(3)设置文本内容

webapp/i18n/i18n.properties

1
2
3
4
5
6
7
...
#Overview Page
Overview_rootLabel=Overview Page
Overview_headerLabel=Header
Overview_contentLabel=Page Content
ratingTitle=Rate the Product
...

在这里,我们将评级面板标题的文本和ARIA区域的标签添加到文本包中。

评论