Developing Plug-Ins - IEditorFactory and IDisplayTypeFactory

From iDempiere en
Jump to navigation Jump to search
CustomEditor1.png


This tutorial is brought to you by Jan Thielemann from evenos GmbH (www.evenos-consulting.de). If you have questions, criticism or improvement suggestions, feel free to visit me (Jan.thielemann) or write me an email

Goal of this tutorial

The goal of this tutorial is to show you, how you can create and provide your own custom editors to iDempiere.

For this tutorial, we will:

  1. Create an editor which allows to make a multiple selection of business partners in the User/Contact window
  2. Add a new field to the AD_User table to display our custom editor
  3. Create a new DisplayType for our editor in the Reference window

Prerequisites

tl;dr

The workflow

Preparing the Database and create the models

Before you start implementing some code, it's always a good idea to think about what you want to achieve and which parts of the application are affected by your changes. In our case, we want to make a connection between a user and several business partners.

Of couse, you could simply use another tab in the user window but we want to use a custom editor here. Nevertheless which way we would go, we need a new table either ways. This table should save a reference to our user as well as a reference to the selected business partners.

So let's start with a new table for our database:

CREATE TABLE eve_user_bpartner
(
  eve_user_bpartner_id numeric(10,0) NOT NULL,
  eve_user_bpartner_uu character varying(36) DEFAULT NULL::character varying,
  ad_client_id numeric(10,0) NOT NULL,
  ad_org_id numeric(10,0) NOT NULL,
  isactive character(1) NOT NULL DEFAULT 'Y'::bpchar,
  created timestamp without time zone NOT NULL DEFAULT now(),
  createdby numeric(10,0) NOT NULL,
  updated timestamp without time zone NOT NULL DEFAULT now(),
  ad_user_id numeric(10,0) NOT NULL,
  c_bpartner_id numeric(10,0) NOT NULL
)
WITH (
  OIDS=FALSE
);
ALTER TABLE eve_user_bpartner
  OWNER TO adempiere;

As you see, we have nothing special here. Just a default iDempiere table with all it's default columns as well as a reference to ad_user_id and c_bpartner_id. Now since we want to display a new editor in the user window, we also need another column in which we can display it. So let's add another column to the AD_User table:

alter table ad_user add eve_bpartners character varying(36) DEFAULT NULL


Execute the above SQLs on your database and you are good to go for the next steps. The first one is to open the application and create a new entry in the Table and Columns window for our new table. After this, you start the model.generator application from within eclipse to generate you model. If you are not sure how to do this, take a look at the model tutorial in this wiki (see Prerequisites). Finally create a component definition and a model factory and provide your models via OSGi.

Create a DisplayType

Reference Window

The next step is to create a new custom DisplayType so we can make the application to show our custom editor. To do this, log on the application with the System Administrator role on the System client and open the Reference window. Create a new reference and give it a name of your choice. As the Entity Type, chose User Maintained and for Validation Type chose DataType. After you saved the entry, click on record info button (The two small numbers in the upper right corner of the window) and note the record id of the entry.

After you created the display type, you can go to your user window and add the field for our new editor. You first need to add the column in the Table and Columns field so it is available in the Window, Tab and Field window.

IDisplayTypeFactory

Now we can create our IDisplayTypeFactory. To do this, just create a plain java class and call if for example DisplayTypeFactory. Implement the IDisplayTypeFactory (you may need to add the ord.adempiere.base plugin to you MANIFEST.MF dependency list). Afterwards, create a new component definition and give it a unqie name, chose the newly created class, add the service.ranking property with a integer value of your choice and on the services tab chose the org.adempiere.base.IDisplayTypeFactory service.

The next step is to create a constant for your display type. You can either use the ID you noted down in the last section or you can use the Query class to query the ID of your type on the fly like this:

Variant 1:
public static int MultiSelection = 1000001;

Variant 2:
public static int MultiSelection = ((X_AD_Reference)new Query(Env.getCtx(), X_AD_Reference.Table_Name, "Name='YourNameHere'", null).first()).getAD_Reference_ID();

