Uso de Editores GWT con un uso complejo
requestfactory gwt-editors (4)
Fundamentalmente, desea que CompositeEditor
maneje los casos en los que los objetos se agregan o quitan dinámicamente de la jerarquía del Editor. Los adaptadores ListEditor
y OptionalFieldEditor
implementan CompositeEditor
.
Si la información requerida para los diferentes tipos de preguntas es fundamentalmente ortogonal, entonces se podría utilizar el OptionalFieldEditor
con diferentes campos, uno para cada tipo de pregunta. Esto funcionará cuando solo tenga unos pocos tipos de preguntas, pero en realidad no se escalará bien en el futuro.
Un enfoque diferente, que se escalará mejor, sería el uso de una implementación personalizada de un CompositeEditor + LeafValueEditor
que maneje una jerarquía de tipos de QuestionData
polimórfica. El elemento de interfaz de usuario desplegable de tipo se convertiría en un detalle de implementación del CompositeEditor
. Cuando se selecciona un tipo de pregunta, el editor llamará a EditorChain.attach()
con una instancia de un subtipo QuestionData
y el sub-Editor específico del tipo. La instancia de QuestionData
recién creada debe conservarse para implementar LeafValueEditor.getValue()
. La implementación de CompositeEditor.setValue()
solo crea el sub-Editor específico de tipo y llama EditorChain.attach()
.
FWIW, OptionalFieldEditor
se puede usar con ListEditor
o cualquier otro tipo de editor.
Intento crear una página que sea muy similar a la página de creación de Google Form.
Así es como estoy intentando modelarlo usando el marco GWT MVP (Lugares y Actividades) y Editores.
CreateFormActivity (actividad y presentador)
CreateFormView (interfaz para ver, con la interfaz del presentador anidada)
CreateFormViewImpl (implementa CreateFormView y Editor <FormProxy>
CreateFormViewImpl tiene los siguientes subeditores:
- Título de TextBox
- Descripción de TextBox
- QuestionListEditor questionList
QuestionListEditor implementa IsEditor <ListEditor <QuestionProxy, QuestionEditor >>
QuestionEditor implementa Editor <QuestionProxy> QuestionEditor tiene los siguientes subeditores:
- TextBox questionTitle
- TextBox helpText
- ValueListBox questionType
- Un subeditor opcional para cada tipo de pregunta a continuación.
Un editor para cada tipo de pregunta:
TextQuestionEditor
ParagraphTextQuestionEditor
MultipleChoiceQuestionEditor
CheckboxesQuestionEditor
ListQuestionEditor
ScaleQuestionEditor
GridQuestionEditor
Preguntás especificas:
- ¿Cuál es la forma correcta de agregar / eliminar preguntas del formulario? (ver pregunta de seguimiento )
- ¿Cómo debo hacer para crear el Editor para cada tipo de pregunta? Intenté escuchar los cambios en el valor de QuestionType, no estoy seguro de qué hacer después. (respondido por BobV)
- ¿Debería cada editor específico de tipo de pregunta ser contenedor con un editor de campo opcional? Como solo se puede usar uno de ellos a la vez. (respondido por BobV)
- Cómo administrar mejor la creación / eliminación de objetos en las profundidades de la jerarquía de objetos. Ej. Especificar respuestas para una pregunta número 3 que es de tipo pregunta de opción múltiple. (ver pregunta de seguimiento )
- ¿Se puede usar el editor OpcionalFieldEditor para envolver un ListEditor? (respondido por BobV)
Implementación basada en la respuesta
El editor de preguntas
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);
}
}
}
Editor de datos base de preguntas
public interface QuestionBaseDataEditor extends HasRequestContext<QuestionDataProxy>, IsWidget {
}
Ejemplo de subtipo
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) {
}
}
El único problema que queda es que los datos específicos del subtipo QuestionData no se muestran o se vacían. Creo que tiene que ver con la configuración del editor que estoy usando.
Por ejemplo, El valor de solicitud en el BooleanQuestionDataEditor
no está configurado ni enrojecido, y es nulo en la carga útil de rpc.
Mi suposición es: dado que QuestionDataEditor implementa LeafValueEditor, el controlador no visitará el subeditor, aunque haya sido adjuntado.
Muchas gracias a todos los que pueden ayudar!
En relación con su pregunta, ¿por qué los datos específicos del subtipo no se muestran o se descargan?
Mi escenario es un poco diferente, pero hice la siguiente observación:
El enlace de datos del editor GWT no funciona como uno esperaría con los editores abstractos en la jerarquía del editor. El subEditor declarado en su QuestionDataEditor es del tipo QuestionBaseDataEditor y este es un tipo totalmente abstracto (una interfaz). Al buscar campos / subeditores para completar con datos / flush, GWT toma todos los campos declarados en este tipo. Dado que QuestionBaseDataEditor no tiene subeditores declarados, no se muestra / descarga nada. Desde la depuración descubrí que esto sucede debido a que GWT usa un EditorDelegate generado para ese tipo abstracto en lugar de EditorDelegate para el subtipo concreto presente en ese momento.
En mi caso, todos los subeditores concretos tenían los mismos tipos de editores de valor de hoja (tenía dos editores concretos diferentes para mostrar y uno para editar el mismo tipo de bean), así que podía hacer algo como esto para evitar esta limitación:
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 :)
}
Ahora GWT encuentra los subeditores en la clase base abstracta y en ambos casos obtengo el nombre y la descripción de los campos correspondientes rellenos y enjuagados.
Lamentablemente, este enfoque no es adecuado cuando los subeditores concretos tienen diferentes valores en su estructura de frijol para editar :(
Creo que esto es un error de la generación de código GWT del marco de los editores, que solo puede ser resuelto por el equipo de desarrollo de GWT.
Implementamos un enfoque similar (ver respuesta aceptada) y funciona así para nosotros.
Dado que el controlador inicialmente no está al tanto de las rutas de edición simples que pueden usar los subeditores, cada subeditor tiene su propio controlador:
public interface CreatesEditorDriver<T> {
RequestFactoryEditorDriver<T, ? extends Editor<T>> createDriver();
}
public interface RequestFactoryEditor<T> extends CreatesEditorDriver<T>, Editor<T> {
}
Luego usamos el siguiente adaptador de editor que permitiría cualquier sub editor que implemente RequestFactoryEditor para ser utilizado. Esta es nuestra solución para apoyar el polimorfismo en los editores:
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;
}
}
Nuestro ejemplo de editor secundario:
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;
}
...
}
Nuestro ejemplo de uso:
@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());
}
}
Sería genial escuchar tus comentarios.
¿No es el problema fundamental que la vinculación se produce en el momento de la compilación, por lo que solo se vinculará con QuestionDataProxy, por lo que no tendrá enlaces específicos del subtipo? El CompositeEditor javadoc dice "Una interfaz que indica que un Editor dado se compone de un número desconocido de subeditores del mismo tipo", ¿así que se descarta este uso?
En mi trabajo actual, estoy presionando para evitar el polimorfismo ya que el RDBMS tampoco lo admite. Lamentablemente tenemos algunos en este momento, así que estoy experimentando con una clase de envoltura ficticia que expone todos los subtipos con getters específicos para que el compilador tenga algo en qué trabajar. No es lindo sin embargo.
¿Has visto esta publicación ?: http://markmail.org/message/u2cff3mfbiboeejr esto parece estar en la línea derecha.
Aunque estoy un poco preocupado por el código hinchado.
Espero que tenga algún tipo de sentido!