首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> J2EE

JSF 2 打造可重用 Ajax 組件實戰(六):Java 端監聽器完整實現與邏輯解析

     編輯:J2EE

前言:Ajax 行為的服務端大腦 — AutocompleteListener

前端 JavaScript 觸發 `jsf.ajax.request()` 後,JSF 生命週期執行至 **Invoke Application** 階段,調用綁定在輸入框 `valueChangeListener` 和列表框 `valueChangeListener` 上的監聽器方法。本文完整呈現 `AutocompleteListener` 代碼,逐行解析核心邏輯:完成項過濾、列表框顯示/隱藏控制、選中值回寫、多實例隔離機制。

1. 完整代碼 (AutocompleteListener.java)

package com.corejsf;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import javax.enterprise.context.SessionScoped;
import javax.faces.component.UIInput;
import javax.faces.component.UISelectItems;
import javax.faces.component.UISelectOne;
import javax.faces.context.FacesContext;
import javax.faces.event.ValueChangeEvent;
import javax.inject.Named;

@Named
@SessionScoped
public class AutocompleteListener implements Serializable {

    private static final String COMPLETION_ITEMS_ATTR = "corejsf.completionItems";

    // 1. 輸入框 valueChangeListener -> 觸發完成項過濾與列表顯示
    public void valueChanged(ValueChangeEvent e) {
        UIInput input = (UIInput) e.getSource();                    // 觸發事件的輸入框
        UISelectOne listbox = (UISelectOne) input.findComponent("listbox"); // 同組件內的列表框

        if (listbox != null) {
            UISelectItems items = (UISelectItems) listbox.getChildren().get(0); // 獲取 f:selectItems
            Map attrs = listbox.getAttributes();                // 列表框屬性映射 (含 style、size)

            // 核心:根據輸入值過濾完成項
            List newItems = getNewItems((String) input.getValue(),
                    getCompletionItems(listbox, items, attrs));

            // 更新列表框數據模型
            items.setValue(newItems.toArray());

            // 動態控制列表框顯示/隱藏、位置、大小
            setListboxStyle(newItems.size(), attrs);
        }
    }

    // 2. 列表框 valueChangeListener -> 用戶選中項,回寫輸入框並隱藏列表
    public void completionItemSelected(ValueChangeEvent e) {
        UISelectOne listbox = (UISelectOne) e.getSource();
        UIInput input = (UIInput) listbox.findComponent("input"); // 對應的輸入框

        if (input != null) {
            input.setValue(listbox.getValue()); // 將選中值寫入輸入框 (觸發 model 更新)
        }

        // 隱藏列表框
        Map attrs = listbox.getAttributes();
        attrs.put("style", "display: none");
    }

    // 3. 過濾邏輯:前綴匹配 (忽略大小寫)
    private List getNewItems(String inputValue, String[] completionItems) {
        List newItems = new ArrayList<>();
        if (inputValue == null || inputValue.isEmpty()) return newItems;

        for (String item : completionItems) {
            if (item.length() >= inputValue.length()) {
                String prefix = item.substring(0, inputValue.length());
                if (prefix.equalsIgnoreCase(inputValue)) {
                    newItems.add(item);
                }
            }
        }
        return newItems;
    }

    // 4. 列表框樣式動態控制:位置、顯示/隱藏、行數
    private void setListboxStyle(int rows, Map attrs) {
        if (rows > 0) {
            // 從請求參數獲取 JS 傳來的 x,y 座標 (見 autoComplete.js)
            Map reqParams = FacesContext.getCurrentInstance()
                    .getExternalContext().getRequestParameterMap();

            attrs.put("style", "display: inline; position: absolute; left: "
                    + reqParams.get("x") + "px; top: " + reqParams.get("y") + "px");
            attrs.put("size", rows == 1 ? 2 : rows); // 至少顯示 2 行避免單行過窄
        } else {
            attrs.put("style", "display: none");
        }
    }