The second approach is maybe the better one if you distribute your editor to systems where you cannot make sure that the display type will get the same ID as on your development system. The next step is to fill all the methods provided by the IDisplayTypeFactory. An even better idea would be to use the UUID of the entry in AD_Reference and use a where clause like "AD_Reference_UU='abcder...'".

IMPORTANT: Whenever you use one of the methods below to return a custom value, make sure you always check for your custom display type befor returning something because otherwise you can overwrite stuff from other display types. For example, if you return a default precision without checking for your custom display type first, the factory will return this precision for all other custom editors in your system which might result in unexpected behaviour or display errors!


isID

This method is used to determine if your editor is showing IDs of other tables. This is for example the case for the ID, Table, TableDirect, Search and other editors. This means that a ID is stored in the column and the editor is responsible for how to display the data.

isNumeric

This method is used to determine if your editor is displaying numbers. The system uses this method and others (e. g. isText()) to determine in which format it should send your editor the value of the column in the database.

getDefaultPrecision

This method returns the default precision for your editor. The system uses the return value to format the numbers it sends to your editor.

isText

This method is used to determine if your editor is displaying text. The system uses this method and others (e. g. isNumber()) to determine in which format it should send your editor the value of the column in the database.

isDate

This method is used to determine if your editor is displaying dates. The system uses this method and others (e. g. isText()) to determine in which format it should send your editor the value of the column in the database.

isLookup

This method is used to determine if your editor can provide a lookup. This is for example the case for most of the ID editors (List, Table, TableDirect and Search). The method is used for example in the DefaultLookupFactory to load a lookup for the field.

isLOB

This method is used to determine if your editor is displaying Large Object Binaries.

getNumberFormat

This method returns the number format for your editor.

getDateFormat

This method returns the date format for your editor. You should only use it if your editor is used to display dates.

getClass

This method is used to provide the java class for your editor. It is for example used in the model generator. Based on the display type of the column, the generator generates another getter/setter in the model class.

getSQLDataType

This method returns the SQL data type which your editor displays. This is for example used when you create a new column in the Application Dictionary and synchronize your column to the database.

getDescription

This method returns a description of your display type or editor. Normaly you just return the name of your display type.

Implementation

Ok, so let's implement our DisplayTypeFactory:

public class DisplayTypeFactory implements IDisplayTypeFactory{
	
//	public static int MultiSelection = 1000001;
	public static int MultiSelection = ((X_AD_Reference)new Query(Env.getCtx(), X_AD_Reference.Table_Name, "Name='MultiSelection'", null).first()).getAD_Reference_ID();
	@Override
	public boolean isID(int displayType) {
		return false;
	}

	@Override
	public boolean isNumeric(int displayType) {
		return false;
	}

	@Override
	public Integer getDefaultPrecision(int displayType) {
		return null;
	}

	@Override
	public boolean isText(int displayType) {
		if(displayType == MultiSelection)
			return true;
		return false;
	}

	@Override
	public boolean isDate(int displayType) {
		return false;
	}

	@Override
	public boolean isLookup(int displayType) {
		return false;
	}

	@Override
	public boolean isLOB(int displayType) {
		return false;
	}

	@Override
	public DecimalFormat getNumberFormat(int displayType, Language language, String pattern) {
		return null;
	}

	@Override
	public SimpleDateFormat getDateFormat(int displayType, Language language, String pattern) {
		return null;
	}

	@Override
	public Class<?> getClass(int displayType, boolean yesNoAsBoolean) {
		if(displayType == MultiSelection)
			return String.class;
		return null;
	}

	@Override
	public String getSQLDataType(int displayType, String columnName, int fieldLength) {
		if(displayType == MultiSelection)
			return "NVARCHAR2(" + fieldLength + ")";
		return null;
	}

	@Override
	public String getDescription(int displayType) {
		if(displayType == MultiSelection)
			return "MultiSelection";
		return null;
	}

}

Notice that we return false or null most of the time. Only when we really need to change something, we FIRST check for our custom display type and THEN return something other than the default value.

Create the Editor and the IEditorFactory (Web)

