头图

我们在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 视图的完整源代码:

<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 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 服务的元数据取回之后,该回调函数被调用:

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 技术内幕感兴趣的朋友们有所启发。


注销
1k 声望1.6k 粉丝

invalid