Using GWT Editors with a complex usecase
Asked Answered
R

4

12

I'm trying to create a page which is very similar to the Google Form creation page.

enter image description here

This is how I am attempting to model it using the GWT MVP framework (Places and Activities), and Editors.

CreateFormActivity (Activity and presenter)

CreateFormView (interface for view, with nested Presenter interface)

CreateFormViewImpl (implements CreateFormView and Editor< FormProxy >

CreateFormViewImpl has the following sub-editors:

  • TextBox title
  • TextBox description
  • QuestionListEditor questionList

QuestionListEditor implements IsEditor< ListEditor< QuestionProxy, QuestionEditor>>

QuestionEditor implements Editor < QuestionProxy> QuestionEditor has the following sub-editors:

  • TextBox questionTitle
  • TextBox helpText
  • ValueListBox questionType
  • An optional subeditor for each question type below.

An editor for each question type:

TextQuestionEditor

ParagraphTextQuestionEditor

MultipleChoiceQuestionEditor

CheckboxesQuestionEditor

ListQuestionEditor

ScaleQuestionEditor

GridQuestionEditor


Specific Questions:

  1. What is the correct way to add / remove questions from the form. (see follow up question)
  2. How should I go about creating the Editor for each question type? I attempted to listen to the questionType value changes, I'm not sure what to do after. (answered by BobV)
  3. Should each question-type-specific editor be wrapper with an optionalFieldEditor? Since only one of can be used at a time. (answered by BobV)
  4. How to best manage creating/removing objects deep in the object hierarchy. Ex) Specifying answers for a question number 3 which is of type multiple choice question. (see follow up question)
  5. Can OptionalFieldEditor editor be used to wrap a ListEditor? (answered by BobV)

Implementation based on Answer

The Question Editor

public class QuestionDataEditor extends Composite implements
CompositeEditor<QuestionDataProxy, QuestionDataProxy, Editor<QuestionDataProxy>>,
LeafValueEditor<QuestionDataProxy>, HasRequestContext<QuestionDataProxy> {

interface Binder extends UiBinder<Widget, QuestionDataEditor> {}

private CompositeEditor.EditorChain<QuestionDataProxy, Editor<QuestionDataProxy>> chain;

private QuestionBaseDataEditor subEditor = null;
private QuestionDataProxy currentValue = null;
@UiField
SimplePanel container;

@UiField(provided = true)
@Path("dataType")
ValueListBox<QuestionType> dataType = new ValueListBox<QuestionType>(new Renderer<QuestionType>() {

    @Override
    public String render(final QuestionType object) {
        return object == null ? "" : object.toString();
    }

    @Override
    public void render(final QuestionType object, final Appendable appendable) throws IOException {
        if (object != null) {
            appendable.append(object.toString());
        }
    }
});

private RequestContext ctx;

public QuestionDataEditor() {
    initWidget(GWT.<Binder> create(Binder.class).createAndBindUi(this));
    dataType.setValue(QuestionType.BooleanQuestionType, true);
    dataType.setAcceptableValues(Arrays.asList(QuestionType.values()));

    /*
     * The type drop-down UI element is an implementation detail of the
     * CompositeEditor. When a question type is selected, the editor will
     * call EditorChain.attach() with an instance of a QuestionData subtype
     * and the type-specific sub-Editor.
     */
    dataType.addValueChangeHandler(new ValueChangeHandler<QuestionType>() {
        @Override
        public void onValueChange(final ValueChangeEvent<QuestionType> event) {
            QuestionDataProxy value;
            switch (event.getValue()) {

            case MultiChoiceQuestionData:
                value = ctx.create(QuestionMultiChoiceDataProxy.class);
                setValue(value);
                break;

            case BooleanQuestionData:
            default:
                final QuestionNumberDataProxy value2 = ctx.create(BooleanQuestionDataProxy.class);
                value2.setPrompt("this value doesn't show up");
                setValue(value2);
                break;

            }

        }
    });
}

/*
 * The only thing that calls createEditorForTraversal() is the PathCollector
 * which is used by RequestFactoryEditorDriver.getPaths().
 * 
 * My recommendation is to always return a trivial instance of your question
 * type editor and know that you may have to amend the value returned by
 * getPaths()
 */
@Override
public Editor<QuestionDataProxy> createEditorForTraversal() {
    return new QuestionNumberDataEditor();
}

@Override
public void flush() {
    //XXX this doesn't work, no data is returned
    currentValue = chain.getValue(subEditor);
}

/**
 * Returns an empty string because there is only ever one sub-editor used.
 */
@Override
public String getPathElement(final Editor<QuestionDataProxy> subEditor) {
    return "";
}

@Override
public QuestionDataProxy getValue() {
    return currentValue;
}

@Override
public void onPropertyChange(final String... paths) {
}

@Override
public void setDelegate(final EditorDelegate<QuestionDataProxy> delegate) {
}

@Override
public void setEditorChain(final EditorChain<QuestionDataProxy, Editor<QuestionDataProxy>> chain) {
    this.chain = chain;
}

@Override
public void setRequestContext(final RequestContext ctx) {
    this.ctx = ctx;
}

/*
 * The implementation of CompositeEditor.setValue() just creates the
 * type-specific sub-Editor and calls EditorChain.attach().
 */
@Override
public void setValue(final QuestionDataProxy value) {

    // if (currentValue != null && value == null) {
    chain.detach(subEditor);
    // }

    QuestionType type = null;
    if (value instanceof QuestionMultiChoiceDataProxy) {
        if (((QuestionMultiChoiceDataProxy) value).getCustomList() == null) {
            ((QuestionMultiChoiceDataProxy) value).setCustomList(new ArrayList<CustomListItemProxy>());
        }
        type = QuestionType.CustomList;
        subEditor = new QuestionMultipleChoiceDataEditor();

    } else {
        type = QuestionType.BooleanQuestionType;
        subEditor = new BooleanQuestionDataEditor();
    }

    subEditor.setRequestContext(ctx);
    currentValue = value;
    container.clear();
    if (value != null) {
        dataType.setValue(type, false);
        container.add(subEditor);
        chain.attach(value, subEditor);
    }
}

}