In this chapter, we will create our editor and our editor factory. For this, create two classes. The first one will implement the IEditorFactory. The second one will extemd the WEditor. Note that you will need a editor and a factory for each client (Web or Swing). You shouln't use the same classes for both.

IEditorFactory

The EditorFactory class implements the IEditorFactory. You also need to create a component definition with a unique name which points to your factory class. Provide the service.ranking property and add the IEditorFactory to the services. Make sure you use the right IEditorFactory interface based on which client (Web or Swing) you want to use.

Next, implement the factory, for example like this:

public class EditorFactory implements IEditorFactory{

	CLogger log = CLogger.getCLogger(EditorFactory.class);
	
	@Override
	public WEditor getEditor(GridTab gridTab, GridField gridField, boolean tableEditor) {
		if (gridField == null)
                {
                    return null;
                }

                WEditor editor = null;
                int displayType = gridField.getDisplayType();

                if(displayType == DisplayTypeFactory.MultiSelection){
        	        log.warning("MY CUSTOM MULTISELECTION DISPLAYTYPE");
        	        editor = new WMultiSelectionEditor(gridField, gridTab);
                }
                if(editor != null)
        	        editor.setTableEditor(tableEditor);
		
		return editor;
	}

}

WEditor

In the section above, we created the editor factory which returns a WMultiSelectionEditor if we have our custom display type. Now we need to implement this class. Start by extending the WEditor (Web) or by implementing the VEditor (Swing).

In our example, we have a very specific editor. It will display a list of business partners where you have a checkbox for each BP so heres how i implemented this:

public class WMultiSelectionEditor extends WEditor implements StateChangeListener{

	private CLogger log = CLogger.getCLogger(WMultiSelectionEditor.class);
	private Listbox box;

	public WMultiSelectionEditor(GridField gridField, GridTab gridTab) {
		super(new Vlayout(), gridField);

		if(gridField.getGridTab() != null){
			gridField.getGridTab().getField("AD_User_ID").addPropertyChangeListener(this);
			gridField.getGridTab().addStateChangeListener(this);
		}
		
		Vlayout layout = (Vlayout) this.getComponent();
		layout.setHeight("100px");
		layout.setWidth("100%");

		Div div = new Div();
		div.setHeight("100px");
		div.setStyle("overflow: auto");
		div.setParent(layout);

		box = new Listbox();
		box.setCheckmark(true);
		box.setMultiple(true);
		box.setParent(div);
		box.addEventListener(Events.ON_SELECT, this);

		List<MBPartner> bps = new Query(Env.getCtx(), MBPartner.Table_Name, null, null).list();
		for (MBPartner bp : bps) {
			box.appendItem(bp.getName(), bp.getC_BPartner_ID() + "");
		}
	}
	
	@Override
	public void onEvent(Event event) throws Exception {
		log.warning("------------");
		if (event.getTarget() instanceof Listbox) {
			Listbox lbox = (Listbox) event.getTarget();
			Set<Listitem> items = lbox.getSelectedItems();
			
			String where = MEVEUserBPartner.COLUMNNAME_AD_User_ID + "=?";
			List<MEVEUserBPartner> eubs = new Query(Env.getCtx(), MEVEUserBPartner.Table_Name, where, null).setParameters(gridField.getGridTab().getRecord_ID()).list();
			for(MEVEUserBPartner eub : eubs)
				eub.deleteEx(true);
			
			for (Listitem item : items) {
					MEVEUserBPartner eub = new MEVEUserBPartner(Env.getCtx(),0,null);
					eub.setAD_User_ID(gridField.getGridTab().getRecord_ID());
					eub.setC_BPartner_ID(Integer.parseInt(item.getValue().toString()));
					eub.saveEx();
			}
		}
	}

	@Override
	public void setReadWrite(boolean readWrite) {
	}

	@Override
	public boolean isReadWrite() {
		return false;
	}

	@Override
	public void setValue(Object value) {
		log.warning("-----------------");
	}

	@Override
	public Object getValue() {
		log.warning("-----------------");
		return null;
	}

	@Override
	public String getDisplay() {
		return null;
	}

	@Override
	public void stateChange(StateChangeEvent event) {
		log.warning("-----------------");
		updateSelection();
	}
	
