我们在Visual Studio Code 里通过 Guided Procedure,可以给 Fiori Elements 框架生成的 List Report 里的 Table,添加自定义按钮,如下图 Jerry 的按钮
所示。
但实际工作中,有朋友反映,在 Fiori Elements 的 Guided Procedure 中通过向导,一路 Next Next,对于开发人员来说就是个黑盒子。虽然实现了需求,但不知道背后是怎么工作的,觉得一切很不踏实。
本文就来深入介绍 Fiori Elements 里 Smart Table 控件的工作原理。
我们知道 Fiori Elements List Report 的模板,包含了 SmartTable.fragment.xml 这个页面片段:
而该页面片段的源代码里,使用了 Smart Table 控件:
XML 视图的完整源代码:
xml
<mvc:View
xmlns=".m"
xmlns:mvc=".ui.core.mvc"
controllerName=".ui.demo.smartControls.SmartTable"
xmlns:smartTable=".ui.comp.smarttable">
<smartTable:SmartTable
id="smartTable_ResponsiveTable"
tableType="ResponsiveTable"
editable="false"
entitySet="Products"
header="Jerry的产品"
showRowCount="true"
useExportToExcel="false"
enableAutoBinding="true">
</smartTable:SmartTable>
</mvc:View>
XML 视图里定义了一个 Smart Table 控件,第 10 行代码 entitySet="Products", 意思是让该控件,在运行时"智能地" 将名称为 Products 的 OData 模型里,所有符合某种条件的字段,渲染成表格列项目。
这个包含了 Smart Table 控件的 UI5 应用,最终渲染成包含如下三列的表格:产品 ID,价格 (含金额和货币单位) 以及产品名称。
我们打开 metadata.xml, 找到了 Product 模型包含的四个属性字段。这四个属性字段,都作为最后渲染出的列项目的备选字段。其中 Price 字段,通过属性 :unit, 和 CurrencyCode 字段关联起来,作为同一个表格备选列项目。
metadata.xml 的源代码:
xml
<?xml version="1.0" encoding="utf-8"?>
<edmx:Edmx Version="1.0"
xmlns:edmx="http://schemas.microsoft.com/ado/2007/06/edmx"
xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata"
xmlns:="http://www..com/Protocols/Data">
<edmx:DataServices m:DataServiceVersion="2.0">
<Schema Namespace="com..wt05"
:schema-version="1" xmlns="http://schemas.microsoft.com/ado/2008/09/edm">
<EntityType Name="Product">
<Key>
<PropertyRef Name="ProductId" />
</Key>
<Property Name="ProductId" Type="Edm.String" :label="Jerry产品ID"
:filterable="false" />
<Property Name="Name" Type="Edm.String" MaxLength="30"
:label="Name" :filterable="false" />
<Property Name="Category" Type="Edm.String" :label="Category"
:filterable="true" />
<Property Name="Price" Type="Edm.String" :unit="CurrencyCode"
MaxLength="3" :label="Price" :filterable="false" />
<Property Name="CurrencyCode" Type="Edm.String" MaxLength="3"
:label="Currency" :semantics="currency-code" :filterable="true" />
</EntityType>
<EntityType Name="Currency">
<Key>
<PropertyRef Name="CURR" />
</Key>
<Property Name="CURR" Type="Edm.String" MaxLength="4"
:display-format="UpperCase" :text="DESCR" :label="Currency Code"
:filterable="false" />
<Property Name="DESCR" Type="Edm.String" MaxLength="25"
:label="Description" />
</EntityType>
<EntityType Name="Category">
<Key>
<PropertyRef Name="CAT" />
</Key>
<Property Name="CAT" Type="Edm.String" MaxLength="4"
:display-format="UpperCase" :text="DESCR" :label="Category"
:filterable="false" />
<Property Name="DESCR" Type="Edm.String" MaxLength="25"
:label="Description" />
</EntityType>
<EntityContainer m:IsDefaultEntityContainer="true"
:supported-formats="atom json">
<EntitySet Name="Products" EntityType="com..wt05.Product" />
<EntitySet Name="Currency" EntityType="com..wt05.Currency" />
<EntitySet Name="Category" EntityType="com..wt05.Category" />
</EntityContainer>
<Annotations Target="com..wt05.Product/CurrencyCode"
xmlns="http://docs.oasis-open.org/odata/ns/edm">
<Annotation Term="com..vocabularies.Common.v1.ValueList">
<Record>
<PropertyValue Property="Label" String="Currency" />
<PropertyValue Property="CollectionPath" String="Currency" />
<PropertyValue Property="SearchSupported" Bool="true" />
<PropertyValue Property="Parameters">
<Collection>
<Record Type="com..vocabularies.Common.v1.ValueListParameterOut">
<PropertyValue Property="LocalDataProperty"
PropertyPath="CurrencyCode" />
<PropertyValue Property="ValueListProperty"
String="CURR" />
</Record>
<Record
Type="com..vocabularies.Common.v1.ValueListParameterDisplayOnly">
<PropertyValue Property="ValueListProperty"
String="DESCR" />
</Record>
</Collection>
</PropertyValue>
</Record>
</Annotation>
</Annotations>
<Annotations Target="com..wt05.Product/Category"
xmlns="http://docs.oasis-open.org/odata/ns/edm">
<Annotation Term="com..vocabularies.Common.v1.ValueList">
<Record>
<PropertyValue Property="Label" String="Category" />
<PropertyValue Property="CollectionPath" String="Category" />
<PropertyValue Property="SearchSupported" Bool="true" />
<PropertyValue Property="Parameters">
<Collection>
<Record Type="com..vocabularies.Common.v1.ValueListParameterOut">
<PropertyValue Property="LocalDataProperty"
PropertyPath="Category" />
<PropertyValue Property="ValueListProperty"
String="CAT" />
</Record>
<Record
Type="com..vocabularies.Common.v1.ValueListParameterDisplayOnly">
<PropertyValue Property="ValueListProperty"
String="DESCR" />
</Record>
</Collection>
</PropertyValue>
</Record>
</Annotation>
</Annotations>
<Annotations Target="com..wt05.Product"
xmlns="http://docs.oasis-open.org/odata/ns/edm">
<Annotation Term="com..vocabularies.UI.v1.LineItem">
<Collection>
<Record Type="com..vocabularies.UI.v1.DataField">
<PropertyValue Property="Value" Path="Name" />
</Record>
<Record Type="com..vocabularies.UI.v1.DataField">
<PropertyValue Property="Value" Path="ProductId" />
</Record>
</Collection>
</Annotation>
</Annotations>
</Schema>
</edmx:DataServices>
</edmx:Edmx>
尽管 Product 模型包含了 4 个字段作为表格备选列项目,但为什么最终渲染出的页面里,我们只看到了 3 个行项目?名为 Category 的字段为什么没能渲染成行项目? 答案在 metadata.xml 的注解区域。
帮助文档提到,其所属的 OData 模型被注解 com..vocabiularies.UI.LineItem 修饰,且类型为 com..vocabularies.UI.DataField 的字段,在运行时会被 UI5 框架绘制成表格列项目。
为了验证这个结论,我们对 metadata.xml 里的元数据进行一些修改。比如现在只定义两个表格列项目,分别为ProductId 和 Name. 同时,我用 :label, 给属性 ProductId 分配标签为 "Jerry产品ID":
运行时的效果:Name 列表项出现在 ProductId 的左边,因为其在元数据里的定义,位置在 ProductId 之前。
至此我们已经了解了 Smart Table 表格列项目渲染的逻辑,最后来看看源代码实现。
我的 UI5 应用里,使用了 Smart Table 控件的 XML 视图,运行时被加载后,会被 UI5 的 XML 模板解析器, XMLTemplateProcessor 的方法 parseTemplate 所解析。XML 视图包含的 XML 字符串,会被反序列化成 DOM 并进行遍历。当模板解析器遍历 DOM 过程中,遇到 SmartTable 标签时,调用 SmartTable.init 方法,进行初始化操作:
根据本文前半部分的介绍,我们已经知道:如果缺乏 OData 元数据提供的注解,Smart Table 控件无法知道该怎么渲染表格的列项目。因此,SmartTable.js 也在 "OData 服务元数据成功取回" 这个事件上,注册了一个钩子函数 _onMetadataInitialised. 当 OData 服务的元数据取回之后,该回调函数被调用:
javascript
SmartTable.prototype._onMetadataInitialised = function() {
this._bMetaModelLoadAttached = false;
if (this.bIsInitialised) {
return;
}
this._bUseColumnLabelsAsTooltips = this.getUseColumnLabelsAsTooltips(); // keep value stable after initialization
// Check whether further custom columns where added in the meantime
this._updateInitialColumns();
this._fireBeforeInitialiseAndValidate();
this._validateCustomizeConfig(this.getCustomizeConfig());
this._createTableProvider();
if (!this._oTableProvider) {
return;
}
this._aTableViewMetadata = this._oTableProvider.getTableViewMetadata();
if (!this._aTableViewMetadata) {
return;
}
if (this._bUseColumnLabelsAsTooltips) {
this._oTable.getColumns().forEach(function(oColumn) {
var oHeader = null;
if (oColumn.getHeader) {
oHeader = oColumn.getHeader();
} else if (oColumn.getLabel) {
oHeader = oColumn.getLabel();
}
var oLabel = oHeader && oHeader.isA && (oHeader.isA(".m.Label") || oHeader.isA(".m.Text")) ? oHeader : null;
var oTooltipTarget = this._isMobileTable ? oLabel : oColumn;
var oTooltip = oTooltipTarget ? oTooltipTarget.getTooltip() : null;
if (oTooltipTarget && oLabel && !oTooltip && !oTooltipTarget.isBound("tooltip")) {
if (oLabel.isBound("text")) {
var oBindingInfo = _getClonedBindingInfo(oLabel.getBindingInfo("text"));
oTooltipTarget.bindProperty("tooltip", oBindingInfo);
} else {
oTooltipTarget.setTooltip(oLabel.getText());
}
}
}, this);
}
// Set width for custom columns after metadata is initialized
if (this.getEnableAutoColumnWidth()) {
this._oTable.getColumns().forEach(this._setWidthForCustomColumn, this);
}
if (!this._isMobileTable && this.getDemandPopin()) {
this.setDemandPopin(false);
Log.error("use SmartTable property 'demandPopin' only with responsive table, property has been set to false");
}
this.detachModelContextChange(this._initialiseMetadata, this);
// Indicates the control is initialised and can be used in the initialise event/otherwise!
this.bIsInitialised = true;
delete this._bInitialising;
this._updateP13nDialogSettings(true);
this._bTableSupportsExcelExport = this._oTableProvider.getSupportsExcelExport();
this._bMultiUnitBehaviorEnabled = this._oTableProvider.getMultiUnitBehaviorEnabled();
this._listenToSmartFilter();
this._createVariantManagementControl(); // creates VariantMngmntCtrl if useVariantManagement OR useTablePersonalisation is true.
// Control is only added to toolbar if useVariantManagement is set otherwise it acts as
// hidden persistance helper
this._createToolbarContent();
this._applyToolbarContentOrder();
this._aAlwaysSelect = this._oTableProvider.getRequestAtLeastFields();
this._createContent();
this._createPersonalizationController();
// Create a local JSONModel to handle editable switch
this._oEditModel = new JSONModel({
editable: this.getEditable()
});
// Set the local model on the SmartTable
this.setModel(this._oEditModel, "sm4rtM0d3l");
this.attachEvent("_change", this._onPropertyChange, this);
this.fireInitialise();
// Trigger initial binding if no Variant exists -or- if it is already initialised
if (!this._oVariantManagement || (this._oVariantManagement && this._bVariantInitialised)) {
this._checkAndTriggerBinding();
}
};
该函数是 UI5 中 SmartTable
控件的一个方法,主要负责在元数据初始化完成后设置表格的一些核心功能和行为。此方法是内部方法,通常在控件初始化过程中自动调用。
该函数每个步骤都旨在确保 SmartTable
控件能够根据提供的配置和元数据正确初始化,提供灵活的个性化设置,并为最终用户呈现丰富、互动的数据表格视图。通过这种方式, UI5 框架提供了强大的工具,支持开发人员创建高度定制的应用程序视图,满足复杂的业务需求。
在该回调函数执行的上下文里,因为 OData 服务元数据已经处于可访问状态,所以 Smart Table 有足够的信息,能够开始渲染逻辑的执行:
下图第 97 行的高亮代码,getLineItemAnnotation, 即是 Smart Table 控件,准备从 Product 这个 EntityType 里,解析出符合表格列项目渲染要求的字段列表:
注意下图第 1909 行硬编码的字符串 com..vocabularies.UI.v1.LineItem, 这就是 UI5 框架代码里查找 Smart Table 待渲染列表项字段的依据。最后解析出的两个列表项字段,Name 和 ProductId,就存储在函数返回变量 oResolvedAnnotation.
有了这个信息,Smart Table 就知道该渲染哪些字段作为表格列项目了。
至此,本文已经完成了 Smart Table 控件渲染表格列项目的原理介绍,以及相应的 UI5 框架是如何解析待渲染列项目的源代码实现的介绍。
希望本文能给对 Smart Table 技术内幕感兴趣的朋友们有所启发。