#20 Embedded DSL with Xtend
- Download:
- source code Project Files in Zip (42.2 KB)
- mp4 Full Size H.264 Video (37 MB)
- m4v Smaller H.264 Video (20.1 MB)
- webm Full Size VP8 Video (20.7 MB)
- ogv Full Size Theora Video (41.3 MB)
Introduction
In this episode we design embedded DSL for forms and use it to specify a registration form.
Parse tree
The execution of the method with embedded DSL should result in the parse tree starting from the GroupDef class.
class ElementDef{} class GroupDef extends ElementDef { @Property val elementList = <ElementDef>newArrayList @Property String text } class InputDef extends ElementDef { @Property EAttribute attribute @Property String label } class TextInputDef extends InputDef {} class NumberInputDef extends InputDef { @Property Integer maximum @Property Integer minimum }
AbstractForm
Our embedded DSL always located in a specifyForm() method of a class inheriting AbstractForm class.
abstract class AbstractForm { @Property String title @Property val form = new GroupDef def abstract void specifyForm() }
Model
Our SWT registration form will use data binding to populate an EMF model object. We specify the corresponding meta-model using Xcore library.
class RegistrationForm {
String name
int age
String city
String zip
String country
}Embedded DSL
Our example embedded DSL is located in the MyForm class.
import org.xtextcasts.edsl.example.edsl.AbstractForm import static org.xtextcasts.edsl.example.model.ModelPackage.Literals.* class MyForm extends AbstractForm { override void specifyForm() { form("Registration form") [ group("Personal information") [ textInput(REGISTRATION_FORM__NAME, "Fullname") numberInput(REGISTRATION_FORM__AGE, "Age") [ minimum = 16 ] ] group("Address") [ textInput(REGISTRATION_FORM__CITY, "City") textInput(REGISTRATION_FORM__ZIP, "ZIP") textInput(REGISTRATION_FORM__COUNTRY, "Country") ] ] } }
Making embedded DSL compile
In order to make our embedded DSL compile (MyForm class) we will need special methods in the AbstractForm, GroupDef and NumberInputDef classes.
@Property String title @Property val form = new GroupDef def abstract void specifyForm() def void form(String title, (GroupDef) => void proc) { this.title = title proc.apply(form) } def static group(GroupDef groupDef, String text, (GroupDef) => void proc) { groupDef.elementList.add(new GroupDef => [ it.text = text proc.apply(it) ]) }
import org.eclipse.emf.ecore.EAttribute class ElementDef{} class GroupDef extends ElementDef { @Property val elementList = <ElementDef>newArrayList @Property String text def textInput(EAttribute attribute, String label) { elementList.add(new TextInputDef => [ it.label = label it.attribute = attribute ]) } def numberInput(EAttribute attribute, String label) { numberInput(attribute, label, null) } def numberInput(EAttribute attribute, String label, (NumberInputDef) => void proc) { elementList.add(new NumberInputDef => [ it.label = label it.attribute = attribute if (proc != null) { proc.apply(it) } ]) } } class InputDef extends ElementDef { @Property EAttribute attribute @Property String label } class TextInputDef extends InputDef {} class NumberInputDef extends InputDef { @Property Integer maximum @Property Integer minimum }
Integrating App
After embedded DSL defined and parse tree created, we can go ahead and create SWT widgets. This is done in Main.xtend.
import org.eclipse.core.databinding.DataBindingContext import org.eclipse.core.databinding.observable.Realm import org.eclipse.emf.databinding.EMFProperties import org.eclipse.jface.databinding.swt.SWTObservables import org.eclipse.jface.databinding.swt.WidgetProperties import org.eclipse.swt.events.* import org.eclipse.swt.layout.* import org.eclipse.swt.widgets.* import org.xtextcasts.edsl.example.edsl.* import org.xtextcasts.edsl.example.forms.MyForm import org.xtextcasts.edsl.example.model.* import static org.eclipse.swt.SWT.* import org.eclipse.swt.widgets.Spinner import org.xtextcasts.edsl.example.edsl.NumberInputDef class Main { Shell shell RegistrationForm model val bindingContext = new DataBindingContext() new(Shell shell, RegistrationForm form) { this.shell = shell this.model = form } def static void main(String[] args) { val display = new Display(); val shell = new Shell(display); shell.setLayout(new GridLayout(1, false)); ModelPackage.eINSTANCE.eClass(); // Retrieve the default factory singleton val factory = ModelFactory.eINSTANCE; val form = factory.createRegistrationForm(); Realm.runWithDefault(SWTObservables.getRealm(display), [| new Main(shell, form).constructForm(new MyForm()); shell.pack(); shell.open(); while (!shell.isDisposed()) { if (!display.readAndDispatch()) display.sleep(); } display.dispose(); ]) } def void constructForm(MyForm myForm) { myForm.specifyForm shell.text = myForm.title ?: "" shell.layout = new GridLayout(1, false) for (elementDef : myForm.form.elementList) { constructElement(elementDef, shell) } new Composite(shell, NONE) => [ layout = new GridLayout(2, false) layoutData = new GridData(RIGHT, BOTTOM, true, true) new Button(it, NONE) => [ text = "OK" onSelect [ println(model) System::exit(0) ] ] new Button(it, NONE) => [ text = "Cancel" onSelect [ System::exit(0) ] ] ] } def dispatch void constructElement(GroupDef compositeDef, Composite parent) { val composite = new Group(parent, BORDER) composite.text = compositeDef.text composite.layout = new GridLayout(2, false) composite.layoutData = new GridData(FILL, FILL, true, false) for (elementDef : compositeDef.elementList) { constructElement(elementDef, composite) } } def dispatch void constructElement(TextInputDef textInputDef, Composite parent) { val label = new Label(parent, NONE); label.setText(textInputDef.label); val text = new Text(parent, BORDER); text.layoutData = new GridData(FILL, CENTER, true, false) => [ widthHint = 150 ] bindingContext.bindValue(WidgetProperties::text(Modify).observe(text), EMFProperties::value(textInputDef.attribute).observe(model)); } def dispatch void constructElement(NumberInputDef numberInputDef, Composite parent) { val label = new Label(parent, NONE); label.setText(numberInputDef.label); val spinner = new Spinner(parent, BORDER); spinner.layoutData = new GridData(FILL, CENTER, true, false) => [ widthHint = 150 ] if (numberInputDef.maximum != null) { spinner.maximum = numberInputDef.maximum } if (numberInputDef.minimum != null) { spinner.minimum = numberInputDef.minimum } bindingContext.bindValue(WidgetProperties::selection.observe(spinner), EMFProperties::value(numberInputDef.attribute).observe(model)); } def static onSelect(Button button, (SelectionEvent) => void proc) { button.addSelectionListener(new OnSelect(proc)) } } @Data class OnSelect implements SelectionListener { (SelectionEvent)=>void proc override void widgetDefaultSelected(SelectionEvent e) {} override void widgetSelected(SelectionEvent e) { proc.apply(e) } }

