Jako, że internety są pełne działających przykładów użycia vaadin-owej klasy CustomField

Problem

Mamy sobie do zbudowania formularz do edycji beanów z osadzonymi beanami. Przykładowo mamy klasę Human z polem Address. Pytanie jak to zrobić w Vaadin by się nie narobić?

Metody stare i pracochłonne

W dodatku niedziałające albo działające nie do końca dobrze. Pierwszą metodą jest wykorzystanie klasy Form Problem polega na tym, że w takim wypadku dostaniemy wygenerowane pole typu TextField zawierające efekty działania metody toString. Co prawda można się poratować implementując po bożemu odpowiednie fabryki, ale jako, że klasa Form jest oznaczona @Deprecated to nie ma to większego sensu (przy czym omawiane dziś rozwiązanie polega na implementacji podobnych fabryk).
Druga metoda to ręczne bindowanie pól za pomocą kodu podobnego do tego poniżej.

Listing 1. ręczny binding pól

bfg = new BeanFieldGroup<Human>(Human.class);                               
bfg.setFieldFactory(new MyFieldFactory());                                  
bfg.setItemDataSource(human);                                               
root.addComponent(bfg.buildAndBind("Imię", "firstName"));                   
root.addComponent(bfg.buildAndBind("Nazwisko", "lastName"));                
root.addComponent(bfg.buildAndBind("Miasto", "address.city"));
root.addComponent(bfg.buildAndBind("Ulica", "address.street"));
root.addComponent(bfg.buildAndBind("Numer", "address.number"));

W ogólności nie jest to zła metoda. Pozwala na tworzenie formularzy w elastyczny sposób. W dodatku szybko i prosto. Problemy związane z tą metodą są dwa. Pierwszy z nich to konieczność ręcznego przeklepania się przez model. Oznacza to iż przy bardzo złożonych formularzach pojawi się litania złożona ze wszystkich potrzebnych pół modelu. Generuje to drugi problem. Napisanie walidatora do takiego formularza zaczyna być skomplikowane ponieważ mamy do dyspozycji tylko prymitywne typy bez wzajemnych relacji jakie występują w modelu. W tym przypadku może okazać się, że podano np. numer domu który nie istnieje dla danej ulicy. Cóż… bywa… jednak znacznie lepszym rozwiązaniem było by podanie walidatora adresu jako jednego obiektu, a nie kilku niezależnych bytów. Odwołując się do otaczającego nas świata – lekarzowi znacznie łatwiej określić czy pacjent jest chory czy zdrowy jak jest w jednym kawałku, a nie w kilku.
Kod z listingu 1 posłuży nam za bazę do rozwoju formularza tak by w efekcie uzyskać kod z listingu 2.

Listing 2. Docelowy formularz

bfg = new BeanFieldGroup<Human>(Human.class);                               
bfg.setFieldFactory(new MyFieldFactory());                                  
bfg.setItemDataSource(human);                                               
root.addComponent(bfg.buildAndBind("Imię", "firstName"));                   
root.addComponent(bfg.buildAndBind("Nazwisko", "lastName"));                
root.addComponent(bfg.buildAndBind("Adres", "address", AddressField.class));

Pierwsze elementy

Na listingu 2 najważniejszą nowością jest użycie innej wersji metody buildAndBind. Metoda w wersji dwuargumentowej jako domyślnego typu pola używa klasy TextField, a tu wymuszamy użycie klasy AddressField. Zanim jednak zaimplementujemy tą klasę musimy wcześniej przygotować jeszcze jeden element. mianowicie klasę implementująca interfejs Converter.

Listing 3. Implementacja Converter

public class AddressConverter implements Converter<AddressField, Address> {

	@Override
	public Address convertToModel(AddressField value, 
		Class<? extends Address> targetType, 
		Locale locale) throws ConversionException {
		return new Address(value.getCity(), value.getStreet(), value.getNumber());
	}

	@Override
	public AddressField convertToPresentation(Address value, 
		Class<? extends AddressField> targetType, 
		Locale locale) throws ConversionException {
		AddressField af = new AddressField();
		if (value != null) {
			af.setCity(value.getCity());
			af.setStreet(value.getStreet());
			af.setNumber(value.getNumber());
		}
		return af;
	}

	@Override
	public Class<Address> getModelType() {
		return Address.class;
	}

	@Override
	public Class<AddressField> getPresentationType() {
		return AddressField.class;
	}
}

Zadaniem tej klasy jest zamienianie obiektu modelu na obiekt prezentacji (\*Field). Dodatkowo Vaadin posiada na pokładzie klasę ReverseConverter, która jest wrapperem zamieniającym działanie konwertera na odwrotne.
Sam konwerter musi zostać jeszcze podpięty do klasy implementującej ConverterFactory. Nie jest to skomplikowane, ale wymaga dodania własnej implementacji, która rozszerzy DefaultConverterFactory.

