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