	@Override
	public void propertyChange(PropertyChangeEvent evt) {
		log.warning("-----------------");
		updateSelection();
	}

	private void updateSelection() {
		String where = MEVEUserBPartner.COLUMNNAME_AD_User_ID + "=?";
		List<MEVEUserBPartner> eubs = new Query(Env.getCtx(), MEVEUserBPartner.Table_Name, where, null).setParameters(gridField.getGridTab().getRecord_ID()).list();
		box.clearSelection();
		Set<Listitem> items = new HashSet<Listitem>();
		for (Listitem item : box.getItems()) {
			for(MEVEUserBPartner eub : eubs)
				if(item.getValue().toString().equals(eub.getC_BPartner_ID()+"")){
					items.add(item);					
					System.out.println("Selecting Item mit ID:"+eub.getC_BPartner_ID());
				}
		}
		
		box.setSelectedItems(items);
	}
	
}


So what does this code do? Let's go over it step by step.

First we have two instance variables. One is our logger. We can use it to log stuff on the console. The second one is our Listbox in which we want to store the business partners. We wan't to access it from different methods so we made it an instance variable. The next piece of code is the constructor of the editor. It is invoked whenever our DisplayTypeFactory is called with our custom display type. What we do here is to check if we have a gridTab and if yes, we add change listeners to the AD_User_ID field which is also the record id of our destination table (Remember we want to use this editor only in the AD_User window?) The reson we register the listeners will be discussed later.

Then we create the layout for our editor. Notice that the super() method was invoked with a new Vlayout as the component so whenever we call getComponent(), it will return our VLayout. We do this and set the width and height. Then we create a overflow div so that if our list is bigger than 100px, we will see a nice scroll bar to scroll the list. Then we implement the Listbox, enable the checkboxes and multiselection and also add a event listener to the box so everytime a user checks or unchecks a checkbox, we will be notified about this.

Last but not least, we load all business partners in the system and add them to our list.

	public WMultiSelectionEditor(GridField gridField, GridTab gridTab) {
		super(new Vlayout(), gridField);

		if(gridField.getGridTab() != null){
			gridField.getGridTab().getField("AD_User_ID").addPropertyChangeListener(this);
			gridField.getGridTab().addStateChangeListener(this);
		}
		
		Vlayout layout = (Vlayout) this.getComponent();
		layout.setHeight("100px");
		layout.setWidth("100%");

		Div div = new Div();
		div.setHeight("100px");
		div.setStyle("overflow: auto");
		div.setParent(layout);

		box = new Listbox();
		box.setCheckmark(true);
		box.setMultiple(true);
		box.setParent(div);
		box.addEventListener(Events.ON_SELECT, this);

		List<MBPartner> bps = new Query(Env.getCtx(), MBPartner.Table_Name, null, null).list();
		for (MBPartner bp : bps) {
			box.appendItem(bp.getName(), bp.getC_BPartner_ID() + "");
		}
	}

The next methos is the onEvent method which get's called everytime a checkbox is checked or unchecked (notice that we registered for the ON_SELECT event. There are plenty of other events which you could register for). So what we do here is, first we check that the target of the event is indeed our Listbox. If so, we get all the selected items. Then we load all entries from our user business partner assignment table for the current user and delete them. Then, for each selected item in the Listbox, we create a new entry in our assignment table.

NOTE that this is just a quick and dirty solution. In a productive environment you may want to check agains the list which is already in the database and only delete/add those entries which aren't there already.

	@Override
	public void onEvent(Event event) throws Exception {
		log.warning("------------");
		if (event.getTarget() instanceof Listbox) {
			Listbox lbox = (Listbox) event.getTarget();
			Set<Listitem> items = lbox.getSelectedItems();
			
			String where = MEVEUserBPartner.COLUMNNAME_AD_User_ID + "=?";
			List<MEVEUserBPartner> eubs = new Query(Env.getCtx(), MEVEUserBPartner.Table_Name, where, null).setParameters(gridField.getGridTab().getRecord_ID()).list();
			for(MEVEUserBPartner eub : eubs)
				eub.deleteEx(true);
			
			for (Listitem item : items) {
					MEVEUserBPartner eub = new MEVEUserBPartner(Env.getCtx(),0,null);
					eub.setAD_User_ID(gridField.getGridTab().getRecord_ID());
					eub.setC_BPartner_ID(Integer.parseInt(item.getValue().toString()));
					eub.saveEx();
			}
		}
	}


