Developing Plug-in Forms
๐ฏ Goalโ
This tutorial shows you how to use the IFormFactory service in your plug-in project to create custom forms in iDempiere.
What is a Form?โ
A form in iDempiere is a custom user interface component that provides specialized functionality beyond standard windows. Forms are typically used for:
- Complex data entry screens with custom layouts
- Data processing interfaces (e.g., allocation forms, kanban board)
- Reporting interfaces with interactive parameters
- Specialized business workflows that don't fit the standard window model
- Custom dashboards and visualizations
โ Prerequisitesโ
Before starting, review:
You should already know how to create a plug-in, as this guide will not cover that in detail.
๐ Implementation Methodsโ
Modern Approach (iDempiere 9+): Mapped Form Factoryโ
Starting with iDempiere 9, a new form factory base class was introduced that's backed by Map and Lambda functional objects. This approach supports multiple registration methods.
Example Form Classโ
First, create your form controller class that implements IFormController:
public class MyTestForm implements IFormController {
private CustomForm form;
public MyTestForm() {
form = new CustomForm();
}
@Override
public ADForm getForm() {
return form;
}
}
Method 1: Register at Plugin Activator Startโ
public void start(BundleContext context) throws Exception {
IMappedFormFactory mappedFactory = Extensions.getMappedFormFactory();
mappedFactory.addMapping(MyTestForm.class.getName(),
() -> new MyTestForm().getForm());
}
Extensions.getMappedFormFactory() can return null if your bundle activates before the service. Use Method 4 for safer registration.
Method 2: Bind via OSGi Component Serviceโ
@Component(immediate = true)
public class MyFormComponent {
@Reference
public void bindService(IMappedFormFactory factory) {
factory.addMapping(MyTestForm.class.getName(),
() -> new MyTestForm().getForm());
}
}
Method 3: Extend MappedFormFactoryโ
@Component(service = IFormFactory.class)
public class MyFactory extends MappedFormFactory {
public MyFactory() {
addMapping(MyTestForm.class.getName(),
() -> new MyTestForm().getForm());
}
}
Do NOT register as IMappedFormFactory service when extending MappedFormFactory.
Method 4: Scan Package with Annotations (Recommended)โ
Use @Component, @Reference, and @Activate for safe registration:
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferenceCardinality;
@Component(immediate = true)
public class MyActivator implements BundleActivator {
@Reference(service = IMappedFormFactory.class,
cardinality = ReferenceCardinality.MANDATORY)
private IMappedFormFactory mappedFormFactory;
public MyActivator() {
}
@Override
public void start(BundleContext context) throws Exception {
}
@Override
public void stop(BundleContext context) throws Exception {
}
// activate() is called only after mappedFormFactory reference is injected
@Activate
public void activate(BundleContext context) {
// Replace with your package name containing form classes
mappedFormFactory.scan(context, "org.mycompany.form");
}
}
The @Activate approach ensures that your forms are only registered after the IMappedFormFactory service is available, avoiding null pointer exceptions.
Using @Form Annotationโ
The @Form annotation allows you to declaratively register your form class. The annotation has an optional name parameter to provide an alternate registration name.
Example:
import org.idempiere.ui.zk.annotation.Form;
import org.adempiere.webui.component.ADForm;
@Form(name = "org.compiere.apps.form.VAllocation")
public class WAllocation extends Allocation
implements IFormController, EventListener<Event> {
@Override
public ADForm getForm() {
return this;
}
// Your form implementation
}
To enable annotation scanning, create an OSGi component that extends AnnotationBasedFormFactory:
@Component(
immediate = true,
service = IFormFactory.class,
property = {"service.ranking:Integer=1"}
)
public class MyAnnotationFormFactory extends AnnotationBasedFormFactory {
// The base class automatically scans for @Form annotations
}
Example Project:
For a complete working example of a custom form with annotation-based registration, see:
idempiere-examples: ZUL Form Example
Technical References:
๐ง Creating a Custom Form (Step by Step)โ
Step 1: Create Your Form Controller Classโ
Create a class that implements IFormController and EventListener to handle form functionality.
Example: WMyFormController.java
package com.mycompany.webui.apps.form;
import org.adempiere.webui.component.ADForm;
import org.adempiere.webui.panel.IFormController;
import org.zkoss.zk.ui.event.Event;
import org.zkoss.zk.ui.event.EventListener;
public class WMyFormController implements IFormController, EventListener<Event> {
private ADForm form;
public WMyFormController() {
form = new ADForm();
initForm();
}
private void initForm() {
// Initialize your form components here
form.setTitle("My Custom Form");
// Add your UI components
}
@Override
public ADForm getForm() {
return form;
}
@Override
public void onEvent(Event event) throws Exception {
// Handle events here
}
}
Step 2: Create Your Form Factory Classโ
Create a class that implements IFormFactory to serve as the form loader for your plug-in.
Example: MyFormFactory.java
package com.mycompany.webui.factory;
import org.adempiere.webui.factory.IFormFactory;
import org.adempiere.webui.panel.IFormController;
import com.mycompany.webui.apps.form.WMyFormController;
public class MyFormFactory implements IFormFactory {
@Override
public IFormController newFormInstance(String formName) {
if (formName.equals("com.mycompany.webui.apps.form.WMyFormController")) {
return new WMyFormController();
}
return null;
}
}
Step 3: Register as OSGi Componentโ
Option A: Using Annotations (Recommended)
Add the @Component annotation to your factory class:
import org.osgi.service.component.annotations.Component;
@Component(
service = IFormFactory.class,
immediate = true,
property = {"service.ranking:Integer=1"}
)
public class MyFormFactory implements IFormFactory {
// ... implementation
}
See Event Handling with Annotations for more details.
Option B: Manual Component Definition
- Create a directory named
OSGI-INFin your project root - Create a file
OSGI-INF/formfactory.xml:
<?xml version="1.0" encoding="UTF-8"?>
<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0"
name="com.mycompany.webui.factory.formfactory">
<implementation class="com.mycompany.webui.factory.MyFormFactory"/>
<property name="service.ranking" type="Integer" value="1"/>
<service>
<provide interface="org.adempiere.webui.factory.IFormFactory"/>
</service>
</scr:component>
- Update your
META-INF/MANIFEST.MF:
Service-Component: OSGI-INF/*.xml
- Ensure
build.propertiesincludes the OSGI-INF directory:
bin.includes = META-INF/,\
.,\
OSGI-INF/
The service.ranking property determines the priority for OSGi service lookup. Higher values have higher priority. If multiple plug-ins provide the same service, OSGi will call them in descending order of service.ranking until one returns a non-null object.
โ๏ธ Configuring Your Form in iDempiereโ
Step 1: Create a Form Recordโ
- Log in as System Administrator
- Open System Admin > General Rules > Form
- Create a new record:
- Name: Your form name (e.g., "My Custom Form")
- Classname: Your form controller's fully qualified class name
(e.g.,com.mycompany.webui.apps.form.WMyFormController) - Description: Brief description of what the form does
Step 2: Create a Menu Entryโ
- Open System Admin > General Rules > Menu
- Create a new record:
- Name: Menu item name
- Action: Select "Form"
- Special Form: Select your form from Step 1
- Sequence: Order in the menu
- Parent Menu: Choose where to place it in the menu tree
Step 3: Grant Role Accessโ
- Open System Admin > Security > Role
- Select the appropriate role (e.g., "GardenWorld Admin")
- Go to the Form Access tab
- Add your new form and grant read access
๐งช Testing Your Formโ
- Deploy your plug-in to iDempiere
- Ensure your plug-in is set to auto-start in the Run Configuration
- Start the iDempiere server
- Log in as GardenAdmin (or the role you configured)
- Navigate to your menu entry
- The custom form should open
If the form doesn't appear:
- Check that your plug-in is activated in the Run Configuration
- Verify the classname in the Form record matches exactly
- Check the console for ClassNotFoundException or other errors
- Ensure the role has access to the form