Question Base Data Editor

public interface QuestionBaseDataEditor extends HasRequestContext<QuestionDataProxy>,                         IsWidget {


}

Example Subtype

public class BooleanQuestionDataEditor extends Composite implements QuestionBaseDataEditor {
interface Binder extends UiBinder<Widget, BooleanQuestionDataEditor> {}

@Path("prompt")
@UiField
TextBox prompt = new TextBox();

public QuestionNumberDataEditor() {
    initWidget(GWT.<Binder> create(Binder.class).createAndBindUi(this));
}

@Override
public void setRequestContext(final RequestContext ctx) {

}
}

The only issue left is that QuestionData subtype specific data isn't being displayed, or flushed. I think it has to do with the Editor setup I'm using.

For example, The value for prompt in the BooleanQuestionDataEditor is neither set nor flushed, and is null in the rpc payload.

My guess is: Since the QuestionDataEditor implements LeafValueEditor, the driver will not visit the subeditor, even though it has been attached.

Big thanks to anyone who can help!!!

Rooseveltroost answered 12/8, 2011 at 17:9 Comment(1)
You are essentially adding two editors to the editor chain for the same value. First, the main editor is attached and then the sub-editor. Have you tried detaching the main editor before attaching the sub-editor?Orchidaceous
P
9

Fundamentally, you want a CompositeEditor to handle cases where objects are dynamically added or removed from the Editor hierarchy. The ListEditor and OptionalFieldEditor adaptors implement CompositeEditor.

If the information required for the different types of questions is fundamentally orthogonal, then multiple OptionalFieldEditor could be used with different fields, one for each question type. This will work when you have only a few question types, but won't really scale well in the future.

A different approach, that will scale better would be to use a custom implementation of a CompositeEditor + LeafValueEditor that handles a polymorphic QuestionData type hierarchy. The type drop-down UI element would become an implementation detail of the CompositeEditor. When a question type is selected, the editor will call EditorChain.attach() with an instance of a QuestionData subtype and the type-specific sub-Editor. The newly-created QuestionData instance should be retained to implement LeafValueEditor.getValue(). The implementation of CompositeEditor.setValue() just creates the type-specific sub-Editor and calls EditorChain.attach().