Then we have the ReadWrite methods. These are usefull if you use a readonly logic or if you generally don't want to allow changes to your editor. Normaly you would store the ReadWrite value as an instance variable and make your editor read only or writeable in the setReadWrite() method. For this showcase, we won't use this methods but take a look at other editors to get a feeling of how to use these methods:

	@Override
	public void setReadWrite(boolean readWrite) {
	}

	@Override
	public boolean isReadWrite() {
		return false;
	}

Next we have the setValue and getValue methods which the system uses to retrieve values stored in your editor or set new values. This is for example used when your editor is shown in the grid view. The system sets the editors value and retrievs it display via getDisplay() to display a String in the grid view. In our case, we don't really need this functionality so we just add some logs:

	@Override
	public void setValue(Object value) {
		log.warning("-----------------");
	}

	@Override
	public Object getValue() {
		log.warning("-----------------");
		return null;
	}

	@Override
	public String getDisplay() {
		return null;
	}

Maybe the most interesting methods in this tutorial are the stateChange and the propertyChange methods. The propertyChagne method is called whenever the value of the grid field for AD_User_ID changes, which is mostly the case when you change the selected entry in the window. The stateChange method is called whenever the grid tabs state changes. This is for example the case when you click on the New button, the Ignore Changes button or the Refresh button. In both cases, we call the updateSelection() method which then loads all the entries from our assignment table and selects the checkboxes in the Listbox.


@Override
	public void stateChange(StateChangeEvent event) {
		log.warning("-----------------");
		updateSelection();
	}
	
	@Override
	public void propertyChange(PropertyChangeEvent evt) {
		log.warning("-----------------");
		updateSelection();
	}

	private void updateSelection() {
		String where = MEVEUserBPartner.COLUMNNAME_AD_User_ID + "=?";
		List<MEVEUserBPartner> eubs = new Query(Env.getCtx(), MEVEUserBPartner.Table_Name, where, null).setParameters(gridField.getGridTab().getRecord_ID()).list();
		box.clearSelection();
		Set<Listitem> items = new HashSet<Listitem>();
		for (Listitem item : box.getItems()) {
			for(MEVEUserBPartner eub : eubs)
				if(item.getValue().toString().equals(eub.getC_BPartner_ID()+"")){
					items.add(item);					
					System.out.println("Selecting Item mit ID:"+eub.getC_BPartner_ID());
				}
		}
		
		box.setSelectedItems(items);
	}


Improvements

Make the editor read only

So our editor is useable by now but is has some weaknesses. If you deactivate the entry, the editor is still changeable. Let's fix this. First add another instance variable to store the read only value in the setReadWrite methods. Then depending on the boolean enable or disable the editor:

	private boolean m_ReadWrite;

	@Override
	public void setReadWrite(boolean readWrite) {
		m_ReadWrite = readWrite;
		if (gridField != null && gridField.getGridTab() != null && box != null) {
			updateSelection();
			for (Listitem item : box.getItems()) {
				item.setDisabled(!readWrite);
			}
		}
	}

	@Override
	public boolean isReadWrite() {
		return m_ReadWrite;
	}

Notice: The constructor and the setReadWrite methods gets called several times. Sometimes, for example if you open a lookup, theres no gridField or gridTab available so make sure you check for null because otherwise you will get NullpointerExceptions.

Now that we have implemented the setReadWrite method, our editor also gets read only. Compare the two images and you will spot the differences:

CustomEditor2.png CustomEditor3.png

Only save entries when the save button is pressed

This one is a little bit more tricky and advanced. There are currently some editors in iDempiere which save things directly and not only when the save button of the window is pressed. An example would be the location in the business partner window. The C_BPartnerLocation entry is only saved when you hit the savebutton. However, the location itself is stored in C_Location and is saved immediatly after you hit the checkbox in the address editor. So how can we tell when to save and when not to save?

