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 (ComposerorADFormimplementation) directly manipulates the View’s components and handles data synchronization. - Key Feature: Components like
WSearchEditorare designed to be created and managed programmatically in Java, and they useValueChangeListenerfor 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
@bindsyntax. - 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:
- MVVM (ViewModel): Manages the data state (
MFormulaLineobjects) and handles business logic (@Commandfor saving). - 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.