FWIW, OptionalFieldEditor can be used with ListEditor or any other editor type.

Pippa answered 15/8, 2011 at 13:15 Comment(9)
Thanks for the reply! This approach looks much cleaner. I'll give it a try and post my results today.Rooseveltroost
What should be returned for CompositeEditor.createEditorForTraversal() ?Rooseveltroost
The only thing that calls createEditorForTraversal() is the PathCollector which is used by RequestFactoryEditorDriver.getPaths(). My recommendation is to always return a trivial instance of your question type editor and know that you may have to amend the value returned by getPaths().Pippa
To load data for a specific subtype of QuestionData, the subtype-specific path needs to be appended to Request.with(), but the QuestionData type isn't know until the data is loaded. So, Should all the possible QuestionData-subtype paths be appended while loading data?Rooseveltroost
Adding all possible sub-paths in the with() call won't break anything on the server; it will ignore paths that don't exist in an object. Sounds like a feature request to allow a with("foo.*") syntax to avoid the need to over-specify fields in polymorphic return cases.Pippa
Thanks! feature request created code.google.com/p/google-web-toolkit/issues/detail?id=6698Rooseveltroost
Hey Bob, I almost got it working, do you see what I'm doing wrong?Rooseveltroost
The GWT generator doesn't support polymorphic editor types (I thought that had been done). Instead, you could create a separate SimpleBeanEditorDriver interface for each mapped-in editor subtype and manage the sub-driver's edit()/flush() cycle based on calls to the fan-out Editor. Filed separate issue 6719 for Editor polymorphism.Pippa
@Pippa I wish I had seen this comment before my answer below but it was folded by stackoverflow ;) Thanks for filing the issue.Bonnet
P
2

We implemented similar approach (see accepted answer) and it works for us like this.

Since driver is initially unaware of simple editor paths that might be used by sub-editors, every sub-editor has own driver:

public interface CreatesEditorDriver<T> {
    RequestFactoryEditorDriver<T, ? extends Editor<T>> createDriver();
}

public interface RequestFactoryEditor<T> extends CreatesEditorDriver<T>, Editor<T> {
}

Then we use the following editor adapter that would allow any sub-editor that implements RequestFactoryEditor to be used. This is our workaround to support polimorphism in editors:

public static class DynamicEditor<T>
        implements LeafValueEditor<T>, CompositeEditor<T, T, RequestFactoryEditor<T>>, HasRequestContext<T> {

    private RequestFactoryEditorDriver<T, ? extends Editor<T>> subdriver;

    private RequestFactoryEditor<T> subeditor;

    private T value;

    private EditorDelegate<T> delegate;

    private RequestContext ctx;

    public static <T> DynamicEditor<T> of(RequestFactoryEditor<T> subeditor) {
        return new DynamicEditor<T>(subeditor);
    }

    protected DynamicEditor(RequestFactoryEditor<T> subeditor) {
        this.subeditor = subeditor;
    }

    @Override
    public void setValue(T value) {
        this.value = value;

        subdriver = null;

        if (null != value) {
            RequestFactoryEditorDriver<T, ? extends Editor<T>> newSubdriver = subeditor.createDriver();

            if (null != ctx) {
                newSubdriver.edit(value, ctx);
            } else {
                newSubdriver.display(value);
            }

            subdriver = newSubdriver;
        }
    }

    @Override
    public T getValue() {
        return value;
    }

    @Override
    public void flush() {
        if (null != subdriver) {
            subdriver.flush();
        }
    }

    @Override
    public void onPropertyChange(String... paths) {
    }

    @Override
    public void setDelegate(EditorDelegate<T> delegate) {
        this.delegate = delegate;
    }

    @Override
    public RequestFactoryEditor<T> createEditorForTraversal() {
        return subeditor;
    }

    @Override
    public String getPathElement(RequestFactoryEditor<T> subEditor) {
        return delegate.getPath();
    }

    @Override
    public void setEditorChain(EditorChain<T, RequestFactoryEditor<T>> chain) {
    }

    @Override
    public void setRequestContext(RequestContext ctx) {
        this.ctx = ctx;
    }
}

