Permalink
Browse files

Add MemberChangedEvent to percolate changes up the model tree.

Models convert property events to member changes, and ListProperties, if they
contain models, forward member changes on to their enclosing models.

This means one can listen to a parent model and know when any of the parent's or
children's properties changed.

Also, added a test for derived values that are based on the contents of list,
which are themselves models. Turns out this already worked, because the implicit
dependency tracking setup both the list and any model property the derived value
accessed as upstream properties.
  • Loading branch information...
stephenh committed Aug 18, 2012
1 parent fbfbb84 commit 47b0c12fe864a7ba1848fece486a13d8d01242f5
@@ -1,13 +1,25 @@
package org.tessell.model;

import org.tessell.model.events.HasMemberChangedHandlers;
import org.tessell.model.events.MemberChangedEvent;
import org.tessell.model.events.MemberChangedHandler;
import org.tessell.model.events.PropertyChangedEvent;
import org.tessell.model.events.PropertyChangedHandler;
import org.tessell.model.properties.Property;
import org.tessell.model.properties.PropertyGroup;

import com.google.gwt.event.shared.EventBus;
import com.google.gwt.event.shared.GwtEvent;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.event.shared.SimplerEventBus;

/** A base class for models. Provides a {@link PropertyGroup} for all of the properties. */
public abstract class AbstractModel implements Model {

protected PropertyGroup all = new PropertyGroup("all", "model invalid");
private final PropertyGroup all = new PropertyGroup("all", "model invalid");
private final EventBus handlers = new SimplerEventBus();

// eventually may not be public?
public PropertyGroup all() {
return all;
}
@@ -17,10 +29,31 @@ public PropertyGroup all() {
return all;
}

@Override
public HandlerRegistration addMemberChangedHandler(MemberChangedHandler handler) {
return handlers.addHandler(MemberChangedEvent.getType(), handler);
}

/** Adds {@code p} to the property group. */
protected <P extends Property<?>> P add(P p) {
protected <P extends Property<U>, U> P add(P p) {
p.addPropertyChangedHandler(new PropertyChangedHandler<U>() {
public void onPropertyChanged(PropertyChangedEvent<U> event) {
fireEvent(new MemberChangedEvent());
}
});
if (p instanceof HasMemberChangedHandlers) {
// forward on member changes up the tree, e.g. from ListProperties
((HasMemberChangedHandlers) p).addMemberChangedHandler(new MemberChangedHandler() {
public void onMemberChanged(MemberChangedEvent event) {
fireEvent(event);
}
});
}
all.add(p);
return p;
}

protected void fireEvent(GwtEvent<?> event) {
handlers.fireEvent(event);
}
}
@@ -1,9 +1,10 @@
package org.tessell.model;

import org.tessell.model.events.HasMemberChangedHandlers;
import org.tessell.model.properties.Property;

/** A model is an entity that contains a number of properties. */
public interface Model {
public interface Model extends HasMemberChangedHandlers {

Property<Boolean> allValid();

@@ -0,0 +1,9 @@
package org.tessell.model.events;

import com.google.gwt.event.shared.HandlerRegistration;

public interface HasMemberChangedHandlers {

HandlerRegistration addMemberChangedHandler(MemberChangedHandler handler);

}
@@ -0,0 +1,7 @@
package org.tessell.model.events;

import org.tessell.GenEvent;

@GenEvent(gwtEvent = true)
public class MemberChangedEventSpec {
}
@@ -8,6 +8,9 @@
import java.util.Collections;
import java.util.List;

import org.tessell.model.events.HasMemberChangedHandlers;
import org.tessell.model.events.MemberChangedEvent;
import org.tessell.model.events.MemberChangedHandler;
import org.tessell.model.events.ValueAddedEvent;
import org.tessell.model.events.ValueAddedHandler;
import org.tessell.model.events.ValueRemovedEvent;
@@ -19,7 +22,7 @@

import com.google.gwt.event.shared.HandlerRegistration;

public class ListProperty<E> extends AbstractProperty<List<E>, ListProperty<E>> {
public class ListProperty<E> extends AbstractProperty<List<E>, ListProperty<E>> implements HasMemberChangedHandlers {

private IntegerProperty size;
private List<E> readOnly;
@@ -59,6 +62,7 @@ public ListProperty(final Value<? extends List<E>> value) {
public void add(final E item) {
getDirect().add(item);
setTouched(true);
listenForMemberChanged(item);
// will fire add+change if needed
reassess();
}
@@ -68,8 +72,11 @@ public void addAll(Collection<? extends E> items) {
if (items.size() == 0) {
return; // this makes sense, right?
}
setTouched(true);
setTouched(true); // move this to be after the addAll to match add?
getDirect().addAll(items);
for (E item : items) {
listenForMemberChanged(item);
}
// will fire adds+change if needed
reassess();
}
@@ -113,6 +120,11 @@ public HandlerRegistration addValueRemovedHandler(final ValueRemovedHandler<E> h
return addHandler(ValueRemovedEvent.getType(), handler);
}

/** Registers {@code handler} to be called when values changed. */
public HandlerRegistration addMemberChangedHandler(final MemberChangedHandler handler) {
return addHandler(MemberChangedEvent.getType(), handler);
}

/**
* Creates a new {@link ListProperty>} of type {@code F}.
*
@@ -220,4 +232,18 @@ protected void fireChanged(List<E> oldValue, List<E> newValue) {
return super.get();
}

// Forwards member changed events on our models to our own model
private void listenForMemberChanged(final E item) {
if (item instanceof HasMemberChangedHandlers) {
((HasMemberChangedHandlers) item).addMemberChangedHandler(new MemberChangedHandler() {
public void onMemberChanged(MemberChangedEvent event) {
// in case the item was removed, we don't currently unsubscribe
if (getDirect().contains(item)) {
fireEvent(event);
}
}
});
}
}

}
@@ -6,8 +6,12 @@
import static org.tessell.model.properties.NewProperty.stringProperty;

import org.junit.Test;
import org.tessell.model.AbstractDtoModel;
import org.tessell.model.AbstractModel;
import org.tessell.model.events.MemberChangedEvent;
import org.tessell.model.events.MemberChangedHandler;
import org.tessell.model.properties.IntegerProperty;
import org.tessell.model.properties.ListProperty;
import org.tessell.model.properties.NewProperty;
import org.tessell.model.properties.StringProperty;
import org.tessell.model.validation.Valid;
import org.tessell.model.validation.events.RuleTriggeredEvent;
@@ -64,22 +68,46 @@ public void onTrigger(RuleTriggeredEvent event) {
assertThat(message[0], is("model invalid"));
}

public static class EmployeeModel extends AbstractDtoModel<EmployeeDto> {
public final IntegerProperty id = integerProperty("id").req().in(all);
public final StringProperty name = stringProperty("name").req().in(all);
public final StringProperty address = stringProperty("address").in(all);
@Test
public void firesMemberChangedEvent() {
final int[] fires = { 0 };
EmployeeModel m = new EmployeeModel();
m.addMemberChangedHandler(new MemberChangedHandler() {
public void onMemberChanged(MemberChangedEvent event) {
fires[0]++;
}
});
m.name.set("asdf");
assertThat(fires[0], is(1));
m.address.set("asdf");
assertThat(fires[0], is(2));
}

@Override
public void merge(EmployeeDto dto) {
}
@Test
public void firesMemberChangedEventForListProperties() {
final int[] fires = { 0 };
EmployeeModel m = new EmployeeModel();
m.addMemberChangedHandler(new MemberChangedHandler() {
public void onMemberChanged(MemberChangedEvent event) {
fires[0]++;
}
});
AccountModel a = new AccountModel();
m.accounts.add(a);
assertThat(fires[0], is(1));
a.name.set("asdf");
assertThat(fires[0], is(2));
}

@Override
public EmployeeDto getDto() {
return null;
}
public static class EmployeeModel extends AbstractModel {
public final IntegerProperty id = add(integerProperty("id").req());
public final StringProperty name = add(stringProperty("name").req());
public final StringProperty address = add(stringProperty("address"));
public final ListProperty<AccountModel> accounts = add(NewProperty.<AccountModel> listProperty("accounts"));
}

public static class EmployeeDto {
public static class AccountModel extends AbstractModel {
public final StringProperty name = add(stringProperty("name"));
}

}
@@ -7,6 +7,12 @@

public class DummyModel extends AbstractModel {

public final StringProperty name = stringProperty("name").max(50);
public final StringProperty name = add(stringProperty("name").max(50));

public DummyModel() {
}

public DummyModel(String name) {
this.name.set(name);
}
}
@@ -6,13 +6,16 @@
import static org.hamcrest.Matchers.contains;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import static org.tessell.model.properties.NewProperty.integerProperty;
import static org.tessell.model.properties.NewProperty.listProperty;

import java.util.ArrayList;
import java.util.List;

import org.junit.Before;
import org.junit.Test;
import org.tessell.model.events.MemberChangedEvent;
import org.tessell.model.events.MemberChangedHandler;
import org.tessell.model.events.PropertyChangedEvent;
import org.tessell.model.events.PropertyChangedHandler;
import org.tessell.model.events.ValueAddedEvent;
@@ -22,6 +25,8 @@
import org.tessell.model.properties.IntegerProperty;
import org.tessell.model.properties.ListProperty;
import org.tessell.model.properties.ListProperty.ElementConverter;
import org.tessell.model.properties.Property;
import org.tessell.model.values.DerivedValue;
import org.tessell.model.values.SetValue;

public class ListPropertyTest {
@@ -279,6 +284,58 @@ public void passedANullListDoesNotNpe() {
assertThat(l.get(), is(nullValue()));
}

@Test
public void firesMemberChanged() {
final int[] fires = { 0 };
ListProperty<DummyModel> models = listProperty("models");
models.addMemberChangedHandler(new MemberChangedHandler() {
public void onMemberChanged(MemberChangedEvent event) {
fires[0]++;
}
});
DummyModel m1 = new DummyModel();
models.add(m1);
assertThat(fires[0], is(0));
m1.name.set("adsf");
assertThat(fires[0], is(1));
}

@Test
public void derivedValueOfMembers() {
final ListProperty<DummyModel> models = listProperty("models");
Property<Integer> startsWithFoo = integerProperty(new DerivedValue<Integer>("startsWithFoo") {
public Integer get() {
int i = 0;
for (DummyModel model : models.get()) {
if (model.name.get() != null && model.name.get().startsWith("foo")) {
i++;
}
}
return i;
}
});
assertThat(startsWithFoo.get(), is(0));
// watch for when foo know's it has changed
final int[] changed = { 0 };
startsWithFoo.addPropertyChangedHandler(new PropertyChangedHandler<Integer>() {
public void onPropertyChanged(PropertyChangedEvent<Integer> event) {
changed[0]++;
}
});
// adding a non-foo doesn't change it
models.add(new DummyModel("bar"));
assertThat(changed[0], is(0));
// adding a foo does change it
models.add(new DummyModel("foo"));
assertThat(changed[0], is(1));
// changing a non-foo to a foo does change it
models.get().get(0).name.set("foot");
assertThat(changed[0], is(2));
// removing a foo does change it
models.remove(models.get().get(1));
assertThat(changed[0], is(3));
}

public static class CountingChanges<P> implements PropertyChangedHandler<P> {
public int count;

0 comments on commit 47b0c12

Please sign in to comment.