【開發實戰】Callout 與 Model Validator:事件驅動開發實戰

Callout 與 Model Validator:事件驅動開發實戰

iDempiere 提供兩種事件驅動機制:Callout 處理 UI 層即時互動,Model Validator 處理資料層驗證與後處理。本文將詳解兩者差異,並提供完整的實作範例。

一、什麼是 Callout?

Callout 是 UI 層級的回呼機制,當使用者在畫面上修改欄位時立即觸發。

適用場景

  • 欄位聯動(選客戶 → 自動帶入地址)
  • 即時計算(數量 × 單價 = 金額)
  • 動態驗證(檢查庫存是否足夠)

Callout 範例:自動計算金額

public class CalloutBookLoan extends CalloutEngine {
    
    public String calculateFee(Properties ctx, int WindowNo, 
            GridTab mTab, GridField mField, Object value) {
        
        if (value == null) return "";
        
        // 取得借閱天數
        int days = ((BigDecimal) value).intValue();
        
        // 計算費用:每天 10 元
        BigDecimal fee = new BigDecimal(days * 10);
        
        // 設定到費用欄位
        mTab.setValue("Fee", fee);
        
        return "";  // 空字串表示成功
    }
}

註冊 Callout

Application Dictionary → Column 找到目標欄位,設定 Callout 欄位:

com.yourcompany.callout.CalloutBookLoan.calculateFee

二、什麼是 Model Validator?

Model Validator 是資料層的攔截器,在資料儲存前後觸發,可用於驗證或後處理。

事件類型

事件 時機 用途
BEFORE_NEW 新增前 自動填入預設值
BEFORE_CHANGE 修改前 驗證資料
BEFORE_DELETE 刪除前 檢查是否可刪除
AFTER_NEW 新增後 建立關聯資料
AFTER_CHANGE 修改後 同步更新其他表
AFTER_DELETE 刪除後 清理關聯資料

Model Validator 範例

public class BookLoanValidator implements ModelValidator {
    
    private int m_AD_Client_ID = -1;
    
    @Override
    public void initialize(ModelValidationEngine engine, MClient client) {
        if (client != null)
            m_AD_Client_ID = client.getAD_Client_ID();
        
        // 註冊要監聽的表
        engine.addModelChange(MBookLoan.Table_Name, this);
    }
    
    @Override
    public String modelChange(PO po, int type) throws Exception {
        if (po instanceof MBookLoan) {
            MBookLoan loan = (MBookLoan) po;
            
            if (type == TYPE_BEFORE_NEW || type == TYPE_BEFORE_CHANGE) {
                // 驗證:借出時必須有借閱人
                if (loan.isBorrow() && loan.getAD_User_ID() <= 0) {
                    return "借出時必須指定借閱人";
                }
            }
            
            if (type == TYPE_AFTER_CHANGE) {
                // 記錄借閱歷史
                if (loan.is_ValueChanged("IsBorrow")) {
                    createHistory(loan);
                }
            }
        }
        return null;  // null 表示通過
    }
    
    @Override
    public String docValidate(PO po, int timing) {
        return null;  // 不處理文件事件
    }
    
    @Override
    public int getAD_Client_ID() {
        return m_AD_Client_ID;
    }
}

註冊 Model Validator

Application Dictionary → Model Validator 新增記錄:

  • Name: BookLoanValidator
  • Entity Type: 選擇你的 Entity
  • Model Validation Class: com.yourcompany.validator.BookLoanValidator

三、Callout vs Model Validator 比較

特性 Callout Model Validator
層級 UI Model (資料)
觸發 欄位變更時 儲存時
API 呼叫 ❌ 不觸發 ✅ 會觸發
Import ❌ 不觸發 ✅ 會觸發
可取消操作 ✅ 可以 ✅ 可以
效能影響 每次輸入都執行 只在儲存時執行

四、最佳實踐

  • UI 即時回饋 → 用 Callout
  • 資料驗證 → 用 Model Validator(確保 API 也會驗證)
  • 商業邏輯 → 放在 Model Validator 或 Model 類別
  • 避免在 Callout 存取資料庫,會影響 UI 效能

常見問題 FAQ

Q1: Callout 回傳值代表什麼?

回傳空字串 "" 表示成功,回傳錯誤訊息字串會顯示在 UI 上。

Q2: Model Validator 可以擋下儲存嗎?

可以!在 modelChange() 回傳錯誤訊息字串,系統會 rollback 並顯示錯誤。

Q3: 用 API 新增資料會觸發 Callout 嗎?

不會。Callout 只在 UI 操作時觸發。如果要確保驗證,必須用 Model Validator。

Q4: 如何偵錯 Callout/Validator?

使用 log.info()log.warning() 輸出到 log 檔案,或用 IDE 設中斷點 debug。

Q5: 一個欄位可以有多個 Callout 嗎?

可以,用分號 ; 分隔多個 Callout class,會依序執行。

留言

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *