# Dynamisch Drop-Down Menüs und Formular Feld Bindings



## SubSonniC (16. Apr 2014)

Hallo,

ich habe hier ein Problem, dass ich bisher mit normalen Bordmitteln von Primefaces 4 nicht habe lösen können. Ich möchte kurz die Situation erklären und dann zu dem eigentlchen Problem kommen.

Zuersteinmal habe ich eine Klasse Kunde und eine Klasse Vertrag. Ein Kunde kann mehrere Verträge besitzen (1:n Beziehung). Um einen Kunden neu anzulegen habe ich mich für den Wizard von Primefaces entschieden. Neben den Vetragsdaten gibts noch weitere Attribute, welche hier aber nicht weiter betrachtet werden müssen. Initial bekommt jedes Kunde-Objekt ein Vertragsobjekt in seine Liste von Verträgen zugewiesen. Diese haben ja bekanntlich noch keine ID (hier ist die DB-ID und nicht die Objekt-ID gemeint). 

Im Tab des Wizards bezüglich der Vertragsdaten sind nun alle Felder des Formularteils an das Vertragsobjeckt 1 der Vertragsliste des Kundenobjekts gebunden. Falls man weitere Verträge dem Kunden hinzufügen möchte (ganz wichtig schon bei der Erstellung des Kunden) habe ich mir überlegt hierfür ein Drop-Down Menu (SelectOneMenu) nebst einem NEU-Button anzubieten. In dem Drop-Down Menu werden die bereits vorhanden Veträge mit ihrer Vertragsnummer angezeigt (Zu Begin ist hier nur ein Eintrag enthalten) Über den Button soll dann serverseitig ein neues Vertragsobjekt erzeugt und der Liste beim Kundeobjekt hinzugefügt werden und in der Managed-Bean die Variable currentCustomerAgreement mit dem neuen Vetragsobjekt belegt werden. Danach wird das Gridpanel für die Vetragsdaten neu geladen (update). Soweit funktiniert das auch und das Drop-Down-Menü zeigt dann auch auf die neue Vertragsnummer. Per onChangeEvent (<p:ajax event="change") will ich es ermöhlichen zwischen den Veträgen hin und her zu springen um nochmals Änderungen durchzuführen. Dafür müsen dann die Felder des Formulars jedes mal an das neue Vetragsobjekt gebunden werden. Das onChangeEvent feuert nur wenn auch eine tatsächliche Änderung der Drop-Down Liste vorliegt. Das wird dummerweise anhand der DB-ID des Objekt evaluiert. Deshalb habe ich initial eine Random ID erzeugt.

Doch überschreibt mir JSF bei der Serverseitigen Eventverarbeitung Das erste Vertragsobjekt mit den Daten des neuen oder umgekehrt. Je nachdem von welchem Eintrag des Drop-Down-Menus ich ausgehe. Das ist natürlich doof, da ich so niemals die Daten von zwei oder mehr Verträgen gleichzeitig pflegen kann.

Die Rheinfolge ist also:

1) Felder füllen (Vertrag 1)
2) Neuen Vertrag hinzufügen
2.1) onChangeNewCustomerAgreement() (Warum auch immer, aber nun gut)
2.2) setField von Vertrag 1
2.3) addNewCustomerAgreement()
3) Felder füllen von Vertrag 2
4) Select Vertrag 1
4.1) onChangeNewCustomerAgreement()
4.2) setField von Vertrag 1 (Hier liegt das Problem, das Object sollte eigentlich Vertrag 2 sein)

Ich habe auch schon mit dem Parameter immediate="true" herumgespielt. Das hat aber auch nicht den gewünschten Effekt erziehlt. Ich bin jetzt etwas ratlos.







Hier noch ein bisschen Code:

[XML]
<p:row>
								<p:column>
									<hutputText value="Vertragsende: " />
								</p:column>
								<p:column>
									<p:calendar tabindex="3"
										value="#{customerPM.currentCustomerAgreement.agreementEndDate}"
										id="newCustomerAgreementEnd" showOn="both" mode="popup"
										showButtonPanel="false" navigator="true"
										pattern="dd. MMMM yyyy" converterMessage="Falsches Format!"
										style="width: 260px; margin-right: 50px;" locale="de"
										converter="DateConverter" />
									<p:message for="newCustomerAgreementEnd" display="tooltip" />
								</p:column>

								<p:column id="newCustomerPowerSystemInstalledDateLabel">
									<hutputText value="Produktionsbegin: " />
									<hutputText style="color: red;" value="*" />
								</p:column>
								<p:column id="newCustomerPowerSystemInstalledDateContent">
									<p:calendar tabindex="7"
										value="#{customerPM.currentCustomerAgreement.powerSystem.installedDate}"
										id="newCustomerPowerSystemInstalledDate" showOn="both"
										mode="popup" showButtonPanel="false" navigator="true"
										required="true"
										requiredMessage="Kein Produktionsbegin angegeben!"
										pattern="dd. MMMM yyyy" converterMessage="Falsches Format!"
										style="width: 260px; margin-right: 50px;" locale="de"
										converter="DateConverter" />
									<p:message for="newCustomerPowerSystemInstalledDate"
										display="both" />
								</p:column>
							</p:row>
[/XML]


```
public void addNewCustomerAgreement(){
		CustomerAgreement customerAgreement = new CustomerAgreement("V00001-14-02");
		this.currentCustomer.getCustomerAgreements().add(customerAgreement);
		this.currentCustomerAgreement=customerAgreement;
	} 
	
	public void onChangeNewCustomerAgreement(ValueChangeEvent event){
		logger.debug("+ CustomerPM.onChangeNewCustomerAgreement()");
		this.currentCustomerAgreement=(CustomerAgreement)event.getNewValue();
		logger.debug("- CustomerPM.onChangeNewCustomerAgreement()");
	}
```


----------



## stg (17. Apr 2014)

Dein geposteter Code ist etwas dürftig, damit kann man (ich jedenfalls) dein Problem nicht nachbilden.. Interessant wäre doch zum Beispiel auch dein dropdown-Menü und der passende Code aus deiner Bean, wie der dazu passende Konverter arbeitet, wie du die Verwaltung der Verträge in der Bean organisierst usw.

Wenn ich dich richtig verstehe, liegt dein Problem aber in der _Reihenfolge_ der Teilprozesse. Vorschlag: Übermittle die Änderungen in einzelnen Eingabekomponenten sofort per AJAX. Bei der Auswahl eines neuen Vertrages submittest du auch nur den Wert des DropDown und lädst anschließend die anderen vertragsspezifischen Eingabekompenenten mit den Werten aus dem soeben ausgewählten Vertrag neu.


----------



## SubSonniC (17. Apr 2014)

Ach herje. Da ist mir ja echt mal der falsche Code reingeraten.

Hier nochmal der richtige

[XML]
		<p:column>
									<hutputText value="Vertrag: " />
								</p:column>
								<p:column colspan="4">
									<p:selectOneMenu id="newCustomerAgreement" tabindex="2"
										style="width: 190px;"
										widgetVar="newCustomerAgreementWidget"
										valueChangeListener="#{customerPM.onChangeNewCustomerAgreement}"
										converter="ObjectConverter"
										value="#{customerPM.currentCustomerAgreement}">
										<f:selectItems
											value="#{customerPM.currentCustomer.customerAgreements}"
											var="customerAgreement"
											itemLabel="#{customerAgreement.agreementNr}"
											itemValue="#{customerAgreement}" />
										<p:ajax event="change" process=":newCustomerWizardForm:newCustomerAgreementDetails" update=":newCustomerWizardForm:newCustomerAgreementDetails"/>
									</p:selectOneMenu>

									<p:commandButton
										style="position:relative; left: 5px; bottom: 9px;"
										value="Neu" icon="ui-icon-plus"
										actionListener="#{customerPM.addNewCustomerAgreement}"
										update="newCustomerAgreementDetails"
										oncomplete="newCustomerAgreementWidget.selectValue('#{customerPM.currentCustomerAgreement}');"/>
								</p:column>
[/XML]

Und der ObjectConverter