Our example sub-editor:

public static class VirtualProductEditor implements RequestFactoryEditor<ProductProxy> {
        interface Driver extends RequestFactoryEditorDriver<ProductProxy, VirtualProductEditor> {}

        private static final Driver driver = GWT.create(Driver.class);

    public Driver createDriver() {
        driver.initialize(this);
        return driver;
    }
...
}

Our usage example:

        @Path("")
        DynamicEditor<ProductProxy> productDetailsEditor;
        ...
        public void setProductType(ProductType type){
            if (ProductType.VIRTUAL==type){
                productDetailsEditor = DynamicEditor.of(new VirtualProductEditor());

            } else if (ProductType.PHYSICAL==type){
                productDetailsEditor = DynamicEditor.of(new PhysicalProductEditor());
            }
        }

Would be great to hear your comments.

Predesignate answered 26/3, 2012 at 16:34 Comment(2)
Your approach is definitely a valid one. We did something similar to start for our editors -- creating a new driver for every sub-editor and binding them together. I don't like this approach anymore because I have fallen in love with editor visitors, and the power they bring. For example; dirty detection. Instead, I believe that you can accomplish the same task using a set of composite editors.Holohedral
A good implementation of the suggestion in this commentHolohedral
B
1

Regarding your question why subtype specific data isn't displayed or flushed:

My scenario is a little bit different but I made the following observation:

GWT editor databinding does not work as one would expect with abstract editors in the editor hierarchy. The subEditor declared in your QuestionDataEditor is of type QuestionBaseDataEditor and this is fully abstract type (an interface). When looking for fields/sub editors to populate with data/flush GWT takes all the fields declared in this type. Since QuestionBaseDataEditor has no sub editors declared nothing is displayed/flushed. From debugging I found out that is happens due to GWT using a generated EditorDelegate for that abstract type rather than the EditorDelegate for the concrete subtype present at that moment.

In my case all the concrete sub editors had the same types of leaf value editors (I had two different concrete editors one to display and one to edit the same bean type) so I could do something like this to work around this limitation:

interface MyAbstractEditor1 extends Editor<MyBean>
{
    LeafValueEditor<String> description();
}

// or as an alternative

abstract class MyAbstractEditor2 implements Editor<MyBean>
{
    @UiField protected LeafValueEditor<String> name;
}


class MyConcreteEditor extends MyAbstractEditor2 implements MyAbstractEditor1
{
    @UiField TextBox description;
    public LeafValueEditor<String> description()
    {
        return description;
    }

    // super.name is bound to a TextBox using UiBinder :)
}

Now GWT finds the subeditors in the abstract base class and in both cases I get the corresponding fields name and description populated and flushed.

Unfortunately this approach is not suitable when the concrete subeditors have different values in your bean structure to edit :(

I think this is a bug of the editors framework GWT code generation, that can only be solved by the GWT development team.

Bonnet answered 23/9, 2011 at 9:50 Comment(2)
One addition: Maybe it will also work for subeditors that edit different types of beans but then the abstract base class/interface must have editor fields/getters for all the properties in question of all types leaving these null that are not used by the current concrete editor subclass.Bonnet
Have you tried using a different @Path annotations on your sub-editors? I definitely prefer the approach of letting the GWT editors bind to interfaces and methods (instead of classes and fields)Holohedral
K
0

Isn't the fundamental problem that the binding happens at compile time so will only bind to QuestionDataProxy so won't have sub-type specific bindings? The CompositeEditor javadoc says "An interface that indicates that a given Editor is composed of an unknown number of sub-Editors all of the same type" so that rules this usage out?

At my current job I'm pushing to avoid polymorphism altogether as the RDBMS doesn't support it either. Sadly we do have some at the moment so I'm experimenting with a dummy wrapper class that exposes all the sub-types with specific getters so the compiler has something to work on. Not pretty though.

Have you seen this post: http://markmail.org/message/u2cff3mfbiboeejr this seems along the right lines.

I'm a bit worried about code bloat though.

Hope that makes some sort of sense!

Kaftan answered 19/5, 2012 at 10:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.