Использование редакторов GWT со сложным вариантом использования

Я пытаюсь создать страницу, очень похожую на страницу создания формы Google.

введите здесь описание изображения

Вот как я пытаюсь смоделировать это, используя структуру GWT MVP (места и действия) и редакторы.

CreateFormActivity (активность и ведущий)

CreateFormView (интерфейс для просмотра с вложенным интерфейсом Presenter)

CreateFormViewImpl (реализует CreateFormView и Editor‹ FormProxy >

CreateFormViewImpl имеет следующие подредакторы:

  • Заголовок текстового поля
  • Описание текстового поля
  • ВопросСписокРедактор вопросовСписок

QuestionListEditor реализует IsEditor‹ ListEditor‹ QuestionProxy, QuestionEditor>>

QuestionEditor реализует Editor ‹ QuestionProxy> QuestionEditor имеет следующие подредакторы:

  • TextBox вопросЗаголовок
  • TextBox helpText
  • ValueListBox тип вопроса
  • Необязательный вспомогательный редактор для каждого типа вопроса ниже.

Редактор для каждого типа вопросов:

Редактор текстовых вопросов

Редактор вопросов абзаца и текста

Редактор вопросов с множественным выбором

Редактор вопросов с флажками

Редактор списка вопросов

Масштабный редактор вопросов

Редактор вопросов сетки


Конкретные вопросы:

  1. Как правильно добавлять/удалять вопросы из формы. (см. дополнительный вопрос)
  2. Как мне создать редактор для каждого типа вопроса? Я попытался прослушать изменения значения questionType, я не уверен, что делать дальше. (ответил BobV)
  3. Должен ли каждый редактор, специфичный для типа вопроса, быть оболочкой с необязательным полем редактирования? Так как одновременно можно использовать только один из. (ответил BobV)
  4. Как лучше управлять созданием/удалением объектов глубоко в иерархии объектов. Пример) Уточнение ответов на вопрос номер 3, который относится к типу вопросов с несколькими вариантами ответов. (см. дополнительный вопрос)
  5. Можно ли использовать редактор OptionalFieldEditor для переноса ListEditor? (ответил BobV)

Реализация на основе ответа

Редактор вопросов

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);
    }
}

}

Редактор базы данных вопросов

public interface QuestionBaseDataEditor extends HasRequestContext<QuestionDataProxy>,                         IsWidget {


}

Пример подтипа

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) {

}
}

Единственная оставшаяся проблема заключается в том, что конкретные данные подтипа QuestionData не отображаются или не сбрасываются. Я думаю, что это связано с настройкой редактора, который я использую.

Например, значение для приглашения в BooleanQuestionDataEditor не задано и не сбрасывается, а в полезной нагрузке rpc равно null.

Мое предположение таково: поскольку QuestionDataEditor реализует LeafValueEditor, драйвер не будет посещать подредактор, даже если он подключен.

Большое спасибо всем, кто может помочь!!!


person Nick Siderakis    schedule 12.08.2011    source источник
comment
По сути, вы добавляете два редактора в цепочку редакторов для одного и того же значения. Сначала прикрепляется основной редактор, а затем подредактор. Пробовали ли вы отключить основной редактор перед подключением вспомогательного редактора?   -  person John Patterson    schedule 04.05.2012


Ответы (4)


По сути, вы хотите, чтобы CompositeEditor обрабатывал случаи, когда объекты динамически добавляются или удаляются из иерархии редактора. Адаптеры ListEditor и OptionalFieldEditor реализуют CompositeEditor.

Если информация, необходимая для разных типов вопросов, принципиально ортогональна, то можно использовать несколько OptionalFieldEditor с разными полями, по одному для каждого типа вопроса. Это сработает, когда у вас всего несколько типов вопросов, но в будущем это не будет хорошо масштабироваться.

Другой подход, который будет лучше масштабироваться, заключается в использовании пользовательской реализации CompositeEditor + LeafValueEditor, которая обрабатывает полиморфную иерархию типа QuestionData. Выпадающий элемент пользовательского интерфейса type станет деталью реализации CompositeEditor. Когда выбран тип вопроса, редактор вызовет EditorChain.attach() с экземпляром подтипа QuestionData и под-редактором для конкретного типа. Вновь созданный экземпляр QuestionData следует сохранить для реализации LeafValueEditor.getValue(). Реализация CompositeEditor.setValue() просто создает подредактор для конкретного типа и вызывает EditorChain.attach().

FWIW, OptionalFieldEditor можно использовать с ListEditor или любым другим типом редактора.

person BobV    schedule 15.08.2011
comment
Спасибо за ответ! Этот подход выглядит намного чище. Сегодня попробую и отпишусь о результатах. - person Nick Siderakis; 15.08.2011
comment
Что должно быть возвращено для CompositeEditor.createEditorForTraversal()? - person Nick Siderakis; 16.08.2011
comment
Единственное, что вызывает createEditorForTraversal(), это PathCollector, который используется RequestFactoryEditorDriver.getPaths(). Я рекомендую всегда возвращать тривиальный экземпляр вашего редактора типа вопроса и знать, что вам, возможно, придется изменить значение, возвращаемое getPaths(). - person BobV; 16.08.2011
comment
Чтобы загрузить данные для определенного подтипа QuestionData, путь для конкретного подтипа необходимо добавить к Request.with(), но тип QuestionData неизвестен до тех пор, пока данные не будут загружены. Итак, следует ли добавлять все возможные пути подтипа QuestionData при загрузке данных? - person Nick Siderakis; 16.08.2011
comment
Добавление всех возможных подпутей в вызов with() ничего не сломает на сервере; он будет игнорировать пути, которые не существуют в объекте. Звучит как запрос функции, чтобы разрешить синтаксис with("foo.*"), чтобы избежать необходимости чрезмерно указывать поля в случаях полиморфного возврата. - person BobV; 16.08.2011
comment
Спасибо! запрос функции создан code.google.com/p/google -web-toolkit/issues/detail?id=6698 - person Nick Siderakis; 16.08.2011
comment
Привет, Боб, у меня почти получилось, видишь, что я делаю не так? - person Nick Siderakis; 19.08.2011
comment
Генератор GWT не поддерживает полиморфные типы редакторов (я думал, что это уже сделано). Вместо этого вы можете создать отдельный интерфейс SimpleBeanEditorDriver для каждого отображаемого подтипа редактора и управлять циклом edit()/flush() поддрайвера на основе вызовов разветвленного редактора. Подан отдельный вопрос 6719 для полиморфизма редактора. - person BobV; 22.08.2011
comment
@BobV Хотел бы я увидеть этот комментарий до моего ответа ниже, но он был свернут stackoverflow;) Спасибо за регистрацию проблемы. - person Gandalf; 23.09.2011

Мы реализовали аналогичный подход (см. принятый ответ), и у нас он работает так.

Поскольку драйвер изначально не знает о простых путях к редактору, которые могут использоваться подредакторами, каждый подредактор имеет собственный драйвер:

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

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

Затем мы используем следующий адаптер редактора, который позволяет использовать любой подредактор, реализующий RequestFactoryEditor. Это наш обходной путь для поддержки полиморфизма в редакторах:

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;
    }
}

Наш пример подредактора:

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;
    }
...
}

Наш пример использования:

        @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());
            }
        }

Было бы здорово услышать ваши комментарии.

person aux    schedule 26.03.2012
comment
Ваш подход, безусловно, правильный. Для начала мы сделали что-то подобное для наших редакторов — создали новый драйвер для каждого субредактора и связали их вместе. Мне больше не нравится такой подход, потому что я влюбился в посетителей редакторов и в силу, которую они приносят. Например; грязное обнаружение. Вместо этого я считаю, что вы можете выполнить ту же задачу, используя набор составных редакторов. - person logan; 25.04.2012
comment
Хорошая реализация предложения в этом комментарии - person logan; 25.04.2012

Что касается вашего вопроса, почему конкретные данные подтипа не отображаются или не сбрасываются:

Мой сценарий немного отличается, но я сделал следующее наблюдение:

Привязка данных редактора GWT не работает, как можно было бы ожидать, с абстрактными редакторами в иерархии редакторов. SubEditor, объявленный в вашем QuestionDataEditor, имеет тип QuestionBaseDataEditor, и это полностью абстрактный тип (интерфейс). При поиске полей/подредакторов для заполнения данными/сбросом GWT берет все поля, объявленные в этом типе. Поскольку в QuestionBaseDataEditor нет объявленных подредакторов, ничего не отображается/не сбрасывается. Из отладки я узнал, что это происходит из-за того, что GWT использует сгенерированный EditorDelegate для этого абстрактного типа, а не EditorDelegate для конкретного подтипа, присутствующего в данный момент.

В моем случае все конкретные подредакторы имели одинаковые типы редакторов конечных значений (у меня было два разных конкретных редактора, один для отображения, а другой для редактирования одного и того же типа компонента), поэтому я мог сделать что-то вроде этого, чтобы обойти это ограничение:

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 :)
}

Теперь GWT находит подредакторы в абстрактном базовом классе, и в обоих случаях я получаю заполненные и очищенные имена и описания соответствующих полей.

К сожалению, этот подход не подходит, когда конкретные подредакторы имеют разные значения в структуре вашего компонента для редактирования :(

Я думаю, что это ошибка генерации кода среды редактора GWT, которую может решить только команда разработчиков GWT.

person Gandalf    schedule 23.09.2011
comment
Одно дополнение: возможно, это также будет работать для подредакторов, которые редактируют различные типы bean-компонентов, но тогда абстрактный базовый класс/интерфейс должен иметь поля редактора/геттеры для всех рассматриваемых свойств всех типов, оставляющих эти нулевые значения, которые не используются текущим конкретным подкласс редактора. - person Gandalf; 23.09.2011
comment
Пробовали ли вы использовать другие аннотации @Path в своих подредакторах? Я определенно предпочитаю подход, позволяющий редакторам GWT привязываться к интерфейсам и методам (вместо классов и полей). - person logan; 25.04.2012

Разве основная проблема не в том, что привязка происходит во время компиляции, поэтому будет привязываться только к QuestionDataProxy, поэтому не будет иметь привязок, специфичных для подтипа? В javadoc CompositeEditor говорится: «Интерфейс, который указывает, что данный редактор состоит из неизвестного количества подредакторов одного типа», чтобы исключить это использование?

На моей текущей работе я стремлюсь вообще избегать полиморфизма, так как СУБД его тоже не поддерживает. К сожалению, на данный момент у нас есть некоторые из них, поэтому я экспериментирую с фиктивным классом-оболочкой, который предоставляет все подтипы с определенными геттерами, чтобы компилятору было над чем работать. Хотя некрасиво.

Видели ли вы этот пост: http://markmail.org/message/u2cff3mfbiboeejr кажется, он соответствует действительности. .

Хотя меня немного беспокоит раздувание кода.

Надеюсь, это имеет какой-то смысл!

person salk31    schedule 19.05.2012