Listing 4. Rozszerzenie DefaultConverterFactory

public class MyConverterFactory extends DefaultConverterFactory {

	private MultiKeyMap additionalConverters = new MultiKeyMap();

	public <PRESENTATION, MODEL> 
		void addConverter(Converter<PRESENTATION, MODEL> converter) {
			additionalConverters.put(converter.getPresentationType(), 
				converter.getModelType(), 
				converter);
	}

	@Override
	public <PRESENTATION, MODEL> 
		Converter<PRESENTATION, MODEL> createConverter(
			Class<PRESENTATION> presentationClass, 
			Class<MODEL> modelClass) {
		Converter<PRESENTATION, MODEL> converter = 
			super.createConverter(presentationClass, modelClass);
		if (converter == null) {
			converter = (Converter<PRESENTATION, MODEL>) 
				additionalConverters.get(presentationClass, modelClass);
		}
		return converter;
	}
}

Metoda addConverter to dodatkowa funkcjonalność dla naszej fabryki. Pozwoli na dodawanie kolejnych konwerterów. Można to oczywiście załatwić inaczej wykorzystując np. IoC i wstrzykując wcześniej przygotowaną listę. Jako kontenera na nasze konwertery używam multimapy.
Fabryka ta musi zostać każdorazowo dodana do sesji przy starcie aplikacji. Możemy to zrobić w metodzie init dla głównej klasy UI. Nie o tym jednak.
Kolejnym krokiem jest przygotowanie własnej fabryki pól. Jest to niezbędny element ponieważ ta domyślna nie potrafi tworzyć obiektów z klas rozszerzających CustomField.

Listing 5. Własna fabryka pól

public class MyFieldFactory implements FieldGroupFieldFactory {
	private DefaultFieldGroupFieldFactory dff = new DefaultFieldGroupFieldFactory();

	@Override
	public <T extends Field> T createField(Class<?> dataType, Class<T> fieldType) {
		T field = dff.createField(dataType, fieldType);
		if (field == null) {
			try {
				field = fieldType.newInstance();
			} catch (Exception e) {
				throw new FieldGroup.BindException("Could not create a field of type "
						+ fieldType, e);
			}
		}
		return field;
	}
}

Tu zasada działania jest banalnie prosta. Jeżeli domyślna fabryka nie daje rady to próbujemy coś ugrać za pomocą refleksji.
Ostatnim krokiem jest stworzenie formularza.

Listing 6. Własna klasa formularza

public class AddressField extends CustomField<Address> {

	private final Panel panel;
	private TextField cityField = new TextField("Miasto");
	private TextField streetField = new TextField("Ulica");
	private TextField numberField = new TextField("Numer Domu");
	private Set<TextField> fields = new HashSet<TextField>();
	{
		fields.add(cityField);
		fields.add(streetField);
		fields.add(numberField);
	}


	public AddressField() {
		super();
		panel = new Panel();
		FormLayout root = new FormLayout();
		root.addComponent(cityField);
		root.addComponent(streetField);
		root.addComponent(numberField);
		panel.setContent(root);
		setImmediate(true);
	}

	@Override
	public void setImmediate(final boolean immediate) {
		super.setImmediate(immediate);
		Collections2.filter(fields, new Predicate<TextField>() {
			@Override
			public boolean apply(@Nullable TextField field) {
				 field.setImmediate(immediate);
				return true;
			}
		});
	}

	@Override
	protected Component initContent() {
		return panel;
	}

	@Override
	@SuppressWarnings("unchecked")
	public Property getPropertyDataSource() {
		Property<Address> pds = super.getPropertyDataSource();
		pds.setValue(getAddress());
		return pds;
	}

	@Override
	public void setPropertyDataSource(Property newDataSource) {
		super.setPropertyDataSource(newDataSource);
		setAddress((Address) newDataSource.getValue());
	}

	@Override
	public Class<? extends Address> getType() {
		return Address.class;
	}
// metody do obsługi konkretnych pól i "magi modelowej"
}

Tu kilka rzeczy obowiązkowych. Po pierwsze implementacja initContent jest wymagana. Musi zwracać główny element naszego pola. Po drugie set/getPropertyDataSource jest u mnie zahakierowane tak by binding działał w poprawny sposób. Podobno da się inaczej, ale internet milczy na ten temat. getType – obowiązkowe. Dodatkowy Set dla pól pozwala na wykonanie takich magii jak w setImmediate, czy jak chcemy używać np. setRequired dla wszystkich pól. No i oczywiście konstruktor bezargumentowy by refleksja działała.

Podsumowanie

Fabryki robimy tylko raz. Z rzeczy dodatkowych musimy napisać konwerter. No i oczywiście samą klasę pola. Zatem ilość dodatkowej roboty przy bardziej rozbudowanym systemie nie jest jakoś przytłaczająca.
Kod do pobrania z
https://github.com/Koziolek/vaadin-pojo-form.git