This is an experimenting project. 💀⚡
The concept is to take the advantage of fsharp CE (Computation Expression) to build declarative UI layout without shadow DOM concept and complex diff algorithm.
It is just a bunch of function or delegate to transform native control. To set its property or add child control. It is similar like you write WinForm or WPF application without designer or xaml, instead, you just use C# to write it. Here I use fsharp CE to make it simpler.
Window'() {
Title "Demo"
Grid'() {
Children [
TextBlock'() {
Text "Hi"
}
Button'() {
Text "Increase me"
Click (fun _ -> ())
}
]
}
}
You can install below dotnet template to get started:
dotnet new --install Fun.SunUI.Templates::0.0.*
- MAUI
- Modern.Forms
- WinForms
- WPF
- Avalonia
- Terminal.Gui
In coming support:
- Fable Html
- WinUI3
When you write
TextBlock'() {
Text "123"
}
It will become a record value like below:
{
ElementCreator.RenderMode = RenderMode.CreateOnce
CreateOrUpdate =
fun (sp, ctx) ->
let newCtx =
match ctx with
| ValueNone -> new ElementBuildContext(TextBlock(), sp, RenderMode.CreateOnce)
| ValueSome ctx -> unbox ctx
// BuildElement(fun ctx index ->
// ctx.NativeElement.Text <- "123"
// index + 1
// ).Invoke(newCtx, 0)
// Because inline, it will become
let index = 0
newCtx.NativeElement.Text <- "123"
let index = index + 1
// Of course it depends on the real use case, but the idea is the same.
// Just some utils to set native properties directly.
// The context is just for manage some resources, like IDispose obj, so we can dispose it when necessary.
// Especially for event which we may need to remove previous handler when we register new handler.
// Also for some adaptive data, we will also need to clear up the subcription.
(newCtx :> IElementContext).RenderMode <- key
newCtx
}
So, it will create a struct, one field is for how to render this, another field is a function for creating or updating the native element.
There are some more cases to cover, some property is an event, a Func or Action, or just a property with only getter. But all the CustomOperation is to help you get or set that native element's properties directly.
If there are some properties, or stuff which is special you want to handle by your self you can use With to access the native element directly:
Grid'() {
With (fun ele ->
ele.RowDefinitions.Add(RowDefinition(Height = GridLength.Auto))
)
}
It is also very easy to extend the building DSL:
type Grid' with
[<CustomOperation("Rows")>]
member inline this.Rows([<InlineIfLambda>] builder: BuildElement<Grid>, rows: RowDefinition seq) =
this.With(builder, fun comp -> comp.RowDefinitions.Clear(); rows |> Seq.iter comp.RowDefinitions.Add)
Grid'() {
// Use it like below
Rows [ RowDefinition(Height = GridLength.Auto) ]
}
Render is controlled by render mode, it is a union case:
[<RequireQualifiedAccess; Struct>]
type RenderMode =
/// Try to create if no old state, and rerender if the state is existing. This is default behavior.
| CreateOnce
/// Try to create if no old state, and do not rerender.
| CreateOnceNoRerender
/// Try to create if no old state with the same key, recreate if key changed. Rerender with old state.
| Key of obj
/// Always create new native element.
| AlwaysRecreate
The parent element will cache the child element's context/state, and according to the child element's render mode, it will try to call the CreateOrUpdate function accordingly. And this will finally set the property of the related native element.
Fun.SunUI is just a light wrapper util to build UI layout, so eventualy, you will have to use it some where. For example in WPF:
First, you will create your UI layout with Fun.SunUI
let window =
Windows'() {
Title "Demo"
}
Then, you will use it
let services = ServiceCollection()
let sp = services.BuildServiceProvider()
let nativeWindow = window.Build<Window>(sp) // use it here
Application() |> ignore
Application.Current.MainWindow <- nativeWindow
nativeWindow.Show()
Application.Current.Run nativeWindow
When you build it, it is required to pass a IServiceProvider in which is the baisc of DependencyInjection in dotnet ecosystem.
To consume a service is easy:
let myComp =
UI.inject (fun ctx -> // wrap with this
let someService = ctx.ServiceProvider.GetService<ISomeService>() // consume here
// Return an ElementCreator
Button'() {
Content' "demo"
Click (fun _ -> someService.DoSomething())
}
)
let window =
Windows'() {
Title "Demo"
myComp // use it here
}
This is used for state management, it is a very great library. For more information, please check its docs.
For most of properties with get and set, the generated DSL will support adaptive, for example:
let count = cval 1
Button'() {
Content' (count |> AVal.map (sprintf "count = %d")) // when count is changed, the Content property of the Button will be reset.
Click(fun _ -> count.Publish((+) 1))
}
Another example is for element which contains multiple child elements:
let count = cval 1
Grid'() {
Children (alist { // You can yield child elements according to the data you depends on.
let! c = count
if c > 0 then
Button'() {
Content' "123"
}
Button'() {
Content' "Increase"
Click(fun _ -> count.Publish((+) 1))
}
})
}
-
You will need to add // hot-reload at the top of the file which you want to enable. For how it works you can check Fun.Blazor hot-reload. Because we share the same cli. So you will also need to install Fun.Blazor.Cli. Then you can run fun-blazor watch your_fsharp_project_path
-
Add package Fun.SunUI.HotReload, it will be better to add below condition to this package so we can avoid bundling it in release mode.
Condition="'$(Configuration)'=='DEBUG'"
- Add below code to the entry you want. Below is from the WPF demo.
let nativeWindow =
#if DEBUG
let dispatcher (fn: unit -> unit) = if Application.Current <> null then Application.Current.Dispatcher.Invoke fn
UI
.hotreload("Fun.SunUI.WPF.Demo.Entry.window", (fun () -> Fun.SunUI.WPF.Demo.Entry.window), (), dispatcher)
.Build<Window>(sp)
#else
window.Build<Window>(sp)
#endif
You will need dotnet 6 SDK to build this whole solution. There are some pipelines which is in build.fsx and it contains some pipelines for some tasks like generate internal DSL binding and nuget packages.
Contribution is quit simple, just raise a PR.