question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Layout DSL to ease composition and separate concerns

See original GitHub issue

Would 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:

Example2.tui.ps1:

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:

Example2.tui.cs:

//------------------------------------------------------------------------------
// <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:

Example2.cs:



// 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:

tui-dsl-example2-ui

Looking for feedback, if this is worth exploring further.

Issue Analytics

  • State:open
  • Created 5 years ago
  • Reactions:2
  • Comments:9 (1 by maintainers)

github_iconTop GitHub Comments

6reactions
IKoshelevcommented, Sep 18, 2018

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:

namespace Designer {

    using Terminal.Gui;
    using static Designer.TerminalGuiDSL;

    public partial class DSLShowcase {

	public View Root;

	public Button DefaultButton;

	public TextField MessageField;

	public void InitView()
	{
             // DSL starts here

	    Window("MyApp", out Root,
		   v => { v.Width = Dim.Percent(100); v.Height = Dim.Percent(100); }

		   , Window("Subwindow1",                                
		   v => { v.X = 10; v.Y = 2; defaultDimensions(v); } 
                   // notice use of function above to set dimension attributes. 
                   // Can easily be used for CSS-like styles

			, TextField(out MessageField,
			tf => { tf.X = 0; tf.Y = 1; tf.Width = Dim.Percent(100);
			    tf.Text = "Eneter some text"; }
			)

		   )

		   , Window("Subwindow2",
		   v => { v.X = 10; v.Y = 8; defaultDimensions(v); }

			, Button("Show text from above", out DefaultButton,
			b => { b.X = 1; b.Y = 1; b.Clicked = DeafaultButtonClicked; })

		   )
	    );

            //DSL ends here

	    void defaultDimensions(View view)
	    {
		view.Width = 50;
		view.Height = 5;
	    }
	}
    }
}

Methods part:

namespace Designer {

    using Terminal.Gui;

    public partial class DSLShowcase {

	public void DeafaultButtonClicked()
	{
	    var n = MessageBox.Query(50, 7,
			    "Click", MessageField.Text.ToString(), "Ok");
	}
    }
}

Result: image

TerminalGuiDSL.cs:

using System;
using System.Linq;
using Terminal.Gui;

namespace Designer {
    public static class TerminalGuiDSL {
	public static Window Window<T>(string name, out T @outVar, Action<Window> attr, params View[] children)
	    where T : View
	{	   
	    var win = new Window(name);
	    attr?.Invoke(win);
	    if(children?.Any() == true) {
		win.Add(children);
	    }

	    @outVar = (T)(View)win;
	    return win;
	}

	public static Window Window(string name, Action<Window> attr, params View[] children)
	{
	    return Window<View>(name, out var _, attr, children);
	}

	public static Window Window(string name, params View[] children)
	{
	    return Window<View>(name, out var _, null, children);
	}

	public static Button Button<T>(string text, out T @outVar, Action<Button> attr = null)
	    where T : View
	{
	    var button = new Button(text);
	    attr?.Invoke(button);

	    @outVar = (T)(View)button;
	    return button;
	}

	public static Button Button<T>(string text, Action<Button> attr = null)
	    where T : View
	{
	    return Button<View>(text, out var _, attr);
	}

	public static TextField TextField<T>(out T @outVar, Action<TextField> attr = null)
	    where T : View
	{
	    var field = new TextField("");
	    attr?.Invoke(field);

	    @outVar = (T)(View)field;
	    return field;
	}
    }
}

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.

3reactions
jpiersoncommented, May 3, 2019

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

Read more comments on GitHub >

github_iconTop Results From Across the Web

Formal Model and DSL for Separation of Concerns based ...
The separation of concerns (SOC), as a conceptual tool, enables us to manage the complexity of software systems that we develop. The benefits...
Read more >
Formal Model and DSL for Separation of Concerns based ...
Abstract The separation of concerns (SOC), as a conceptual tool, enables us to manage the complexity of software systems that we develop.
Read more >
Design Guidelines for Domain Specific Languages
languages. We defined guidelines to support a DSL devel- oper to achieve better quality of the language design and a better acceptance among...
Read more >
How do you reuse existing DSLs or combine them ...
A fifth way to reuse existing DSLs or combine them with other languages is to compose them. DSL composition is a technique that...
Read more >
A conceptual framework for building good DSLs
A DSL is a focussed, processable language for describing a specific concern when building a system in a specific domain. The abstractions and...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found