Использование редакторов GWT со сложной утилитой
Я пытаюсь создать страницу, которая очень похожа на страницу создания формы Google.
![enter image description here]()
Вот как я пытаюсь моделировать его с помощью среды GWT MVP ( "Места и действия" ) и "Редакторы".
CreateFormActivity (активность и презентатор)
CreateFormView (интерфейс для просмотра, с вложенным интерфейсом Presenter)
CreateFormViewImpl (реализует CreateFormView и Editor <FormProxy>
CreateFormViewImpl имеет следующие подредактора:
- Заголовок TextBox
- Описание TextBox
- QuestionListEditor questionList
QuestionListEditor реализует IsEditor < ListEditor < QuestionProxy, QuestionEditor →
QuestionEditor реализует редактор <QuestionProxy> В QuestionEditor есть следующие субредакторы:
- TextBox questionTitle
- TextBox helpText
- ValueListBox questionType
- Дополнительный субредактор для каждого типа вопросов ниже.
Редактор для каждого типа вопроса:
TextQuestionEditor
ParagraphTextQuestionEditor
MultipleChoiceQuestionEditor
CheckboxesQuestionEditor
ListQuestionEditor
ScaleQuestionEditor
GridQuestionEditor
Конкретные вопросы:
- Каков правильный способ добавления/удаления вопросов из формы. (см. следующий вопрос)
- Как мне создать редактор для каждого типа вопросов? Я попытался прислушаться к изменениям значения questionType, я не уверен, что делать после. (ответил BobV)
- Если каждый редактор, зависящий от типа запроса, должен быть обертованным с необязательнымFieldEditor? Так как только один из них может использоваться одновременно. (ответил BobV)
- Как лучше всего управлять созданием/удалением объектов в глубину иерархии объектов. Ex) Задание ответов на вопрос номер 3, который относится к типу множественного выбора. (см. следующий вопрос)
- Может ли дополнительный редактор FieldEditor использоваться для обертывания 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.
Мое предположение: поскольку QuestionDataEditor реализует LeafValueEditor, драйвер не будет посещать субредактор, даже если он был прикреплен.
Большое спасибо всем, кто может помочь!!!
Ответы
Ответ 1
По сути, вы хотите, чтобы CompositeEditor
обрабатывал случаи, когда объекты динамически добавляются или удаляются из иерархии Editor. Адаптеры ListEditor
и OptionalFieldEditor
реализуют CompositeEditor
.
Если информация, требуемая для разных типов вопросов, принципиально ортогональна, тогда можно использовать несколько OptionalFieldEditor
с разными полями, по одному для каждого типа вопроса. Это будет работать, если у вас будет только несколько типов вопросов, но в будущем они не будут хорошо масштабироваться.
Другой подход, который будет лучше масштабироваться, заключается в использовании пользовательской реализации CompositeEditor + LeafValueEditor
, которая обрабатывает полиморфную иерархию типов QuestionData
. Выпадающий элемент интерфейса типа станет деталью реализации CompositeEditor
. Когда выбран тип вопроса, редактор вызывает EditorChain.attach()
с экземпляром подтипа QuestionData
и подредактором типа. Созданный экземпляр QuestionData
следует сохранить для реализации LeafValueEditor.getValue()
. Реализация CompositeEditor.setValue()
просто создает суб-редактор типа и вызывает EditorChain.attach()
.
FWIW, OptionalFieldEditor
может использоваться с ListEditor
или любым другим типом редактора.
Ответ 2
Мы реализовали аналогичный подход (см. принятый ответ), и он работает для нас таким образом.
Поскольку драйвер изначально не знает о простых путях редактора, которые могут использоваться субредакторами, каждый подредактор имеет собственный драйвер:
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());
}
}
Было бы здорово услышать ваши комментарии.
Ответ 3
Что касается вашего вопроса, почему данные подтипа не отображаются или не отображаются:
Мой сценарий немного отличается, но я сделал следующее замечание:
Редактирование данных редактора GWT не работает так, как ожидалось бы с абстрактными редакторами в иерархии редактора. SubEditor, объявленный в вашем QuestionDataEditor, имеет тип QuestionBaseDataEditor, и это полностью абстрактный тип (интерфейс). При поиске полей/субредакторов для заполнения данными/сброса GWT принимает все поля, объявленные в этом типе. Так как у QuestionBaseDataEditor нет субредакторов, ничего не отображается или не отображается. От отладки я узнал, что это происходит из-за GWT, используя сгенерированный EditorDelegate для этого абстрактного типа, а не EditorDelegate для конкретного подтипа, присутствующего в данный момент.
В моем случае у всех конкретных субредакторов были одинаковые редакторы значений листьев (у меня было два разных конкретных редактора, один из которых отображался, а один - для редактирования того же типа bean), чтобы я мог сделать что-то подобное для работы это ограничение:
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 находит подчиненные в абстрактном базовом классе, и в обоих случаях я получаю соответствующие имена и описание полей, заполненных и очищенных.
К сожалению, этот подход не подходит, когда конкретные субредадеры имеют разные значения в структуре bean для редактирования: (
Я думаю, что это ошибка генерации кода GWT для редакторов, которая может быть решена только командой разработчиков GWT.
Ответ 4
Разве это не основная проблема, связанная с тем, что привязка происходит во время компиляции, поэтому будет привязана только к QuestionDataProxy, поэтому не будет иметь привязки подтипа? CompositeEditor javadoc говорит: "Интерфейс, который указывает, что данный редактор состоит из неизвестного количества подредакторов всех одного и того же типа", чтобы исключить это использование?
В моей текущей работе я пытаюсь избежать полиморфизма вообще, поскольку СУРБД также не поддерживает его. К сожалению, у нас есть некоторые на данный момент, поэтому я экспериментирую с классом-заглушкой, который предоставляет все подтипы с определенными геттерами, чтобы у компилятора есть над чем работать. Хотя не очень.
Вы видели это сообщение: http://markmail.org/message/u2cff3mfbiboeejr это похоже на правильные строки.
Я немного беспокоюсь о раздувании кода.
Надеюсь, что это какой-то смысл!