First we need a way to tell the system that we made some changes in our field. To do this, let's change the onEvent method of our editor. This method is called everytime we make a selection. So instead of saving changed immediately, let's just tell the system that we have changes. We used a 36 character for our column in the database and this has a reason. It's the length of a UUID. So this is what our onEvent method looks by now:

	@Override
	public void onEvent(Event event) throws Exception {
		log.warning("------------");
		if (event.getTarget() instanceof Listbox) {
//			Listbox lbox = (Listbox) event.getTarget();
//			Set<Listitem> items = lbox.getSelectedItems();
//
//			String where = MEVEUserBPartner.COLUMNNAME_AD_User_ID + "=?";
//			List<MEVEUserBPartner> eubs = new Query(Env.getCtx(), MEVEUserBPartner.Table_Name, where, null).setParameters(
//					gridField.getGridTab().getRecord_ID()).list();
//			for (MEVEUserBPartner eub : eubs)
//				eub.deleteEx(true);
//
//			for (Listitem item : items) {
//				MEVEUserBPartner eub = new MEVEUserBPartner(Env.getCtx(), 0, null);
//				eub.setAD_User_ID(gridField.getGridTab().getRecord_ID());
//				eub.setC_BPartner_ID(Integer.parseInt(item.getValue().toString()));
//				eub.saveEx();
//			}
			
			GridTab gridTab = gridField.getGridTab();
			gridTab.getTableModel().setCompareDB(false);
			gridTab.setValue(gridField.getColumnName(), UUID.randomUUID().toString());
		}
	}


Now everytime we make a change, the Save button get's active. Then we want to save the changes only when the save button was pressed so go to the stateChange() methods. As mentioned before, this one get's called whenever the grid tab made a change like Save, New, Delete or Refresh. Take a look at the StateChangeEvent class to see what changes can happen. Now check for the StateChangeEvent.DATA_Save event type and only then save the editors changes:

	public void stateChange(StateChangeEvent event) {
		log.warning("-----------------");

		if (event.getEventType() == StateChangeEvent.DATA_SAVE) {

			Set<Listitem> items = box.getSelectedItems();

			String where = MEVEUserBPartner.COLUMNNAME_AD_User_ID + "=?";
			List<MEVEUserBPartner> eubs = new Query(Env.getCtx(), MEVEUserBPartner.Table_Name, where, null).setParameters(
					gridField.getGridTab().getRecord_ID()).list();
			for (MEVEUserBPartner eub : eubs)
				eub.deleteEx(true);

			for (Listitem item : items) {
				MEVEUserBPartner eub = new MEVEUserBPartner(Env.getCtx(), 0, null);
				eub.setAD_User_ID(gridField.getGridTab().getRecord_ID());
				eub.setC_BPartner_ID(Integer.parseInt(item.getValue().toString()));
				eub.saveEx();
			}
		}
		updateSelection();	
	}

Start the application and test it. The save button gets available but what is that? It seems that our editor is losing the changes immediatly after we checked something. The reason behind this is, that a PropertyChangeEvent for AD_User_ID is fired beaucse the field is filled again. So how can we go around this culprit? Let's try it by storing the AD_User_ID in an instance variable and only rebuild the selection when the AD_User_ID changes:

	private int m_AD_User_ID = 0;
	@Override
	public void propertyChange(PropertyChangeEvent evt) {
		log.warning("-----------------");
		if (evt.getSource() != null && evt.getSource() instanceof GridField
				&& ((GridField) evt.getSource()).getColumnName().equalsIgnoreCase("AD_User_ID")) {
			if (evt.getNewValue() != null) {
				int user = Integer.parseInt(evt.getNewValue().toString());
				if (m_AD_User_ID != user) {
					m_AD_User_ID = user;
					updateSelection();
				}
			}
		}
	}