    // 5. 獲取/緩存原始完成項數組 (支援多實例隔離)
    private String[] getCompletionItems(UISelectOne listbox,
            UISelectItems items, Map attrs) {
        String[] completionItems = (String[]) attrs.get(COMPLETION_ITEMS_ATTR);
        if (completionItems == null) {
            completionItems = (String[]) items.getValue(); // 首次從 f:selectItems 獲取
            attrs.put(COMPLETION_ITEMS_ATTR, completionItems); // 緩存到列表框屬性
        }
        return completionItems;
    }
}

2. 核心邏輯深度解析

2.1 組件樹導航:`findComponent("listbox")` / `findComponent("input")`

  • 復合組件 `autoComplete.xhtml` 定義了 `id="input"` (輸入框) 和 `id="listbox"` (列表框)。
  • 監聽器通過 `UIComponent.findComponent(String id)` 在**同一命名容器**內查找兄弟組件,**不依賴全局 ID**,天然支援同頁多實例。

2.2 數據流向


用戶輸入 (keyup) 
  -> JS: jsf.ajax.request(input, event, {render: listboxId, x, y})
  -> JSF 執行生命週期 (執行 input 的 valueChangeListener)
  -> AutocompleteListener.valueChanged()
       1. 讀取 input.getValue() (新輸入文本)
       2. 從 listbox 屬性獲取/緩存原始完成項數組
       3. 前綴過濾 -> newItems
       4. items.setValue(newItems.toArray()) 更新列表數據模型
       5. setListboxStyle() 計算 style (x,y,display:inline) 和 size
  -> JSF Render Response: 僅渲染 listbox (render=clientId)
  -> 瀏覽器局部更新: 列表框顯示過濾後項目,定位在輸入框下方

2.3 多實例隔離機制

每個 `` 實例擁有獨立的 `listbox` 組件實例,其 `getAttributes()` 返回獨立的 `Map`。監聽器將原始完成項緩存於 **各自 listbox 的屬性映射** 中 (`corejsf.completionItems`),互不干擾。這比存放在 Session/Application 層級的 Map 更簡潔、無併發風險、隨組件銷毀自動釋放。

2.4 選中值回寫


用戶點擊列表項
  -> JSF 觸發 listbox 的 ValueChangeEvent
  -> AutocompleteListener.completionItemSelected()
       1. listbox.getValue() 獲取選中項
       2. input.setValue(...) 寫入輸入框 (觸發 model 更新 #{user.country})
       3. attrs.put("style", "display: none") 隱藏列表

3. CDI 與 JSF 整合細節

  • `@Named` + `@SessionScoped`:CDI 管理的會話級 Bean,EL 表達式 `#{autocompleteListener}` 可直接引用 (若需)。
  • Facelet 中綁定:``
  • `ValueChangeEvent` 由 JSF 在 **Process Validations** 階段 (即時 Ajax) 或 **Invoke Application** 階段 (非即時) 觸發,本組件使用即時 Ajax (`event="keyup"`)。

4. 可擴展點與生產化建議

擴展點建議實現
匹配模式策略模式:`MatchStrategy` 介面 (前綴/模糊/正則/拼音),屬性注入。
數據源注入 `CompletionProvider` 介面 (內存列表/JPA/REST/Redis),支援分頁/懶加載。
大小寫/語言環境使用 `Collator.getInstance(locale)` 進行本地化比較。
無障礙 (ARIA)監聽器輸出 `aria-activedescendant`、`aria-expanded` 屬性到 listbox。
客戶端緩存JS 端維護 LRU 緩存,減少 Ajax 請求 (需失效策略)。

結語

`AutocompleteListener` 展示了 JSF 2 **組件行為封裝在 CDI Bean 中** 的優雅模式:無需繼承 `UIComponent`、無需 `Renderer`、無需 `.taglib.xml`。通過標準 `ValueChangeEvent`、組件樹導航、屬性映射存儲狀態,實現了完整的自動完成交互邏輯,且天然支援多實例隔離。下篇將解析前端 JavaScript 協同邏輯 (`autoComplete.js`)。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved