CustomField w Vaadin – bo w sieci nie ma

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

6 myśli na temat “CustomField w Vaadin – bo w sieci nie ma

  1. Hej,
    bardzo dobry wpis.
    Pobrałem przykład z gita, pokombinowałem i coś mi nie pasuje.
    W kodzie servletu całkowicie wyłączyłem ustawienie converterFactory – nie widać żadnej różnicy 🙂
    Druga sprawa, skoro kombinujemy z converterFactory czemu jawnie mówimy z jakiego typu pola chcemy skorzystać w buildAndBind?

  2. ad 1. w sensie wykomentowałeś linijkę wu klasie MyVaadinUI?
    ad 2. BY nie złapał tego domyślny konwerter. Przykład jest generalnie mocno uproszczony.

  3. tak, w MyVaadinUI.
    Skoro kombinujemy z konwerterami spodziewam się, że buildAndBind poprzez konwerter znajdzie odpowiednią klasę prezentującą.

    Kombinuję jeszcze inaczej.
    Zostawiam całą implementację zgodnie z Twoim przykładem oprócz jednej zmiany:

    root.addComponent(bfg.buildAndBind(„Adres”, „address”));

    Efektem jest poniższy wyjątek:
    Unable to convert value of type pl.koziolekweb.vaadin.pojoform.model.Address to presentation type class java.lang.String. No converter is set and the types are not compatible

    Tutaj właśnie spodziewam się, że converterFactory za pośrednictwem AddressConverter poda AddressField. Niestety tak się nie dzieje.

    Mogę prosić o pociągnięcie tematu dalej?

  4. jako update do komentarza.
    Zmieniłem MyFieldFactory zgodnie z poniższym:
    public class MyFieldFactory extends DefaultFieldGroupFieldFactory {
    @Override
    public T createField(Class type, Class fieldType) {
    if (Address.class.isAssignableFrom(type)) {
    return (T) new AddressField();
    }
    return createDefaultField(type, fieldType);
    }
    }

    Udało mi się uzyskać zamierzony efekt.
    Mimo wszystko proszę o dalsze części dotyczące Vaadin 🙂

Napisz odpowiedź

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *

To create code blocks or other preformatted text, indent by four spaces:

    This will be displayed in a monospaced font. The first four 
    spaces will be stripped off, but all other whitespace
    will be preserved.
    
    Markdown is turned off in code blocks:
     [This is not a link](http://example.com)

To create not a block, but an inline code span, use backticks:

Here is some inline `code`.

For more help see http://daringfireball.net/projects/markdown/syntax