If we test it again, we notice that now the selection stays until we hit the save button. Then the selection is gone again. Why is this the case? Now, the onEvent() method gets called and if you notice, we previously used the Listbox from the event to determine the selected entries. Now we try to use our instance variable to access the Listbox. It seems that after the onEvent is fired, our instance variable is not yet updated when the stateChange method gets called. So our workaround could be like: When the onEvent() method is fired, we store all the selected entries in a list or a set and in the stateChanged, we use this list or whatever to create/delete our entries. This could look something like this:

	Set<Object> items = null;

	@Override
	public void onEvent(Event event) throws Exception {
		if (event.getTarget() instanceof Listbox) {
			Listbox lbox = (Listbox) event.getTarget();
			items = new HashSet<Object>();
			for(Listitem item : lbox.getSelectedItems()){
				items.add(item.getValue());
			}
			
			GridTab gridTab = gridField.getGridTab();
			gridTab.getTableModel().setCompareDB(false);
			gridTab.setValue(gridField.getColumnName(), UUID.randomUUID().toString());
		}
	}

	@Override
	public void stateChange(StateChangeEvent event) {
		if (event.getEventType() == StateChangeEvent.DATA_SAVE) {

			if(items == null)
				return;

			String where = MEVEUserBPartner.COLUMNNAME_AD_User_ID + "=?";
			List<MEVEUserBPartner> eubs = new Query(Env.getCtx(), MEVEUserBPartner.Table_Name, where, null).setParameters(
					gridField.getGridTab().getRecord_ID()).list();
			for (MEVEUserBPartner eub : eubs)
				eub.deleteEx(true);

			
			for (Object item : items) {
				MEVEUserBPartner eub = new MEVEUserBPartner(Env.getCtx(), 0, null);
				eub.setAD_User_ID(gridField.getGridTab().getRecord_ID());
				eub.setC_BPartner_ID(Integer.parseInt(item.toString()));
				eub.saveEx();
			}
		}
		if(items != null)
			items.clear();
		items = null;
		updateSelection();	
	}

Notice that we clear the items list everytime stateChanged() is called. This is necessary so that if we change the entry or hit the ignore data button, the list gets clearerd and does not affect selection of other entries in the user window.

When is which StateChangeEvent fired?

I did some investigation to find out when which kind of event is fired and heres what i think the system does:

Usecase Event
Window is opened the first time DATA_QUERY
Hit the lookup button (Magnifying Glass) DATA_SAVE
Hit the OK checkmark in the lookup DATA_QUERY
Hit the refresh button DATA_REFRESH_ALL
Change data and hit the undo button DATA_IGNORE, DATA_REFRESH
Change data and hit the save button DATA_SAVE, DATA_REFRESH_ALL
Hit the new button (either without changes or with changes) DATA_IGNORE, DATA_NEW OR DATA_SAVE, DATA_NEW
Change the current selected row (either without changes or changes) DATA_IGNORE OR DATA_SAVE

Final code for the editor

Here again, you have the final code for our example editor:

package org.evenos.editor;

import java.beans.PropertyChangeEvent;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;

import org.adempiere.webui.editor.WEditor;
import org.compiere.model.GridField;
import org.compiere.model.GridTab;
import org.compiere.model.MBPartner;
import org.compiere.model.Query;
import org.compiere.model.StateChangeEvent;
import org.compiere.model.StateChangeListener;
import org.compiere.util.CLogger;
import org.compiere.util.Env;
import org.evenos.model.MEVEUserBPartner;
import org.zkoss.zk.ui.event.Event;
import org.zkoss.zk.ui.event.Events;
import org.zkoss.zul.Div;
import org.zkoss.zul.Listbox;
import org.zkoss.zul.Listitem;
import org.zkoss.zul.Vlayout;

public class WMultiSelectionEditor extends WEditor implements StateChangeListener {

	private CLogger log = CLogger.getCLogger(WMultiSelectionEditor.class);
	private Listbox box;

