ofbiz学习——深入分析lookup标签实现原理

上一章介绍了一个查询界面的实现。但是查询条件里有个生产标识的控件lookup实现逻辑没有深入的分析,本章主要就是分析lookup的具体实现逻辑。

lookup标签生成的控件有2个功能:

1. 类似jquery autocomplete的功能,输入字符后会自动弹出提示选项供用户选择。

2. 右边有个小图标,点击小图标可以弹出一个查询窗口,通过查询条件筛选出查询结果后,选中目标选项,自动回填之前的输入框。



1. 先找出相关的源码

<field name="productId" title="${uiLabelMap.ProductProductId}"><lookup target-form-name="LookupProduct"/></field>  

具体重点应该是lookup标签的配置。访问controller.xml文件,找到LookupProduct的配置:
<view-map name="LookupProduct" page="component://product/widget/catalog/LookupScreens.xml#LookupProduct" type="screen"/>
打开文件component://product/widget/catalog/LookupScreens.xml#LookupProduct
    <screen name="LookupProduct">
        <section>
            <condition>
                <if-service-permission service-name="catalogPermissionCheck" main-action="VIEW"/>
            </condition>
            <actions>
                <property-map resource="ProductUiLabels" map-name="uiLabelMap" global="true"/>
                <set field="title" value="${uiLabelMap.PageTitleLookupProduct}"/>
                <set field="queryString" from-field="result.queryString"/>
                <set field="entityName" value="Product"/>
                <set field="searchFields" value="[productId, internalName, brandName]"/>
            </actions>
            <widgets>
                <decorator-screen name="LookupDecorator" location="component://common/widget/CommonScreens.xml">
                    <decorator-section name="search-options">
                        <include-form name="LookupProduct" location="component://product/widget/catalog/FieldLookupForms.xml"/>
                    </decorator-section>
                    <decorator-section name="search-results">
                        <include-form name="ListLookupProduct" location="component://product/widget/catalog/FieldLookupForms.xml"/>
                    </decorator-section>
                </decorator-screen>
            </widgets>
        </section>
    </screen>

打开文件component://common/widget/CommonScreens.xml#LookupDecorator

    <screen name="LookupDecorator">
        <section>
            <condition>
                <not><if-compare operator="equals" value="Y" field="parameters.ajaxLookup"/></not>
            </condition>
            <widgets>
                <section>
                    <actions>
                        <property-map resource="CommonUiLabels" map-name="uiLabelMap" global="true"/>
                        <service service-name="getUserPreferenceGroup" result-map="prefResult">
                            <field-map field-name="userPrefGroupTypeId" value="GLOBAL_PREFERENCES"/>
                        </service>
                        <set field="userPreferences" from-field="prefResult.userPrefMap" global="true"/>

                        <property-map resource="general" map-name="generalProperties" global="true"/>
                        <set field="visualThemeId" from-field="userPreferences.VISUAL_THEME" global="true"/>
                        <set field="defaultOrganizationPartyId" from-field="userPreferences.ORGANIZATION_PARTY" global="true"/>
                        <service service-name="getVisualThemeResources">
                            <field-map field-name="visualThemeId"/>
                            <field-map field-name="themeResources" from-field="layoutSettings"/>
                        </service>

                        <set field="layoutSettings" from-field="themeResources" default-value="${layoutSettings}" global="true"/>

                        <set field="messagesTemplateLocation" from-field="layoutSettings.VT_MSG_TMPLT_LOC[0]" default-value="component://common/template/includes/Messages.ftl"/>
                    </actions>
                    <widgets>
                        <section>
                        <condition>
                                <if-compare value="layer" operator="not-equals" field="parameters.presentation"/>
                        </condition>
                            <widgets>
                                <platform-specific><html><html-template location="component://common/template/includes/Lookup.ftl" /></html></platform-specific>
                            </widgets>
                        </section>
                        <platform-specific><html><html-template location="${messagesTemplateLocation}"/></html></platform-specific>
                        <section>
                            <condition>
                                <not><if-empty-section section-name="body"/></not>
                            </condition>
                            <widgets>
                                <decorator-section-include name="body"/>
                            </widgets>
                            <fail-widgets>
                                <screenlet title="${title}" id="findScreenlet" collapsible="true" padded="false">
                                    <container id="search-options">
                                        <decorator-section-include name="search-options"/>
                                    </container>
                                </screenlet>
                                <screenlet>
                                    <container id="search-results">
                                        <decorator-section-include name="search-results"/>
                                    </container>
                                </screenlet>
                            </fail-widgets>
                        </section>
                        <section>
                        <condition>
                            <if-compare value="layer" operator="not-equals" field="parameters.presentation"/>
                        </condition>
                            <widgets>
                                <platform-specific><html><html-template location="component://common/template/includes/LookupFooter.ftl"/></html></platform-specific>
                            </widgets>
                        </section>
                    </widgets>
                </section>
            </widgets>
            <fail-widgets>
                <section>
                    <actions>
                        <property-map resource="CommonUiLabels" map-name="uiLabelMap" global="true"/>
                        <set field="searchType" from-field="parameters.searchType" default-value="${searchType}"/>
                        <script location="component://common/groovyScripts/FindAutocompleteOptions.groovy"/>
                    </actions>
                    <widgets>
                        <decorator-screen name="AjaxGlobalDecorator">
                            <decorator-section name="body">
                                <platform-specific>
                                    <html>
                                        <html-template location="component://common/template/includes/AjaxAutocompleteOptions.ftl" />
                                    </html>
                                </platform-specific>
                            </decorator-section>
                        </decorator-screen>
                    </widgets>
                </section>
            </fail-widgets>
        </section>
    </screen>
