Part 2: Using Computation Expressions to Model Building UIs
While the
formgen tool can save you significant time when building complex UIs, it suffers from providing no feedback on correctness until the generated file is compiled. Ideally, it is best to retain the same shortness and declarative style of definition but as F# code. That way, semantic errors would surface as you type the form definition in Visual Studio and you can call IntelliSense for help, etc.
If you study the formgen generated code, you can see that most of the gain you get by using it
is in generating the code for adding child controls to parent containers. You can use computation expressions in a
clever way to get the same benefit, say by overriding the let! construct in your own UI control builder objects. So it would look something like:
let form =
formM()
{ let! panel1 = new Panel()
let! panel2 = new Panel()
return () }
Here you have a form builder object (an instance of the
formM type) and each
let! binding creates a new child control. You can then build similar builder objects for other controls, encapsulating the details of what property is assigned when a new child is added, etc.
Computation Builders
F# computation expressions are a great way to add domain specific functionality to your F# code. By defining a given
code block as a computation expression, you allow the code inside that block to be governed by a computation builder
object. This is performed by the F# compiler automatically de-sugaring each F# construct in the code block to calling
certain methods in your computation builder object. You can define computation builders as normal class types and
define how each F# construct is handled by adding the appropriate de-sugared members. Below is a list of F#
constructs (defined as
cexp, and
exp being ordinary F# expressions) and their de-sugared counterparts:
let! pat = exp in cexp b.Bind(exp,(fun pat -> «cexp»))
let pat = exp in cexp b.Let(exp,(fun pat -> «cexp»))
use pat = exp in cexp b.Using(exp,(fun pat -> «cexp»))
use! pat = cexp in cexp b.Bind(exp,(fun x->b.Using(x,(fun pat-> «cexp»))))
do! exp in cexp b.Bind(exp,(fun () -> «cexp»))
do exp in cexp b.Let(exp,(fun () -> «cexp»))
for pat in exp do cexp b.For(exp,(fun pat -> «cexp»))
while exp do cexp b.While((fun ()-> exp),b.Delay(fun ()-> «cexp»))
if exp then cexp1 else cexp2 if exp then «cexp1» else «cexp2»
if exp then cexp if exp then «cexp» else b.Zero()
cexp1; cexp2 b.Combine(«cexp1», b.Delay(fun () -> «cexp2»))
return exp b.Return(exp)
return! exp exp
You do not have to provide all of these methods in a computation builder object. For instance, if you use
let! in a computation expression for which the builder object does not have a corresponding
Bind member you will get an error from the compiler.
Defining Computation Builders for UI Controls
Most UI controls are descendants of the
System.Windows.Forms.Control class, so you can create an
interface that all computation builder objects that build these
Control descendants have to implement:
open System.Windows.Forms
open System.Drawing
[<AbstractClass>]
type 'a IComposableControl when 'a :> Control () =
abstract Self : 'a
member self.Return (e: unit) = self.Self
member self.Zero () = self.Self
[<OverloadID("1")>]
member self.Bind(e : Control, body) : 'a =
e |> self.Self.Controls.Add
body e
[<OverloadID("2")>]
member self.Bind(e: 'b IComposableControl when 'b :> Control, body) : 'a =
e.Self |> self.Self.Controls.Add
body e
This interface contains an abstract
Self member that you can use later to fetch the actual UI
control that was built using the computation builder. These computation expressions are likely to contain
let! and
let! bindings so you will need a way to "end" the computation expression
with a trailing statement. You don't want these expressions to return just any value; you want to actually return the
UI control object that is managed by the builder object. Hence, the interface also contains an implementation for the
Return and
Zero members, requiring that you can only return a unit value, which you use to return the managed control itself.
Your interface also contains two overloaded Bind members that correspond to let! constructs, each taking
a direct Control descendant or another IComposableControl builder object, and adding them to the Controls collection of the managed UI control.
Armed with this interface you can go ahead and implement various builders for Control descendants. Below you can find three builder classes that build forms, panels and rich text boxes.
type formM() =
inherit IComposableControl
Implementing Additional UI Control Builders
There are UI controls that are not
Control descendants, and as such they may have a container
property different than
Controls. One such control is
MainMenu and its
related
MenuItem controls. For these, you create a similar abstraction but wrap
the
MenuItems container property (see
Listing 6).
At this point you can define your Notepad UI as shown in Listing 7.
Speeding Up the Development of UI Code
In this article, you saw two fundamental language-oriented approaches to speed up developing UI code in your F# applications. In the first approach, you defined a DSL, implemented a lexer and parser for it, and wrapped it as a tool to generate F# code from a UI definition in this DSL. The generated files can then be added to your application logic to form a complete application.
In the second approach, you developed various computation builders that manage UI controls and used these builders in nested computation expressions to describe the UI of your application in a succinct and concise way without having to add any code for managing the parent-child relationship between the participating controls. The added benefit of this approach over the first one is that you get instant feedback on your UI's correctness from the compiler, making it an excellent alternative to building UIs in the absence of a Visual Studio form designer that can output F# code, while keeping the entire application code in F#.