	public WMultiSelectionEditor(GridField gridField, GridTab gridTab) {
		super(new Vlayout(), gridField);

		if (gridField.getGridTab() != null) {
			gridField.getGridTab().getField("AD_User_ID").addPropertyChangeListener(this);
			gridField.getGridTab().addStateChangeListener(this);
		}

		Vlayout layout = (Vlayout) this.getComponent();
		layout.setHeight("100px");
		layout.setWidth("100%");

		Div div = new Div();
		div.setHeight("100px");
		div.setStyle("overflow: auto");
		div.setParent(layout);

		box = new Listbox();
		box.setCheckmark(true);
		box.setMultiple(true);
		box.setParent(div);
		box.addEventListener(Events.ON_SELECT, this);

		List<MBPartner> bps = new Query(Env.getCtx(), MBPartner.Table_Name, null, null).list();
		for (MBPartner bp : bps) {
			box.appendItem(bp.getName(), bp.getC_BPartner_ID() + "");
		}
	}
	
	Set<Object> items = null;
	@Override
	public void onEvent(Event event) throws Exception {
		log.warning("------------");
		if (event.getTarget() instanceof Listbox) {
			Listbox lbox = (Listbox) event.getTarget();
			items = new HashSet<Object>();
			for(Listitem item : lbox.getSelectedItems()){
				items.add(item.getValue());
			}
			
			GridTab gridTab = gridField.getGridTab();
			gridTab.getTableModel().setCompareDB(false);
			gridTab.setValue(gridField.getColumnName(), UUID.randomUUID().toString());
		}
	}

	private boolean m_ReadWrite;

	@Override
	public void setReadWrite(boolean readWrite) {
		m_ReadWrite = readWrite;
		if (gridField != null && gridField.getGridTab() != null && box != null) {
			updateSelection();
			for (Listitem item : box.getItems()) {
				item.setDisabled(!readWrite);
			}
		}
	}

	@Override
	public boolean isReadWrite() {
		return m_ReadWrite;
	}

	@Override
	public void setValue(Object value) {
		log.warning("-----------------");
	}

	@Override
	public Object getValue() {
		log.warning("-----------------");
		return null;
	}

	@Override
	public String getDisplay() {
		return null;
	}

	@Override
	public void stateChange(StateChangeEvent event) {
		if (event.getEventType() == StateChangeEvent.DATA_SAVE) {

			if(items == null)
				return;

			String where = MEVEUserBPartner.COLUMNNAME_AD_User_ID + "=?";
			List<MEVEUserBPartner> eubs = new Query(Env.getCtx(), MEVEUserBPartner.Table_Name, where, null).setParameters(
					gridField.getGridTab().getRecord_ID()).list();
			for (MEVEUserBPartner eub : eubs)
				eub.deleteEx(true);

			
			for (Object item : items) {
				MEVEUserBPartner eub = new MEVEUserBPartner(Env.getCtx(), 0, null);
				eub.setAD_User_ID(gridField.getGridTab().getRecord_ID());
				eub.setC_BPartner_ID(Integer.parseInt(item.toString()));
				eub.saveEx();
			}
		}
		if(items != null)
			items.clear();
		items = null;
		updateSelection();	
	}

	private int m_AD_User_ID = 0;
	@Override
	public void propertyChange(PropertyChangeEvent evt) {
		log.warning("-----------------");
		if (evt.getSource() != null && evt.getSource() instanceof GridField
				&& ((GridField) evt.getSource()).getColumnName().equalsIgnoreCase("AD_User_ID")) {
			if (evt.getNewValue() != null) {
				int user = Integer.parseInt(evt.getNewValue().toString());
				if (m_AD_User_ID != user) {
					m_AD_User_ID = user;
					updateSelection();
				}
			}
		}
	}

	private void updateSelection() {
		String where = MEVEUserBPartner.COLUMNNAME_AD_User_ID + "=?";
		List<MEVEUserBPartner> eubs = new Query(Env.getCtx(), MEVEUserBPartner.Table_Name, where, null).setParameters(
				gridField.getGridTab().getRecord_ID()).list();
		box.clearSelection();
		Set<Listitem> items = new HashSet<Listitem>();
		for (Listitem item : box.getItems()) {
			for (MEVEUserBPartner eub : eubs)
				if (item.getValue().toString().equals(eub.getC_BPartner_ID() + "")) {
					items.add(item);
					System.out.println("Selecting Item mit ID:" + eub.getC_BPartner_ID());
				}
		}

		box.setSelectedItems(items);
	}

}


Create the Editor and the IEditorFactory (Swing)

TODO...