打开文件component://common/groovyScripts/FindAutocompleteOptions.groovy

import org.apache.ofbiz.base.util.StringUtil
import org.apache.ofbiz.base.util.UtilDateTime
import org.apache.ofbiz.base.util.Debug
import org.apache.ofbiz.entity.util.EntityFindOptions
import org.apache.ofbiz.entity.condition.EntityCondition
import org.apache.ofbiz.entity.condition.EntityConditionList
import org.apache.ofbiz.entity.condition.EntityExpr
import org.apache.ofbiz.entity.condition.EntityFieldValue
import org.apache.ofbiz.entity.condition.EntityFunction
import org.apache.ofbiz.entity.condition.EntityOperator
import org.apache.ofbiz.entity.util.EntityUtilProperties

def mainAndConds = []
def orExprs = []
def entityName = context.entityName
def searchFields = context.searchFields
def displayFields = context.displayFields ?: searchFields
def searchDistinct = Boolean.valueOf(context.searchDistinct ?: false)

def searchValueFieldName = parameters.term
def fieldValue = null
if (searchValueFieldName) {
    fieldValue = searchValueFieldName
} else if (parameters.searchValueFieldName) { // This is to find the description of a lookup value on initialization.
    fieldValue = parameters.get(parameters.searchValueFieldName)
    context.description = "true"
}

def searchType = context.searchType
def displayFieldsSet = null

def conditionDates = context.conditionDates
def fromDateName = null
def thruDateName = null
def filterByDateValue = null

//If conditionDates is present on context, resolve values use add condition date to the condition search
if (conditionDates) {
    filterByDateValue = conditionDates.filterByDateValue ?: UtilDateTime.nowTimestamp()
    fromDateName = conditionDates.fromDateName ?: null
    thruDateName = conditionDates.thruDateName ?: null
    //if the field filterByDate is present, init default value for fromDate and thruDate
    if (!fromDateName && !thruDateName) {
        fromDateName = "fromDate"
        thruDateName = "thruDate"
    }
}

if (searchFields && fieldValue) {
    def searchFieldsList = StringUtil.toList(searchFields)
    displayFieldsSet = StringUtil.toSet(displayFields)
    if (context.description && fieldValue instanceof java.lang.String) {
        returnField = parameters.searchValueFieldName
    } else {
        returnField = searchFieldsList[0] //default to first element of searchFields
        displayFieldsSet.add(returnField) //add it to select fields, in case it is missing
    }
    context.returnField = returnField
    context.displayFieldsSet = displayFieldsSet
    if ("STARTS_WITH".equals(searchType)) {
        searchValue = fieldValue.toUpperCase() + "%"
    } else if ("EQUALS".equals(searchType)) {
        searchValue = fieldValue
    } else {//default is CONTAINS
        searchValue = "%" + fieldValue.toUpperCase() + "%"
    }
    searchFieldsList.each { fieldName ->
        if ("EQUALS".equals(searchType)) {
            orExprs.add(EntityCondition.makeCondition(EntityFieldValue.makeFieldValue(searchFieldsList[0]), EntityOperator.EQUALS, searchValue))
            return //in case of EQUALS, we search only a match for the returned field
        } else {
            orExprs.add(EntityCondition.makeCondition(EntityFunction.UPPER(EntityFieldValue.makeFieldValue(fieldName)), EntityOperator.LIKE, searchValue))
        }        
    }
}

/* the following is part of an attempt to handle additional parameters that are passed in from other form fields at run-time,
 * but that is not supported by the Jquery Autocompleter, but this is still useful to pass parameters from the
 * lookup screen definition:
 */
