Skip to content
Manuel Mauky edited this page Mar 25, 2019 · 7 revisions

ModelWrapper

package: de.saxsys.mvvmfx.utils.mapping

A helper class that can be used to simplify the mapping between the ViewModel and the Model for use cases where a typical CRUD functionality is needed and there is no big difference between the structure of the model class and the view.

A typical workflow would be:

  • load an existing model instance from the backend and copy all values from the model to the properties of the ViewModel
  • the user changes the values of the viewModel properties (via the UI). The state of the underlying model instance may not be changed at this point in time!
  • when the user clicks an Apply button (and validation is successful) copy all values from the ViewModel fields into the model instance.

Additional requirements:

  • click "reset" so that the old values are back in the UI. In this case all UI fields should get the current values of the model
  • if we are creating a new model instance and the user clicks "reset" we want all UI fields to be reset to a meaningful default value

These requirements are quite common but there is a lot of code needed to copy between the model and the viewModel. Additionally we have a tight coupling because every time the structure of the model changes (for example a field is removed) we have several places in the viewModel that need to be adjusted.

This component can be used to simplify use cases like the described one and minimize the coupling between the model and the viewModel. See the following code example. First without and afterwards with the ModelWrapper.

The model class:

public class Person {
    private String name;
    private String familyName;
    private int age;

    public String getName() {
        return name;
    }
 	
    public void setName(String name) {
        this.name = name;
    }
 	
 	public String getFamilyName() {
 		return familyName;
 	}
 	
 	public void setFamilyName(String familyName) {
 		this.familyName = familyName;
 	}
 	
 	public int getAge() {
 		return age;
 	}
 	
 	public void setAge(int age) {
 		this.age = age;
 	}
 }

Without ModelWrapper:

public class PersonViewModel implements ViewModel {
 	
    private StringProperty name = new SimpleStringProperty();
 	private StringProperty familyName = new SimpleStringProperty();
 	private IntegerProperty age = new SimpleIntegerProperty();
 	
 	private Person person;
 	
 	public void init(Person person) {
 		this.person = person;
 		reloadFromModel();
 	}
 	
 	public void reset() {
 		this.name.setValue("");
 		this.familyName.setValue("");
 		this.age.setValue(0);
 	}
 	
 	public void reloadFromModel() {
 		this.name.setValue(person.getName());
 		this.familyName.setValue(person.getFamilyName());
 		this.age.setValue(person.getAge());
 	}
 	
 	public void save() {
 		if (someValidation() && person != null) {
 			person.setName(name.getValue());
 			person.setFamilyName(familyName.getValue());
 			person.setAge(age.getValue());
 		}
 	}
 	
 	public StringProperty nameProperty() {
 		return name;
 	}
 	
 	public StringProperty familyNameProperty() {
 		return familyName;
 	}
 	
 	public IntegerProperty ageProperty() {
 		return age;
 	}
}

With ModelWrapper:

public class PersonViewModel implements ViewModel {
    private ModelWrapper<Person> wrapper = new ModelWrapper<>();

    private StringProperty name = wrapper.field(Person::getName, Person::setName, "");
    private StringProperty familyName = wrapper.field(Person::getFamilyName, Person::setFamilyName, "");
    private IntegerProperty age = wrapper.field(Person::getAge, Person::setAge, 0);    

    public void setPerson(Person person) {
        wrapper.set(person);
        wrapper.reload();
    }

    public void reset() {
        wrapper.reset();
    }

    public void reloadFromModel(){
        wrapper.reload();
    }

    public void save() {
        if (someValidation()) {
            wrapper.commit();
        }
    }

    public StringProperty nameProperty(){
        return name;
    }

    public StringProperty familyNameProperty(){
        return familyName;
    }

    public IntegerProperty ageProperty() {
        return age;
    }
}

In the first example without the ModelWrapper we have several lines of code that are specific for each field of the model. If we would add a new field to the model (for example "email") then we would have to update several pieces of code in the ViewModel.

In the second example with the ModelWrapper we only define the fields with the field method of the wrapper. The handling of the save, reset and reload actions are independend from the fields. The example can be written even shorter:

public class PersonViewModel implements ViewModel {
    private ModelWrapper<Person> wrapper = new ModelWrapper<>();

    public void setPerson(Person person) {
        wrapper.set(person);
        wrapper.reload();
    }

    public void reset() {
        wrapper.reset();
    }

    public void reloadFromModel(){
        wrapper.reload();
    }

    public void save() {
        if (someValidation()) {
            wrapper.commit();
        }
    }

    public StringProperty nameProperty(){
        return wrapper.field("name", Person::getName, Person::setName, "");
    }

    public StringProperty familyNameProperty(){
        return wrapper.field("familyName", Person::getFamilyName, Person::setFamilyName, "");
    }

    public IntegerProperty ageProperty() {
        return wrapper.field("age", Person::getAge, Person::setAge, 0);
    }
}

This time we don't have fields for the wrapped properties in our ViewModel. Instead we directly call the field method of the ModelWrapper in the accessor methods. When the View binds itselfs to the ViewModel it will use these methods and the Properties will be generated on the fly.

Note that we have added an additional argument of type string as first parameter. This is an Identifier for the field. This is needed because it is possible that the accessor method is invoked multiple times and we have to take care to not create new Properties every time but instead return the same Property when the method is called a second time.

Dirty-Flag, Different-Flag

In some use cases you like to know if some values where changed by the user or not. This can be done with the observable boolean values dirtyProperty and differentProperty that the ModelWrapper provides.

Dirty-Flag The dirty flag indicates whether there was at least one property changed. As soon as as a wrapped property is changed this flag will switch to true. Only when either the commit() or reload() methods are invoked this flag will switch back to false.

Different-Flag The different flag indicates whether there is a difference between the wrapped object and the properties of the ModelWrapper instance. When a value of the property is changed this flag will switch to true. If the value is changed back to the old value so that both the wrapped object and the property have the same value again, this flag will switch back to false. It will also switch to false if either the commit() or reload() methods are invoked.