Layout DSL to ease composition and separate concerns
See original GitHub issueWould there be any value in crafting a domain-specific language to define layouts?
Using a DSL you could separate the layout, child nesting, and default argument/property/handler assignments more naturally, and then focus on behavior and logic separately in say a subclass or partial class.
As a proof-of-concept, I hacked together a quick DSL using PowerShell. Here’s a sample of the language:
Import-Module ..\DSL\Terminal.UI.DSL -Force
$winTitle = "Terminal UI via DSL"
$winSubtitle = "Example #2"
Layout {
MenuBar -Name mainMenuBar {
MenuBarItem "File" {
MenuItem "Open" | -handle Action
MenuItem "Close" | -handle Action
MenuItem "Exit" | -handle Action OnExit
}
}
Window -Name mainWindow "$winTitle`: $winSubtitle" {
-set X 0
-set Y 1
-set Width -Expr "Dim.Fill()"
-set Height -Expr "Dim.Fill() - 2"
## Name fields
Label -X 5 -Y 1 "First:"
TextField -X 15 -Y 1 -Width 15
Label -X 5 -Y 3 "Last:"
TextField -X 15 -Y 3 -Width 15
## Collect a Password
Label -X 35 -Y 1 "Password:"
TextField -X 45 -Y 1 -Width 15 -Secret
Label -X 35 -Y 3 "Again:"
TextField -X 45 -Y 3 -Width 15 -Secret
## ToS Languge and Acceptance
FrameView -Title "ToS:" -Bounds "new Rect(5, 6, 25, 5)" {
TextView -Text "This is a block of hard to read\nlegalese text\nfor your review." `
-Frame "new Rect(0, 0, 23, 3)"
}
Checkbox -X 7 -Y 12 "Accepted" -Name acceptedCheckBox
Checkbox -X 7 -Y 13 "Skimmed" -Checked
## Captures date of ToS Acceptance
Label -X 45 -Y 15 "Accepted Date:"
Label -Frame "new Rect(60, 15, 20, 1)" " " -Name acceptedDateLabel
## Submit or Abort
Button -Name okButton -X 10 -Y 18 "OK" -IsDefault |
-handle Clicked
Button -Name cancelButton -X 20 -Y 18 "Cancel" |
-handle Clicked
## Graceful Exit -- reuses existing handler as menu item
Button -Text "Exit" {
-set X -Expr "Pos.Right(mainWindow) - 10"
-set Y -Expr "Pos.Bottom(mainWindow) - 5"
} | -handle Clicked OnExit
}
}
In this PoC, the DSL gets translated at build-time into a 1 or 2 partial classes, the primary class defines the UI layout and element configuration and relationships. For the example above, this is the generated code:
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace DSL.Example
{
using System;
using Mono.Terminal;
using Terminal.Gui;
public partial class Example2
{
private MenuBar mainMenuBar;
private Window mainWindow;
private CheckBox acceptedCheckBox;
private Label acceptedDateLabel;
private Button okButton;
private Button cancelButton;
partial void okButton_Clicked();
partial void cancelButton_Clicked();
partial void OnExit();
partial void MenuItem1_Action();
partial void MenuItem2_Action();
private void InitLayout()
{
MenuItem MenuItem1 = new MenuItem("Open", "", null);
MenuItem MenuItem2 = new MenuItem("Close", "", null);
MenuItem MenuItem3 = new MenuItem("Exit", "", null);
MenuBarItem MenuBarItem1 = new MenuBarItem("File", new MenuItem[] {
MenuItem1,
MenuItem2,
MenuItem3});
this.mainMenuBar = new MenuBar(new MenuBarItem[] {
MenuBarItem1});
this.mainWindow = new Window("Terminal UI via DSL: Example #2");
Label Label1 = new Label(5, 1, "First:");
TextField TextField1 = new TextField(15, 1, 15, "");
Label Label2 = new Label(5, 3, "Last:");
TextField TextField2 = new TextField(15, 3, 15, "");
Label Label3 = new Label(35, 1, "Password:");
TextField TextField3 = new TextField(45, 1, 15, "");
Label Label4 = new Label(35, 3, "Again:");
TextField TextField4 = new TextField(45, 3, 15, "");
FrameView FrameView1 = new FrameView(new Rect(5, 6, 25, 5), "ToS:");
TextView TextView1 = new TextView(new Rect(0, 0, 23, 3));
this.acceptedCheckBox = new CheckBox(7, 12, "Accepted");
CheckBox CheckBox1 = new CheckBox(7, 13, "Skimmed", true);
Label Label5 = new Label(45, 15, "Accepted Date:");
this.acceptedDateLabel = new Label(new Rect(60, 15, 20, 1), " ");
this.okButton = new Button(10, 18, "OK", true);
this.cancelButton = new Button(20, 18, "Cancel");
Button Button1 = new Button("Exit");
// mainWindow
mainWindow.Add(Label1);
mainWindow.Add(TextField1);
mainWindow.Add(Label2);
mainWindow.Add(TextField2);
mainWindow.Add(Label3);
mainWindow.Add(TextField3);
mainWindow.Add(Label4);
mainWindow.Add(TextField4);
mainWindow.Add(FrameView1);
mainWindow.Add(acceptedCheckBox);
mainWindow.Add(CheckBox1);
mainWindow.Add(Label5);
mainWindow.Add(acceptedDateLabel);
mainWindow.Add(okButton);
mainWindow.Add(cancelButton);
mainWindow.Add(Button1);
mainWindow.X = 0;
mainWindow.Y = 1;
mainWindow.Width = Dim.Fill();
mainWindow.Height = Dim.Fill() - 2;
//
TextField3.Secret = true;
//
TextField4.Secret = true;
//
FrameView1.Add(TextView1);
//
TextView1.Text = "This is a block of hard to read\nlegalese text\nfor your review.";
// okButton
okButton.Clicked = () => okButton_Clicked();
// cancelButton
cancelButton.Clicked = () => cancelButton_Clicked();
//
Button1.X = Pos.Right(mainWindow) - 10;
Button1.Y = Pos.Bottom(mainWindow) - 5;
Button1.Clicked = () => OnExit();
// mainMenuBar
MenuItem1.Action = () => MenuItem1_Action();
MenuItem2.Action = () => MenuItem2_Action();
MenuItem3.Action = () => OnExit();
}
}
}
This generated code is combined with hand-written logic which is cleanly separated from the UI composition to create more complete app:
// This file will be auto-generated if missing with
// sample code, otherwise it will be left untouched
namespace DSL.Example
{
using System;
using Mono.Terminal;
using Terminal.Gui;
public partial class Example2
{
public Example2()
{
this.InitLayout();
}
partial void OnExit()
{
if (ConfirmExit())
Application.Top.Running = false;
}
partial void okButton_Clicked()
{
if (!acceptedCheckBox.Checked)
{
MessageBox.Query(60, 10, "Accept ToS",
"You must accept our Terms of Service to continue!", "OK");
}
else
{
acceptedDateLabel.Text = DateTime.Now.ToString();
}
}
partial void cancelButton_Clicked()
{
Application.Top.Running = false;
}
static bool ConfirmExit ()
{
var n = MessageBox.Query (50, 7, "Quit Demo",
"Are you sure you want to quit this demo?", "Yes", "No");
return n == 0;
}
public static void Start()
{
var layout = new Example2();
Application.Init ();
Application.Top.Add(new View[] {
layout.mainWindow,
layout.mainMenuBar,
});
Application.Run ();
}
}
}
Which materializes to this:
Looking for feedback, if this is worth exploring further.
Issue Analytics
- State:
- Created 5 years ago
- Reactions:2
- Comments:9 (1 by maintainers)
Top GitHub Comments
Dear colleagues, IMHO, DSLs are a bad idea when we are dealing with a language as expressive as C#. Gui markup DSLs like HTML and XAML mostly come down to glorified alternative shorthand syntax for constructor invocations and property assignment. For this we pay by having to study additional syntax, adding maintenance overhead of DSL layer and loosing lots of powerful features of a fully fledged programming language. If we look at HTML trends in the last 3 years, specifically React, it moves away from markup towards using a programming language mixed with markup-like elements, with great results.
Between the power of features like ‘out variables’, ‘params’, ‘using static’ etc… we can define our UI in C# that looks like a DSL language:
Methods part:
Result:
TerminalGuiDSL.cs:
With this approach, we don’t need to maintain any separate DSL functionality. Mixin behaviors, template, higher order components - C# already does it for us.
This project appears to be related to the approach I was describing above for those interested in taking a look.
https://github.com/DieselMeister/Terminal.Gui.Elmish