```
@FacesConverter("ObjectConverter")
public class ObjectConverter implements Converter {

    private static HashMap<String, Object> map = new HashMap<String, Object>();
    @Inject
    private transient Logger logger; 
	
    @Override
    public Object getAsObject(FacesContext context, UIComponent component, String value) {
    	if (value == null || value.isEmpty()) {
            return null;
        }
    	Object object=null;
    	try{
        	object = map.get(value);
        }catch(Exception e){
        	String error=e.getMessage();
        }
    	logger.debug(object.getClass().toString() );
        return object;
    }
   
	
    @Override
    public String getAsString(FacesContext context, UIComponent component, Object value) {
    	if (value == null || value.toString().isEmpty()) {
            return null;
        }
    	Object object=null;
    	try{
    	object = value;
        map.put(object.toString(), object);
    	}catch(Exception e){
        	String error=e.getMessage();
        }
        return object.toString();
    }
    
}
```


Und auch nochmal ein bisschen aus der Bean:


```
@Named("customerPM")
@ViewScoped
public class CustomerPM implements Serializable {
	
	private static final long serialVersionUID = 1L;

	@Inject
	private transient Logger logger;
	
	@Inject
	private transient CustomerService customerService;
	@Inject
	private transient BankService bankService;
	
	private LazyCustomerDataModel lazyModel;
	
	private Customer emptyCustomer; // Um neue Mitarbeiter anzulegen wird selectedCustomer durch emptyCustomer überschrieben
	private Customer currentCustomer; // Der aktuell betrachtete oder neu anzulegenden Mitarbeiter
	private Customer selectedCustomer;
	private List<Customer> customerList;
	private List<Bank> bankList;	
	
	private CustomerAgreement currentCustomerAgreement;

	@PostConstruct
	private void init() {
		logger.debug("+ CustomerPM.init()");
		try{
			customerList = customerService.findWithNamedQuery(Customer.FIND_ALL);
			lazyModel = new LazyCustomerDataModel(customerService);
			bankList = bankService.findWithNamedQuery(Bank.FIND_ALL);
		} catch(Exception e){
			logger.error(e.getMessage());
		}
				
		emptyCustomer = new Customer("K00001-14","V00001-14-01");
		emptyCustomer.getAddress().setCountry("Deutschland");
		currentCustomer = new Customer("K00001-14","V00001-14-01");
		currentCustomer.getAddress().setCountry("Deutschland");
		
		this.currentCustomerAgreement=this.currentCustomer.getCustomerAgreements().get(0);
		
		logger.debug("- CustomerPM.init()");
	}
```


----------



## SubSonniC (25. Apr 2014)

Ist mein Vorhaben denn wirklich so ungewöhnlich, das es bisher keine Lösung gibt?

Für alternative Umsetzungsmöglichkeiten bin ich ebenfalls offen.


Grüße


----------



## SubSonniC (24. Mai 2014)

Ich formulier mein Anliegen noch mal um.

Das Problem ist, dass das changeEvent immer vor den Aufrufen der Setter-Methoden des Vertragsobjekts ausgeführt wird. Dadurch wird das alte Objekt (event.oldValue()) nicht mit Daten gefüllt und das Neue wird mit den Daten des alten überschrieben.

Ich habe dazu jetzt eine mehr schlecht als rechte Lösung gefunden.
Ich Speichere das NewObjekt in einer TMP-Variablen (selectedCustomerAgreement) beim ChangeEvent und Kopiere es dann nachdem die Setter Methoden Aufgerufen wurden im AjaxBehaviorEvent-Handler (Das ist das explizite Ajax-Event) in das currentCustomerAgreement-Objekt.


```
public void setCurrentCustomerAgreement(CustomerAgreement currentCustomerAgreement) {
		//this.currentCustomerAgreement=currentCustomerAgreement;
	}
	
	public List<CustomerAgreement> getCurrentCustomerAgreements() {
		return currentCustomerAgreements;
	}

public void addNewCustomerAgreement(){		
		CustomerAgreement customerAgreement = new CustomerAgreement("V00001-14-02");
		this.currentCustomerAgreements.add(customerAgreement);
		this.currentCustomerAgreement=customerAgreement;
	}
	
	public void onChangeNewCustomerAgreement(AjaxBehaviorEvent event){
		this.currentCustomerAgreement=this.selectedCustomerAgreement;
	}
	
	public void onChangeNewCustomerAgreement(ValueChangeEvent event){
		this.selectedCustomerAgreement = (CustomerAgreement)event.getNewValue();
	}
```


----------

