Can I use a dynamically populated ComboBox for Gateway Web Field

Hello,

I am in the process of creating a module, and I’d like to create a configuration page for the module that includes a drop-down / ComboBox that I can populate at runtime.

Okay, I’ll answer the inevitable “why” question. The purpose of the module is to do small data transformations. We have a solution with several classes that encapsulate events that happen in our module; all these classes implement an interface designed for the events, but each event has a unique data structure. I’d like to get a list of all the classes that implement that interface in order to also load up a list of fields from the class so that they can be used in the transformation.

Is this possible with any of the existing IEditorSource implementations or would I need to roll my own?

Also, I need this to happen at runtime because we have the potential for several other modules to be installed which declare a dependency on the module in which the interface is defined, and we want to be able to dynamically detect and include those implementations of the interface as well.

For what it’s worth, I have considered loading these into a PersistentRecord subclass and then using a ReferenceField, but I don’t like the idea of having to manage the records. Still, it is a possibility.

How dynamic is dynamic?

If you can know it on the backend (without it needing to react to, e.g. the selection in another combobox) then it’s very doable; we do it ourselves with things like the options for remote history providers and the like.

If you need it to be truly dynamic, then you’ll have to move from a Wicket page to a React based config page, and I don’t think we have any public examples of doing that yet.

Nah. Wicket works just fine. It's just that everything IA does with it uses the PersistentRecord models. I implemented my own. The abstract base:

public abstract class AbstractModelEditor<T> extends Panel {
	private static final long serialVersionUID = 1L;
	protected final WebMarkupContainer body;
	protected final FormComponent<T> editor;

	/**
	 * Mimic's Ignition's Field style in the RecordEditForm, but not dependent on
	 * any PersistentRecord instance.  Instead, the appropriate wicket model is
	 * supplied to the fields.
	 * 
	 * Subclasses must have html with wicket:extend surrounding a suitable form
	 * component snippet having wicket:id="editor".  The subclass should pass a
	 * bound CompoundPropertyModel to the wicket form component.  Ideally, the
	 * CompoundPropertyModel is attached to the form itself.
	 *
	 * @param id          Typically an auto-generated child ID of a RepeatingView.
	 * @param fieldModel  Model to supply/receive the HTML field value
	 * @param baseKey     Required bundle key prefix for .Name, optional .Desc, and
	 *                    optional .Default static text.
	 */
	public AbstractModelEditor(String id, IModel<T> fieldModel, String baseKey) {
		super(id);
		TransparentWebMarkupContainer row = new TransparentWebMarkupContainer("editorRow");
		Label title = new Label("fieldTitle", new LenientResourceModel(baseKey+".Name", ""));
		row.add(title);
		body = new WebMarkupContainer("editorBody");

		/* Subclass markup injected by wicket:child for editor component. */
		editor = createEditor(fieldModel);
		body.add(editor);

		LenientResourceModel descModel = new LenientResourceModel(baseKey+".Desc", "");
		MultiLineLabel desc = new MultiLineLabel("descriptionLabel", descModel);
        desc.setEscapeModelStrings(false);
        desc.setVisible(!TypeUtilities.isNullOrEmpty(descModel.getObject()));
        body.add(desc);
		LenientResourceModel defModel = new LenientResourceModel(baseKey+".Default", "");
        Label defValue = new Label("defaultLabel", defModel);
        defValue.setVisible(!TypeUtilities.isNullOrEmpty(defModel.getObject()));
        body.add(defValue);

        row.add(body);
        add(row);
	}

	protected abstract FormComponent<T> createEditor(IModel<T> fieldModel);

	public FormComponent<T> getEditor() {
		return editor;
	}
}

Then concrete implementations like this:

public class ModelChoiceEditor<T> extends AbstractModelEditor<T> {
	private static final long serialVersionUID = 1L;

	public ModelChoiceEditor(String id, IModel<T> fieldModel, String baseKey, IModel<? extends List<? extends T>> choicesModel, IChoiceRenderer<T> renderer) {
		super(id, fieldModel, baseKey);
		DropDownChoice<T> f = (DropDownChoice<T>) editor;
		if (choicesModel != null)
			f.setChoices(choicesModel);
		if (renderer != null)
			f.setChoiceRenderer(renderer);
	}

	protected FormComponent<T> createEditor(IModel<T> fieldModel) {
		DropDownChoice<T> f = new DropDownChoice<T>("editor");
		f.setDefaultModel(fieldModel);
		return f;
	}
}

