The iDempiere ERP framework, built upon the foundation of ADempiere, leverages ZK as its core web UI technology. Over the years, ZK has introduced the highly effective Model-View-ViewModel (MVVM) pattern. While MVVM excels at separating UI logic from data state, iDempiere’s established custom components (like WSearchEditor and standard Forms) are deeply rooted in the traditional MVC (Model-View-Controller) pattern.

This blog post explores how to leverage both patterns in a single iDempiere application, specifically focusing on the advanced technique of Component Injection to embed MVC-style components within a modern MVVM-driven View.


1. The Architectural Divide: MVC vs. MVVM in iDempiere

🔸 MVC (Model-View-Controller)

  • Role in iDempiere: Dominates core functionality like standard windows (WEditor, ADForm), where the Controller (Composer or ADForm implementation) directly manipulates the View’s components and handles data synchronization.
  • Key Feature: Components like WSearchEditor are designed to be created and managed programmatically in Java, and they use ValueChangeListener for data binding and synchronization.

🔸 MVVM (Model-View-ViewModel)

  • Role in ZK: Used for modern, highly dynamic screens (like Dashboards and custom forms), where the binding is declared directly in the ZUL file using the @bind syntax.
  • Key Feature: The ViewModel manages the View’s state. Developers avoid direct component manipulation, letting the ZK Binder automatically handle data synchronization between the View and the ViewModel.

The Conflict

When building a custom MVVM Form (e.g., a “Dispensing” screen), developers often need to use powerful iDempiere components like WSearchEditor for lookup fields. Since WSearchEditor is a Java object and not a simple ZK component, the standard ZK @bind annotation fails to work with it.


2. The Solution: Dynamic Component Injection

To bridge this gap, we employ a hybrid approach: using an MVC Controller (Composer) to inject MVC components into an MVVM View.

This technique separates the concerns perfectly:

  1. MVVM (ViewModel): Manages the data state (MFormulaLine objects) and handles business logic (@Command for saving).
  2. MVC (Composer): Handles the UI wiring specific to the custom iDempiere components (WSearchEditor).

Technical Implementation Steps

Let’s use the example of dynamically injecting a WSearchEditor into a Listbox within an MVVM-driven Form.

Step A: Prepare the MVVM View (ZUL)

The ZUL file acts as the View. We replace the standard editable field with an empty container (Listcell) that includes a dynamic index ID.

<listbox id="formulaLineListbox" model="@load(vm.formulaForEdit.components)">
    <template name="model" var="each" status="eachStatus"> 
        <listitem>
            <listcell id="materialTypeCell_@{eachStatus.index}">
                </listcell>
            <listcell>
                <textbox value="@bind(each.name)" /> 
            </listcell>
        </listitem>
    </template>
</listbox>

<window apply="tw.topgiga.rnd.form.FormulaEditComposer" ...>

Step B: The Injection Controller (FormulaEditComposer.java)

This dedicated ZK Composer is responsible for the manual injection and data synchronization.

// FormulaEditComposer.java
public class FormulaEditComposer extends GenericForwardComposer<Window> {

    @Override
    public void doAfterCompose(Window comp) throws Exception {
        super.doAfterCompose(comp);
        
        // Retrieve the data model from the parent ViewModel (often passed as an argument)
        List<MFormulaLine> lines = ... // Get the list of lines

        // Retrieve the Listbox component by its ID
        Listbox listbox = (Listbox) comp.getFellow("formulaLineListbox"); 
        
        // Loop through the data and inject the WSearchEditor
        for (int i = 0; i < lines.size(); i++) {
            MFormulaLine line = lines.get(i);
            
            // 1. Find the target container using the dynamic ID
            Listcell cell = (Listcell) listbox.getFellow("materialTypeCell_" + i); 
            
            // 2. Create the MVC Component (WSearchEditor)
            MLookup lookup = MLookupFactory.get(...); 
            WSearchEditor editor = new WSearchEditor("RND_MaterialType_ID", ..., lookup);

            // 3. Set the initial value from the Model
            editor.setValue(line.getRND_MaterialType_ID()); 
            
            // 4. ***The Crucial Step: Manual Data Write-back***
            editor.addValueChangeListener(evt -> {
                // The MVC Component's event writes the new value directly back 
                // to the MVVM Model object (line).
                line.setRND_MaterialType_ID((Integer) evt.getNewValue());
            });

            // 5. Inject the component's ZK UI part into the Listcell
            cell.appendChild(editor.getComponent()); 
        }
    }
}

3. The Grand Finale: Unified Saving

Once the ValueChangeListener (Step 4 above) is in place, the core Model object (MFormulaLine) is immediately updated in memory whenever the user changes the value in the WSearchEditor.

The final save operation remains purely MVVM:

// MainDispensingVM.java (The MVVM Layer)
@Command
@NotifyChange("projectFormulas")
public void saveEditedFormula() {
    // Because the Composer ensured the Model object (this.formulaForEdit) 
    // is up-to-date, the save command is simple and clean.
    if (this.formulaForEdit.save()) {
        Clients.showNotification("✅ Formula saved successfully!");
    } else {
        Clients.showNotification("❌ Save failed. Check data.");
    }
}

By leveraging a small, dedicated MVC Composer for component injection, we successfully integrate iDempiere’s powerful MVC tooling into a modern ZK MVVM architecture, resulting in highly maintainable and clean code that separates UI wiring from business logic.

By Ray Lee (System Analyst)

iDempeire ERP Contributor, 經濟部中小企業處財務管理顧問 李寶瑞

Leave a Reply

Your email address will not be published. Required fields are marked *