def conditionFields = context.conditionFields
if (conditionFields) {
    // these fields are for additonal conditions, this is a Map of name/value pairs
    for (conditionFieldEntry in conditionFields.entrySet()) {
        if (conditionFieldEntry.getValue() instanceof java.util.List) {
            def orCondFields = []
            for (entry in conditionFieldEntry.getValue()) {
                orCondFields.add(EntityCondition.makeCondition(EntityFieldValue.makeFieldValue(conditionFieldEntry.getKey()), EntityOperator.EQUALS, entry))
            }
            mainAndConds.add(EntityCondition.makeCondition(orCondFields, EntityOperator.OR))
        } else {
            mainAndConds.add(EntityCondition.makeCondition(EntityFieldValue.makeFieldValue(conditionFieldEntry.getKey()), EntityOperator.EQUALS, conditionFieldEntry.getValue()))
        }
    }
}

if (orExprs && entityName && displayFieldsSet) {
    mainAndConds.add(EntityCondition.makeCondition(orExprs, EntityOperator.OR))

    //if there is an extra condition, add it to main condition list
    if (context.andCondition && context.andCondition instanceof EntityCondition) {
        mainAndConds.add(context.andCondition)
    }
    if (conditionDates) {
        def condsDateList = []
        if (thruDateName) {
            def condsByThruDate = []
            condsByThruDate.add(EntityCondition.makeCondition(EntityFieldValue.makeFieldValue(thruDateName), EntityOperator.GREATER_THAN, filterByDateValue))
            condsByThruDate.add(EntityCondition.makeCondition(EntityFieldValue.makeFieldValue(thruDateName), EntityOperator.EQUALS, null))
            condsDateList.add(EntityCondition.makeCondition(condsByThruDate, EntityOperator.OR))
        }

        if (fromDateName) {
            def condsByFromDate = []
            condsByFromDate.add(EntityCondition.makeCondition(EntityFieldValue.makeFieldValue(fromDateName), EntityOperator.LESS_THAN_EQUAL_TO, filterByDateValue))
            condsByFromDate.add(EntityCondition.makeCondition(EntityFieldValue.makeFieldValue(fromDateName), EntityOperator.EQUALS, null))
            condsDateList.add(EntityCondition.makeCondition(condsByFromDate, EntityOperator.OR))
        }

        mainAndConds.add(EntityCondition.makeCondition(condsDateList, EntityOperator.AND))
    }

    def entityConditionList = EntityCondition.makeCondition(mainAndConds, EntityOperator.AND)

    String viewSizeStr = context.autocompleterViewSize
    if (viewSizeStr == null) {
        viewSizeStr = EntityUtilProperties.getPropertyValue("widget", "widget.autocompleter.defaultViewSize", delegator)
    }
    Integer autocompleterViewSize = Integer.valueOf(viewSizeStr ?: 10)
    EntityFindOptions findOptions = new EntityFindOptions()
    findOptions.setMaxRows(autocompleterViewSize)
    findOptions.setDistinct(searchDistinct)

    autocompleteOptions = delegator.findList(entityName, entityConditionList, displayFieldsSet, StringUtil.toList(displayFields), findOptions, false)
    if (autocompleteOptions) {
        context.autocompleteOptions = autocompleteOptions
    }
}

打开文件component://common/template/includes/AjaxAutocompleteOptions.ftl

<#if description??>
  <#if autocompleteOptions??>
    <#list autocompleteOptions as autocompleteOption>
      <#assign displayString = ""/>
      <#list displayFieldsSet as key>
        <#assign field = autocompleteOption.get(key)!>
        <#if field?has_content>
          <#if (key != context.returnField)>
            <#assign displayString = displayString + field + " ">
          </#if>
        </#if>
      </#list>
      <#if (displayString?trim?has_content )>${displayString?trim}</#if>
    </#list>
  </#if>
<#else>
<script type="text/javascript">
    var autocomp = [
        <#if autocompleteOptions?has_content>
          <#if !displayReturnField??>
            <#assign displayReturnField = Static[
                "org.apache.ofbiz.entity.util.EntityUtilProperties"].getPropertyValue(
                "widget", "widget.autocompleter.displayReturnField", delegator)>
          </#if>
          <#list autocompleteOptions as autocompleteOption>
              {
                <#assign displayString = ""/>
                <#assign returnField = ""/>
                <#list displayFieldsSet as key>
                  <#assign field = autocompleteOption.get(key)!>
                  <#if field?has_content>
                    <#if (key == context.returnField)>
                      <#assign returnField = field/>
                    <#else>
                      <#assign displayString = displayString + StringUtil.wrapString(field?string) + " ">
                    </#if>
                  </#if>
                </#list>
                <#if ("Y" == displayReturnField)>
                  <#assign displayString = displayString +  "[" + returnField + "]">
                </#if>
                  "id": "${returnField}",
                  "label": "<#if (displayString?trim?has_content )>${displayString?trim}<#else>${returnField}</#if>",
                  "value": "${returnField}"
              }<#if autocompleteOption_has_next>,</#if>
          </#list>
        <#else>
            {
                "id": "",
                "label": "${uiLabelMap.CommonNoRecordFound}",
                "value": ""
            }
        </#if>];
</script>
</#if>

2. 分析autocomplete功能。

2.1 登录ofbiz后台系统 -》生产 -》 按F12调出开发者工具 -》 选择Network页签 -》 产品标识输入款输入字符串“wit”

然后我们能看到如下界面:

对应的后台请求:

从请求中可以看到,当输入三个字符“wit”后会马上发出一个post请求,其中表单数据只有一个term参数,其值就是我们输入的wit。url参数有3个:

ajaxLookup  = Y 。 表示这个请求是个ajax请求,LookupDecorator中有个判断,如果该参数等于Y,则表示是autocomplete功能,否则就是弹出查询窗口功能。

_LAST_VIEW_NAME_ =FindProductionRun 这里可以不用关注这个参数

searchValueFieldName:
productId productId就是前面field定义的="productId"

2.2 分析FindAutocompleteOptions.groovy

这个脚本有点长,先从后面往前面分析吧。先看下面代码:

autocompleteOptions = delegator.findList(entityName, entityConditionList, displayFieldsSet, StringUtil.toList(displayFields), findOptions, false)
从这里我们知道了输入的wit三个字符后,会通过这个语句到数据库里查找匹配的数据记录返回。

很容易的可以知道entityName的值就是在component://product/widget/catalog/LookupScreens.xml#LookupProduct文件中定义的:

<set field="entityName" value="Product"/>

entityConditionList主要是下面这条代码:
    searchFieldsList.each { fieldName ->
        if ("EQUALS".equals(searchType)) {
            orExprs.add(EntityCondition.makeCondition(EntityFieldValue.makeFieldValue(searchFieldsList[0]), EntityOperator.EQUALS, searchValue))
            return //in case of EQUALS, we search only a match for the returned field
        } else {
            orExprs.add(EntityCondition.makeCondition(EntityFunction.UPPER(EntityFieldValue.makeFieldValue(fieldName)), EntityOperator.LIKE, searchValue))
        }        
    }
由于 LookupScreens.xml#LookupProduct中配置了
<set field="searchFields" value="[productId, internalName, brandName]"/>
所以 entityConditionList最终等效的where语句是:
where (product_Id like '%wit%' orinternal_Name like '%wit%' or brand_Name like '%wit%' )

displayFieldsSet和displayFields好像数据基本都是一样的。

displayFieldsSet对应的sql语句是:

select product_Id, internal_Name, brand_Name

displayFields对应的sql语句是:

order by product_Id, internal_Name, brand_Name

findOptions这个暂时不管吧。

所以lookup标签能实现autocomplete功能,主要是使用的公共的CommonScreens.xml#LookupDecorator装饰器。而具体到例子中的“产品标识”的应用,则主要是配置了component://product/widget/catalog/LookupScreens.xml#LookupProduct

其中最关键的是要配置如下2个参数:

<set field="entityName" value="Product"/>
<set field="searchFields" value="[productId, internalName, brandName]"/>

3. 弹出查询窗口功能

该功能和其他普通界面区别不到,主要是该功能是通过layer方式打开。主要要注意的地方是component://product/widget/catalog/FieldLookupForms.xml#ListLookupProduct中的如下定义:

        <field name="productId" title="${uiLabelMap.ProductProductId}" widget-style="buttontext">
            <hyperlink description="${productId}" target="javascript:set_value('${productId}')" also-hidden="false" target-type="plain"/>
        </field>
具体 set_value函数的实现就不深入展开了,但正是因为有了该定义,所以才会实现点击弹出的查询窗口选项后,会自动把产品标识回填到原页面,并关闭弹出窗口。

4. 总结

lookup标签使用有了基本的了解。

如要实现autocomplete功能,需要配置好entityName和searchFields参数。

如要实现弹出查询窗口功能,则需要实现查询窗口,并在对应连接添加点击触发set_value函数。

在分析调试过程中发现个现象,就是但lookup对应的输入框失去焦点时,会触发一个post请求,请求信息如下:



我们可以注意到多了个参数searchType=EQUALS



重点注意:

1. 在文件component://product/widget/catalog/LookupScreens.xml#LookupProduct中下面这个要配置才能再弹出窗口实现国际化。

<property-map resource="BookingUiLabels" map-name="uiLabelMap" global="true"/>

2. 配置searchFields时,里面的数组必须是逗号+空格分割,之前我只用了一个逗号分割总是报错,后来逗号后面加个空格就好了。

<set field="searchFields" value="[courseId, courseName]"/>


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值