Sorry, Eric, that's all you get. Just plucking out what else I could share would be rather time-consuming. Wicket isn't very friendly until you wrap your head around the IModel<?>. /:

1 Like

Thanks so much for this example. I’ll try to wrap my head around this more but it looks like it gets me pretty close to my goal.

I don’t necessarily need cascading drop-downs. I just need a drop-down that can be populated using reflection (to show the list of class names that implement my event interface).

It can either happen upon registration of a dependent module, or when the page is loaded.

Despite the fact that I spend most of my time these days working on Ignition modules, I’m still a C# developer, so Wicket still has a fair deal of learning curve for me. That said, I’m also very interested in what could be done with React, as that’s more familiar territory.

However, the best-case UI for this config would be something that would allow me to drop string tokens that represent class properties into a multi-line text editor (which would be swapped out with actual values from the objects at runtime – basic string interpolation).

You just implement an IModel that delivers the list of valid choices at runtime.

I guess I can share the HTML resources that go alongside those two classes. Abstract base:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
	xmlns:wicket="http://wicket.sourceforge.net/">
<body>

<table>

	<wicket:panel>
		<tr wicket:id="editorRow">
			<td wicket:id="fieldTitle" class="trow-title">Field</td>
			<td wicket:id="editorBody" class="trow-body">
				<wicket:child />
				<p wicket:id="descriptionLabel">Field Description</p>
				<p wicket:id="defaultLabel" class="field-default">(default: 123)</p>
			</td>
		</tr>
	</wicket:panel>

</table>
</body>
</html>

And the dropdown component:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
	xmlns:wicket="http://wicket.sourceforge.net/">
<body>
<wicket:extend>
	<select wicket:id="editor">
		<option>Enum Value 1</option>
		<option>Enum value 2</option>
	</select>
</wicket:extend>
</body>
</html>

If you’d like to see this in action, load my Advanced Modbus Module and play with its configuration page. If you dig around under the hood, be discreet.

Okay, I’m really struggling with the learning curve here. I’ve been trying to do this as simply as possible by attempting to extend AbstractEditorSource and AbstractEditor.

I can see my list options are getting populated properly, but it’s not rendering any html on the page.

Here’s the code I’m using…

public class MyEventEditorSource implements IEditorSource {
    static final MyEventEditorSource INSTANCE = new MyEventEditorSource();
    private final List<String> events = new ArrayList<>();

    private MyEventEditorSource() {
        Reflections reflections = new Reflections("my.package.name");
        var eventImplementations = reflections.getTypesAnnotatedWith(MyCustomEventInterface.class);
        events.addAll(eventImplementations.stream().map(e -> e.getSimpleName()).collect(Collectors.toList()));
    }

    public static MyEventEditorSource getSharedInstance() { return INSTANCE; }

    @Override
    public Component newEditorComponent(String id, RecordEditMode editMode, SRecordInstance record, FormMeta formMeta) {
        return new MyEventEditor(id, formMeta, editMode, record, this.events);
    }
}
public class MyEventEditor extends AbstractEditor {
    public MyEventEditor(String id, FormMeta formMeta, RecordEditMode editMode, SRecordInstance record,
                           List<String> list) {
        super(id, formMeta, editMode, record);
        DropDownChoice<String> dropdown = new DropDownChoice<>("editor", list);
        formMeta.installValidators(dropdown);
        dropdown.setLabel(new LenientResourceModel(formMeta.getFieldNameKey()));
        this.add(new Component[]{dropdown});
    }

}

It seems like this should be relatively simple; what am I doing wrong?

Are you providing html templates for your classes like the ones I provided above? Elements of the template with IDs corresponding to the code are replaced with the appropriate generated content to produce the final content. (Recursively.)

(It took me days to get my implementation working. Seems like you are proceeding apace.)

2 Likes

I was not. I wasn’t sure where to put the html templates and was (perhaps naively) hoping that the AbstractEditor would handle that for me. Perhaps that’s the missing link here.

The HTML files are resources with the same package and base name as the class they are associated with. You can only omit them if you are subclassing something that already has all of the appropriate HTML elements needed.

1 Like

That was it! It’s working! Thank you so much for taking the time to help. Hopefully one of these days I can return the favor.

1 Like

9 posts were split to a new topic: React config page example