fsprojects / avalonia.funcui Goto Github PK
View Code? Open in Web Editor NEWDevelop cross-plattform GUI Applications using F# and Avalonia!
Home Page: https://funcui.avaloniaui.net/
License: MIT License
Develop cross-plattform GUI Applications using F# and Avalonia!
Home Page: https://funcui.avaloniaui.net/
License: MIT License
Linux, Manjaro 18.1.5, KDE
dotnet new funcui.basic -n NewApp
dotnet run
I had a go at hacking a style DSL. It seems to work. I'm sure you could pick it apart but maybe it's a start for some ideas. XAML really should die!!
module Control =
open Avalonia.Styling
open Avalonia.Controls
open FSharpx
let styling stylesList =
let styles = Styles()
for style in stylesList do
styles.Add style
Control.styles styles
let style (selector:Selector->Selector) (setters:IAttr<'a> seq) =
let s = Style(fun x -> selector x )
for attr in setters do
match attr.Property with
| Some p ->
match p.accessor with
| InstanceProperty x -> failwith "Can't support instance property"
| AvaloniaProperty x -> s.Setters.Add(Setter(x,p.value))
| None -> ()
s
and then in my view
let private binFileTemplate (indexedBinFile: IndexedBinFile) (binFileViewer:BinFile->unit) (dispatch: Msg -> unit) =
let (id, binFile) = indexedBinFile
let foreground =
match binFile.eq_status with
| DEGREE_EQUAL -> "green"
| DEGREE_DIFFERENT -> "red"
| DEGREE_EXCEPTION_0|DEGREE_EXCEPTION_1|DEGREE_EXCEPTION_2 -> "yellow"
| DEGREE_SIMILAR|DEGREE_EQUAL_IN_TOLERANCE -> "darkgreen"
| _ -> "brightred"
(* create row for name, eq_status, deviation *)
StackPanel.create [
StackPanel.orientation Orientation.Horizontal
StackPanel.background "black"
StackPanel.onDoubleTapped (fun _ -> ViewBinFile(binFileViewer, binFile) |> dispatch )
let style = [
TextBlock.width columnWidth
TextBlock.foreground foreground
TextBlock.horizontalAlignment HorizontalAlignment.Left
]
StackPanel.children [
TextBlock.createFromSeq <| seq {
yield TextBlock.text binFile.name
yield! style
}
TextBlock.createFromSeq <| seq {
TextBlock.text (binFile.eq_status |> DU.toString )
yield! style
}
TextBlock.createFromSeq <| seq {
TextBlock.text (binFile.deviation |> sprintf "%g")
yield! style
}
]
]
I'm creating a program to try to simulate a lab chip that moves liquid with electrodes. I used the game of life example as a bit of a jumping off point to make the electrode grid, and id like to now make droplets of liquid that move on top of this uniform grid. Ive been messing around a bit with trying to create a canvas with ellipses but im feeling a bit lost.
ill paste in my view method with the canvas part commented out.
let view (grid: GridModel) (dispatch: Msg -> unit) : IView =
DockPanel.create[
DockPanel.children[
Button.create [
Button.dock Dock.Bottom
Button.background "#d35400"
Button.onClick ((fun _ -> ImportProcedure (fullPath) |> dispatch), SubPatchOptions.Always)
Button.content "Import Procedure"
]|> generalize
UniformGrid.create [
UniformGrid.columns grid.Width
UniformGrid.rows grid.Height
UniformGrid.children (
grid.Electrodes
|> Array2D.flati
|> Array.map (fun (x, y, electrode) ->
let electrodePosition = { x = x; y = y }
Button.create [
match electrode.ChemList with
| [] ->
yield Button.onClick ((fun _ -> AddChem (electrodePosition,("test",1.1)) |> dispatch), SubPatchOptions.OnChangeOf electrodePosition)
yield Button.background "gray"
| _ ->
yield Button.onClick ((fun _ -> RemoveChem (electrodePosition,("test",1.1)) |> dispatch), SubPatchOptions.OnChangeOf electrodePosition)
yield Button.background "green"
] |> generalize
)
|> Array.toList
|> List.append (List.map (GridModel.DropletValues >> (fun (chems,x,y,r) ->
Canvas.create [
Canvas.background "#2c3e50"
Canvas.children[
Ellipse.create[
Ellipse.top (float x)
Ellipse.left (float y)
Ellipse.width r
Ellipse.height r
Ellipse.fill "#ecf0f1"
]
]
] |> generalize)) grid.Droplets)
)
]
]]
|> generalize
I apologize if the question is a bit vague, feel free to ask for additional information. An older version of the program(without the droplets added) is here: https://github.com/rasmusmm/DMBSim.
*edited with a slightly closer version...
I'd like to be able to set the NativeMenu like this:
First, I'd like to say thank you for working on this project! It's wonderful! Keep up the great work! ๐
So I'm wondering, is there a way to get the width of the window in a view function? I'd like to get the window's width so I can create a sidebar that always has a width of, say, 1/3 of that width, but I'm not sure if there's a way to get the window width. (If that be achieved another way without knowing the window width please let me know.)
Is there any planned support for DataGrid? I assume it's a heavy beast!!
I tried modifying your counter example code to use a grid instead of a dock panel:
let view (state : CounterState) (dispatch) : View =
Views.grid [
Attrs.rowDefinitions (RowDefinitions "1*,1*")
Attrs.columnDefinitions (ColumnDefinitions "1*, 1*")
Attrs.children [
Views.button [
// Attrs.dockPanel_dock Dock.Bottom
Attrs.grid_row 1
Attrs.grid_column 0
Attrs.onClick (fun sender args -> dispatch Decrement)
Attrs.content "-"
]
Views.button [
// Attrs.dockPanel_dock Dock.Bottom
Attrs.grid_row 1
Attrs.grid_column 1
Attrs.onClick (fun sender args -> dispatch Increment)
Attrs.content "+"
]
Views.textBlock [
// Attrs.dockPanel_dock Dock.Top
Attrs.grid_row 0
Attrs.grid_column 0
Attrs.grid_columnSpan 2
Attrs.fontSize 48.0
Attrs.verticalAlignment VerticalAlignment.Center
Attrs.horizontalAlignment HorizontalAlignment.Center
Attrs.text (string state.count)
]
]
]
The state is not updated anymore - the update function always gets the initial state for some reason.
The issue is with these two lines:
Attrs.rowDefinitions (RowDefinitions "1*,1*")
Attrs.columnDefinitions (ColumnDefinitions "1*, 1*")
I get this error:
testafb3 Error: 0 : Unable to process the message: Increment: System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation. ---> System.NotSupportedException: Reassigning RowDefinitions not yet implemented.
at Avalonia.Controls.Grid.set_RowDefinitions(RowDefinitions value) in D:\a\1\s\src\Avalonia.Controls\Grid.cs:line 131
--- End of inner exception stack trace ---
at System.RuntimeMethodHandle.InvokeMethod(Object target, Object[] arguments, Signature sig, Boolean constructor, Boolean wrapExceptions)
at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
at System.Reflection.RuntimePropertyInfo.SetValue(Object obj, Object value, BindingFlags invokeAttr, Binder binder, Object[] index, CultureInfo culture)
at System.Reflection.RuntimePropertyInfo.SetValue(Object obj, Object value, Object[] index)
at Avalonia.FuncUI.VirtualDom.Patcher.AttrPatcher.setValue@373(IControl view, PropertyAttrDelta attr, Object value)
at Avalonia.FuncUI.VirtualDom.Patcher.AttrPatcher.patchProperty(IControl view, PropertyAttrDelta attr)
at Avalonia.FuncUI.VirtualDom.Patcher.patch(IControl view, ViewDelta viewElement)
at Avalonia.FuncUI.Hosts.HostWindow.Avalonia-FuncUI-Hosts-IViewHost-UpdateView(View viewElement)
at [email protected](model state, FSharpFunc`2 dispatch)
at [email protected](msg msg) in /Users/eugene/sources/elmish/elmish/src/program.fs:line 142
Is there a way to avoid this issue?
Here is the repro: https://github.com/Thecentury/Avalonia.FuncUI.Repro
Can FuncUI provide some special handling for a set of known properties that do not allow null values to be set?
Wanna setup value with async after initialization, but don't find any examples.
https://github.com/CreateLab/WeatherUI/blob/master/WeatherUI/WeatherWindow.fs#L44
Hi, can you suggest a way I can provide an input mask for TextBox, e.g. so that it only accepts numeric input? I've tried modifying the input data in the OnTextInput and OnKeyUp event handlers and also by simply setting the text property as part of the msg/model/view process but none of that works. This is on .Net Core/Windows. Also I am new to Avalonia.
Thanks for bringing Elmish to Avalonia btw!
I've just run into a problem I don't know how to solve.
I have a view module for rendering a single item in a list. The view for the single item doesn't know about the list itself. However double clicking on the item needs to update a field on the item. Given that it is all immutable I need to tell the parent view or the parent of the parent view to do this. How to bubble up msgs to where they can be handled?
Hi @JaggerJo ,
Very interesting project! While you provide samples, I still feel kinda lost creating a new project from scratch. For example, what program are you using here? Is it required?
Whats the ideal project template to start from? Is it related to the Avalonia UI .NET core template? (e.g. dotnet new avalonia.app -o MyApp
)
Can you guide me through some basic setup steps? I could do a PR adding the basic steps to your README when i then managed to get a new project running.
Thanks!
Is the source code of the editor (Flink Editor), shown in the readme, available somewhere?
In my app I saw something that looks rather odd: the selected item is showed only on second click. I tried to create MCVE and notice that behaviour is different depending on State.
I can't exclude that I'm missing something obvious though.
Anyway, here is a MCVE:
open Avalonia.Controls
open Avalonia.FuncUI.DSL
open Avalonia.FuncUI.Components
open Avalonia.Layout
module Sample =
type State(v) =
member __.Value = v
override s.ToString() = s.Value.ToString()
let init() = State(1)
type Msg =
| Select of int
let update msg _ : State =
match msg with
| Msg.Select v -> State(v)
let data = [ 1..10 ] |> List.map State
let dataTemplate (v:State) =
TextBlock.create [
v.Value |> string |> TextBlock.text
]
let view (state: State) (dispatch) =
StackPanel.create [
StackPanel.verticalAlignment VerticalAlignment.Center
StackPanel.children [
ComboBox.create [
ComboBox.dataItems data
ComboBox.minWidth 120.0
ComboBox.minHeight 50.0
ComboBox.selectedItem state
ComboBox.onSelectedItemChanged (fun v ->
if v <> null then
v
|> unbox<State>
|> fun state -> state.Value
|> Msg.Select
|> dispatch
)
ComboBox.itemTemplate
(DataTemplateView<State>.create dataTemplate)
]
]
]
I thought about adding something like Reacts useEffect to simplify interop with some existing Avalonia Controls (CefGlue for Example)
Browser.create [
Browser.startUrl "duckduckgo.com"
// not sure how to trigger
Browser.useEffect (fun browser -> browser.GoBack())
]
Implementing this feature is not complicated, maybe adding more 'hook' for attach und detach could also be helpful for controls that need to be specifically initialised.
Maybe effects should also be located in a new namespace like Avalonia.FuncUI.DSL.Effects
, because they introduce side effects to an otherwise pure DSL.
@uxsoft @AngelMunoz @AngelMunoz
opinions ?
Hi @JaggerJo!
What do you think about the idea of not generating the new view and not patching the UI when model hasn't changed after the update function?
Sometimes it is quite tedious to create comparers for all FuncUI properties to eliminate the need to patch the value in the UI, and this change can help to fix problems when repeated setting of the same property with the same value nevertheless changes something under the hood.
If I have a textbox that needs parsing to an int and I would like that textbox to show errors if the parsing fails this is easy to do with Avalonia proper. One just needs to set the errors property of the textbox.
However ( and forgetting about the lens stuff I'm working on ) How would you handle validation if you had the following complex domain model
However it is tedious to have to reflect the parsing errors into the immutable model tree. In fact the model should not be updated if if the parsing fails. This is a local failure. It feels like I should be able to do something like this in FUNCUI.
```fsharp
type Company = {
id: int
name: string
business: string
employees: Person array
revenue: int32
}
type State {
companies Company array
selectedCompany: int
}
So there are two levels. Let's say we want to edit the Company.revenue field
and this requires validation. There are two types of validation. 1 easy and 1 hard(er).
The first is validation of the revenue:int32
As this is part of the data model we can just perform some validation during the view function and render some message out.
The second is parsing validation. The user should only be allowed to enter in valid integers and if they don't then a message should be displayed. However strings that don't parse to an integer can't be put in the model and won't be available on the next render pass. This means there needs to be extra storage for this error data which seems a bit tedious though maybe there is no other way.
Do you have any suggestions on this?
I have a basic view that just ensure that one text box is synchronised with another textbox view the model.
module FooView =
type FooState = {
text : string
}
let initialState = {
text = "Yo ho ho"
}
type FooMsg =
| Edit of string
let update (msg:FooMsg ) (state:FooState) : FooState =
match msg with
| Edit text -> { state with text = text}
let view (state:FooState) (dispatch) : View =
Views.stackPanel [
Attrs.children [
Views.textBox [
Attrs.text state.text
Attrs.onKeyUp(fun sender args ->
dispatch (Edit (sender :?> TextBox).Text)
)
]
Views.textBox [
Attrs.text state.text
]
]
]
And then my main model has two copies of this
module ParentView =
// The model holds data that you want to keep track of while the application is running
type ParentState = {
foo1 : FooView.FooState
foo2 : FooView.FooState
}
//The initial state of of the application
let initialState = {
foo1 = { text = "foo1"}
foo2 = { text = "foo1"}
}
// The Msg type defines what events/actions can occur while the application is running
// the state of the application changes *only* in reaction to these events
type CounterMsg =
| Foo1Msg of FooView.FooMsg
| Foo2Msg of FooView.FooMsg
// The update function computes the next state of the application based on the current state and the incoming messages
let update (msg: CounterMsg) (state: ParentState) : ParentState =
match msg with
| Foo1Msg msg -> { state with foo1 = FooView.update msg state.foo1 }
| Foo2Msg msg -> { state with foo2 = FooView.update msg state.foo2 }
// The view function returns the view of the application depending on its current state. Messages can be passed to the dispatch function.
let view (state: ParentState) (dispatch): View =
Views.dockpanel [
Attrs.children [
Views.uniformGrid[
Attrs.children [
FooView.view state.foo1 (Foo1Msg >> dispatch)
FooView.view state.foo2 (Foo2Msg >> dispatch)
]
]
]
]
As this is my first go at FuncUI it feels a bit boilerplatey. Maybe there is a better way to do this but composition of the states and messages into a hierarchy does feel natural. Maybe you can add this example to the readme as a basic example of what to do or not do.
Hi @JaggerJo, please take a look at repro of this bug โ Thecentury@07df113
Button is expected to increase count by 1 continuously, but works only once.
And in the next commit there is a fix: Thecentury@245d864
Bug happens because in the repro new subscription is equal to the previous one.
I added func into a list of members which are used to compare two subscriptions.
I think another possible fix is to always consider two subscriptions to different if at least one of them captures state.
Cannot estimate which solution is better.
What do you think of it?
Hi,
I have an issue where the GridSplitter will stop working if I change the elements in ListView in the adjacent grid cells.
If I selectively render the GridSplitter via a button - remove and then add - it will work again
Repo can be accessed here: https://github.com/sharp-fsh/funcui_gridsplitter_issue
`
open Avalonia.Controls
open Avalonia.FuncUI.DSL
open Avalonia.Layout
open Avalonia.FuncUI.Components
open Avalonia.Controls.Primitives
type FixGridSplitIssue =
| RenderGridSplitter
| DoNotRenderGridSplitter
type State = {
ListOneItems : string list
ListTwoItems : string list
FixGridSplitIssue : FixGridSplitIssue
}
let generateStrings total word =
[
for i in 0..total-1 do
sprintf "%s: %i" word (i+1)
]
let rowDefinition1 = "35, 100*, 5, 400*"
let listViewA1 = generateStrings 20 "List A V1"
let listViewB1 = generateStrings 30 "List B V1"
let listViewA2 = generateStrings 5 "List A V2"
let listViewB2 = generateStrings 15 "List B V2"
let init = {
ListOneItems = listViewA1
ListTwoItems = listViewB1
FixGridSplitIssue = RenderGridSplitter
}
type Msg =
| ChangeItems
| ToggleFix
let update (msg: Msg) (state: State) : State =
match msg with
| ChangeItems -> { state with ListOneItems = if state.ListOneItems = listViewA1 then listViewA2 else listViewA1
ListTwoItems = if state.ListTwoItems = listViewB1 then listViewB2 else listViewB1}
| ToggleFix -> { state with FixGridSplitIssue = if state.FixGridSplitIssue = RenderGridSplitter then DoNotRenderGridSplitter else RenderGridSplitter }
let buttonPanel gridPosition state dispatch =
StackPanel.create [
gridPosition
StackPanel.orientation Orientation.Horizontal
StackPanel.children [
Button.create [
Button.content "Change Lists"
Button.width 120.
Button.height 35.
Button.onClick (fun _ -> dispatch ChangeItems)
]
Button.create [
Button.content (if state.FixGridSplitIssue = RenderGridSplitter then "Click Twice To Fix (1 of 2)" else "Click Again To Fix (2 of 2)")
Button.width 120.
Button.height 35.
Button.onClick (fun _ -> dispatch ToggleFix)
]
]
]
let genericListBox (items : string list) =
ListBox.create [
ListBox.dataItems items
ListBox.itemTemplate (
DataTemplateView<string>.create (fun (text) ->
TextBlock.create [ TextBlock.text text ]
)
)
]
let verticalScrollerWithItems gridPostion items =
ScrollViewer.create [
gridPostion
ScrollViewer.verticalScrollBarVisibility ScrollBarVisibility.Auto
ScrollViewer.content (
genericListBox items
)
]
let gridSplitterComponent gridPosition =
GridSplitter.create [
gridPosition
GridSplitter.horizontalAlignment HorizontalAlignment.Stretch
]
let view (state: State) (dispatch) =
Grid.create [
Grid.rowDefinitions rowDefinition1
Grid.showGridLines true
Grid.children [
buttonPanel (Grid.row 0) state dispatch
verticalScrollerWithItems (Grid.row 1) state.ListOneItems
match state.FixGridSplitIssue with
| RenderGridSplitter -> gridSplitterComponent (Grid.row 2)
| DoNotRenderGridSplitter -> StackPanel.create [ (Grid.row 2) ]
verticalScrollerWithItems (Grid.row 3) state.ListTwoItems
]
]
`
I've tested adding 1000 items to a stack panel using the naive implementation and the UI grinds. However I spent a bit of time figuring out how to get DataTemplates working in FuncUI way and it seems to work and the UI becomes super snappy even for 1000 element lists.
The code for the working mini app is
https://gist.github.com/bradphelan/06d2e2250facfcf01b848ee71fda4064
The critical line is a helper
let itemTemplate view dispatch =
Avalonia.Controls.Templates.FuncDataTemplate<(int*'state)>( ( fun (id,state) ->
let viewElement = (view state id dispatch)
let view = viewElement |> VirtualDom.createView
let delta = Avalonia.FuncUI.VirtualDom.Delta.ViewDelta.From viewElement
VirtualDom.Patcher.patch(view, delta)
view
) ,true )
and can be used to render lists efficiently like below
module PersonsModule =
type PersonsMsg =
| Update of IndexedMessage<PersonModule.PersonMsg>
| Delete of int
let update (personsMsg:PersonsMsg) state =
match personsMsg with
| Update (id, msg) -> pvSet id (PersonModule.update msg (pvGet id state)) state
| Delete id -> pvDel id state
let itemView (person:PersonModule.PersonState) (id:int) dispatch : View =
// Set up a dispatcher for a person at a specific id
let dispatchPerson id = (fun msg -> dispatch(Update(id,msg)))
Views.dockpanel [
Attrs.children [
Views.button [
Attrs.content "X"
Attrs.onClick ( fun sender args -> dispatch (PersonsMsg.Delete id) )
]
PersonModule.view person (dispatchPerson id)
]
]
let view (state:PersonModule.PersonState PersistentVector) (dispatch) : View =
Views.scrollViewer [
Attrs.content (
Views.listBox [
Attrs.itemTemplate (itemTemplate itemView dispatch )
Attrs.items (state |> Seq.indexed |> PersistentVector.ofSeq )
]
)
]
Maybe I'm telling you something you already know but it seemed like a non-obvious trick and I had to pull some code out of the API to make it work. Hope it is useful for vNext as you think about it.
I am building an application with Avalonia FuncUI, but can run functions with heavy computations and http requests only synchronously, i.e. blocking the main(UI) thread. Could someone please show me a way how to update with a function that runs async and returns the new state on e.g. Button.onClick?
Is it possible to pass in args to the ViewBuilder to initialize the view with?
If you change the root view type (e.g. from Button to TextBlock), the patcher will try to apply attributes to existing element (of the wrong type).
This will cause exception during view update.
System.ArgumentException: Property 'Text not registered on 'Avalonia.Controls.Button
I've tried
module FooView =
type FooState = {
text : string
}
let initialState = {
text = "Yo ho ho"
}
type FooMsg =
| Edit of string
let update (msg:FooMsg ) (state:FooState) : FooState =
match msg with
| Edit text -> { state with text = text}
let view (state:FooState) (dispatch) : View =
Views.stackPanel [
Attrs.children [
Views.textBox [
Attrs.text state.text
Attrs.onTextInput(fun sender args -> dispatch (Edit args.Text))
]
Views.textBox [
Attrs.text state.text
]
]
]
and the two text boxes render and I can add text but the onTextInput never fires. Is this a bug or am I doing something wrong?
Is it possible to add children or a single child to a button? For example to create buttons like those in the sidebar of this app:
So, ideally I'd do something like this:
Button.create [
Button.child [
StackPanel.create [ (* ... *) ]
]
Button.fontSize 20.0
// ...
]
Is that possible or is there another way to achieve this sort of thing?
Hello @JaggerJo,
This project looks really good and I really like the API used for the view elements. It looks like it was loosely based on fabulous-simple-elements. If that the case, I would like to suggest something that I regret not adding to that library -> not being able to see which elements you can use!
Let me explain: if the user is a beginner, then there is no way to tell which elements are available in the framework. You have to go through the docs and search for elements you need. Of course once you have found them, it becomes easy to find their properties, but only after you have searched the docs.
I propose to add a module, maybe named Ava
that contains the constructor for all possible UI elements. Instead of
Button.create [
Button.onClick (fun _ -> dispatch Increment)
Button.content "click to increment"
]
I suggest writing:
Ava.button [
Button.onClick (fun _ -> dispatch Increment)
Button.content "click to increment"
]
This way the beginner user has a nice "entry point" to the possible UI elements that can be used and from there the search for elements becomes even easier. The reason for the name Ava
is that the formatting works nicely when the propery list is indented with 4 spaces such that the property modules become aligned with the constructor function. This is how it is done in both Mui
module from Fable.MaterialUI and the Ant
module from Fable.AntDesign.
It would be a plus if the property module was also lower-case (but would introduce a breaking change)
Ava.button [
button.onClick (fun _ -> dispatch Increment)
button.content "click to increment"
]
What do you think?
For example
type ValidatingTextBox() =
inherit Avalonia.Controls.TextBox()
and
module ValidatingTextBox =
let inline create<'a when 'a : equality> (parser:Epimorphism<'a,string,string>) (data:Image<'a>) (errors:string option->unit) attrs =
Builder.ViewBuilder.Create<ValidatingTextBox> [
//TextBox.create [
yield! attrs
]
One would assume that I could use ValidatingTextBox and TextBox interchangably but it doesn't work. None of the TextBox.*
attributes seem to do anything
ValidatingTextBox.create [
TextBox.width 150.0
TextBox.text state.text
]
does not generate a textbox with width 150 and the text set but if I change the commented out text to TextBox then it does work.
module ValidatingTextBox =
let inline create<'a when 'a : equality> (parser:Epimorphism<'a,string,string>) (data:Image<'a>) (errors:string option->unit) attrs =
//Builder.ViewBuilder.Create<ValidatingTextBox> [
TextBox.create [
yield! attrs
]
Is this a fundamental problem that cannot be solved?
I am using Observables to retrieve market feeds (tick data) and simply would like to dispatch an update to the UI state on changes.
let tickObserver (_state: LiveMarketMonitor.State) =
let sub (dispatch: LiveMarketMonitor.Msg -> unit) =
marketFeedObservable
|> Observable.filter (fun event -> event.MarketPair = MarketPair.Btcaud)
|> Observable.subscribe (fun event ->
dispatch <|
LiveMarketMonitor.Msg.UpdateMarketTick
{ TimeUpdatedUtc = event.Timestamp.Value
LastPrice = event.LastPrice
BestAsk = event.BestAsk
BestBid = event.BestBid })
|> ignore
Cmd.ofSub sub
do
base.Title <- "FinFlow.Desktop.App"
base.Width <- 400.0
base.Height <- 400.0
this.VisualRoot.VisualRoot.Renderer.DrawFps <- true
this.VisualRoot.VisualRoot.Renderer.DrawDirtyRects <- true
Elmish.Program.mkSimple (fun _ -> LiveMarketMonitor.init) LiveMarketMonitor.update LiveMarketMonitor.view
|> Program.withHost this
|> Program.withSubscription tickObserver
|> Program.withConsoleTrace
|> Program.run
The dispatch appears to called in the subscribe method. But I am not seeing any changes in the UI. If I put a breakpoint on the UI view method, I can see the correct state, but the UI for some reason has only displays the initial state?
Is there any approach that we can have a hot-reloading designer for the DSL-based view engine? If not, how much effort does it need to build one?
This is about potentially allowing the use of adaptive view specifications, see https://github.com/fsprojects/FSharp.Data.Adaptive
From fabulous-dev/Fabulous#258 (comment)
@JaggerJo The FuncUI implementation is really good - this file is impressive https://github.com/dsyme/Avalonia.FuncUI/blob/master/src/Avalonia.FuncUI/Core/VirtualDom.fs
That said, it's still using view-reevaluation - for example if there are 10K data points in a chart and one is removed then the view is re-evaluated and, in the absence of other hacks, this will involve a significant amount of work to spot the minimal diff.
FuncUI can, I think, be adapted to work with adaptive data relatively easily. This would mean that the 'view' functions are not re-executed on update (except where necessary for incremental DOM maintenance). The diffs in the view would flow out of the adaptive data, rather than having to diff an old and new view like you do here
There's a sample showing how to define a tree of adaptive view-like data and perform incremental maintenance on a mutable HTMLElement data strucutre here: https://github.com/dsyme/FSharp.Data.Adaptive/blob/dom-node/src/FSharp.Data.Adaptive.Tests/DomUpdater.fs. The variation FuncUI would need would be a bit different - for example FuncUI could define a ViewReader
for the AdaptiveView
type, producing a ViewDelta
(and then patch
is called on that).
Adding dotnet templates for Avalonia.FuncUI will make starting from scratch easier than copying example files to new projects.
I would start creating a template from the elmish counter example. This would be consistent with other templates like the SAFE-Stack template.
Should this be included in the main repo (e.g. a templates
folder in the root of this repo) or be its own project?
Fable and Fabulous are great sounding names. FuncUI is ok, but not as great.
If you have suggestions please comment below.
I did some poking around and found an example of opening a modal dialog in a separate window in Elmish.WPF using their Binding
type (https://github.com/elmish/Elmish.WPF/tree/master/src/Samples/NewWindow). Is rendering to a separate dialog box possible in FuncUI? How would I go about doing it?
Currently I use something like this:
Button.create [
Button.content "Some text"
Button.onClick (fun _ ->
let ctx = System.Threading.SynchronizationContext()
async {
let ofd = OpenFolderDialog()
let window = getWindow()
let! path =
ofd.ShowAsync(window)
|> Async.AwaitTask
do! Async.SwitchToContext ctx
path
|> Msgs.SomeMsg
|> dispatch
} |> Async.Start
)
]
but I'm not sure whether this is a good approach or not.
Hi!
When I comment this line out, I get an issue that the whole board stops to evolve:
Could you please help me to understand why it happens so?
P.S. Great project, thank you!
Hi @JaggerJo are the HTML docs for this project published anywhere?
I'm adding a "Use F# for Desktop Apps" to http://fsharp.org and making this a main entry, just wondering what to link to.
The current / 1.x version of FuncUI is basically a proof of concept that escalated a bit. I started using it for my projects and a lot of my assumptions on "how to do x" were right, but some were wrong. The main issues that will be addressed with the next version are:
FuncUI 1.x has a pretty readable DSL for defining views in a type checked manner. But there are a few downsides that need to be addressed:
Reflection is used everywhere. This has several downsides (Performance, AOT Limitations, ..). The vNext will not use reflection AT ALL.
The DSL currently uses Statically Resolved Type Constraints. This does not scale well (ask for details). The new DSL will use a different and in my opinion overall better approach. Static E
extension methods (or other members) that are attached to the control type. This also leads to a better browsable DSL.
TextBox.create [
TextBox.text state.Name
TextBox.onTextChanged (fun text -> dispatch ..)
]
Currently there is no way of knowing what control a View will create. This is no problem in most cases, but makes it harder to provide a good API in some cases. For example when a control takes another control of a certain type as an attribute (Popup / Tooltip / ...).
v1.x
let btn : View = Views.button [ ... ]
vNext
let btn : IView<Button> = Views.button [ ... ]
let btn : IView = Views.button [ ... ]
The current virtual Dom implementation is super primitive. This was good in the beginning but there are some thing that need to change.
In Avalonia all Properties, RoutedEvents and Events are (with a bit of work) Observable. They will be treated all the same and you will finally be able to subscribe to text changed notifications.
This is needed because for example the Grid
Control throws an error if the ColumnDefinitions are set more than one time and ColumnDefinitons
don't implement value equality.
This will also bring some performance improvements because currently bitmaps are also created on each state change.
It will also be possible to configure the LazyViewCache and stuff like that.
This is the stock Counter template project changed like this:
module Counter =
type State = { value : string }
let init = { value = "" }
type Msg = | SetValue of string
let update (msg: Msg) (state: State) : State =
match msg with
| SetValue v -> { state with value = v }
let view (state: State) (dispatch) =
TextBox.create [
TextBox.text "123"
TextBox.onTextChanged (fun t -> dispatch (SetValue t))
]
With withConsoleTrace
on running the program gives
New message:: SetValue "123"
Updated state:: { v = "123" }
printed repeatedly. netcoreapp3.1 on Windows, latest FuncUI and Avalonia packages. I don't get any problems with the FuncUI control catalog project.
FuncUI currently implements super simple list diffing that works just fine in most cases.
There are cases where a list contains complex or a lot of different items this can be problematic. Especially when items are inserted at index 1 - because this currently will result in rebuilding all items.
Implementing a 2nd list diffing strategy that utilises keys to reduce the required patch work could speed up list diffing by a lot. (here is how react does it)
Adding a Key to Avalonia Controls is easily possible using AttachedProperties. (We actually implicitly attache properties to controls for internal subscription handling - IIRC)
StackPanel.children [
TextBlock.create [
TextBlock.key "item 1"
TextBlock.text "item 1"
...
]
...
]
I think it makes sense to add link to the docs (or alternatively replace one that leads to the wiki) in the ReadMe under Getting started
section.
Here's your counter example rewritten just using functions instead of DU's. It removes the need for a centralised msg handler.
namespace CounterElmishSample
open System
open Avalonia.Controls
open Avalonia.Controls
open Avalonia.FuncUI.DSL
open Avalonia.FuncUI.Components
open Avalonia.FuncUI.Components
open Avalonia.FuncUI.DSL
open Avalonia.FuncUI.DSL
open Avalonia.FuncUI.Types
open Avalonia.Layout
type CustomControl() =
inherit Control()
member val Text: string = "" with get, set
[<AutoOpen>]
module ViewExt =
()
module Counter =
open Avalonia.FuncUI.DSL
type CounterState = {
count : int
numbers: int list
}
let init = {
count = 0
numbers = [0 .. 100_000]
}
let increment state = {state with count = state.count + 1}
let decrement state = {state with count = state.count - 1}
let specific number_state = {state with count = number }
let remove_number = { state with numbers = List.except [number] state.numbers }
let view (state: CounterState) (dispatch) =
DockPanel.create [
DockPanel.children [
ListBox.create [
ListBox.items state.numbers
ListBox.itemTemplate (
TemplateView.create(fun data ->
let data = data :?> int
DockPanel.create [
DockPanel.children [
Button.create [
Button.content "delete"
Button.dock Dock.Right
Button.width 50.0
Button.tag data
Button.onClick (fun args ->
let number = (args.Source :?> Button).Tag :?> int
dispatch remove_number number
)
]
TextBlock.create [
TextBlock.text (sprintf "%A" data)
TextBlock.width 100.0
]
]
]
|> generalize
)
)
]
(*
TextBox.create [
TextBox.dock Dock.Bottom
TextBox.text (sprintf "%i" state.count)
TextBox.onTextChanged (fun text ->
printfn "new Text: %s" text
)
]
TextBlock.create [
TextBlock.dock Dock.Top
TextBlock.fontSize 48.0
TextBlock.foreground "blue"
TextBlock.verticalAlignment VerticalAlignment.Center
TextBlock.horizontalAlignment HorizontalAlignment.Center
TextBlock.text (string state.count)
]
LazyView.create [
LazyView.args dispatch
LazyView.state state.count
LazyView.viewFunc (fun state dispatch ->
let view =
TextBlock.create [
TextBlock.dock Dock.Top
TextBlock.fontSize 48.0
TextBlock.foreground "green"
TextBlock.verticalAlignment VerticalAlignment.Center
TextBlock.horizontalAlignment HorizontalAlignment.Center
TextBlock.text (string state)
]
view |> fun a -> a :> IView
)
]
*)
]
]
Is there an advantage to using the DU's here that just plain function composition can't do? For simple functions it is even possible to inline the operations.
Instead of
Button.onClick (fun args ->
let number = (args.Source :?> Button).Tag :?> int
dispatch remove_number number
)
you could write
Button.onClick (fun args ->
let number = (args.Source :?> Button).Tag :?> int
dispatch (fun state -> { state with numbers = List.except [number] state.numbers })
)
In the MusicPlayer example, this represents song in the playlist.
let private songTemplate (song: Types.SongRecord) (dispatch: Msg -> unit) =
StackPanel.create [
StackPanel.spacing 8.0
StackPanel.onDoubleTapped ((fun _ -> dispatch (PlaySong song)), SubPatchOptions.OnChangeOf song)
StackPanel.onKeyUp (fun keyargs ->
match keyargs.Key with
| Key.Enter -> dispatch (PlaySong song)
/// eventually add other shortcuts to re-arrange songs or something alike
| _ -> ()
, SubPatchOptions.OnChangeOf song)
StackPanel.children [
TextBlock.create [
TextBlock.text song.name
]
]
]
In the app itself, onDoubleTapped does not fire if the TextBlock does not pressed because StackPanel is the same size as it. I tried to change it by using Grid and DockPanel by they were like that too. I saw this stackoverflow page and tried that by adding
<Style Selector="ListBoxItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch"></Setter>
</Style>
to Styles.xaml but to no avail. Couldn't add it to ListBox because it would throw error saying that property is not available in ListBox.
When i added background property to Grid, like this
Grid.create [
Grid.rowDefinitions "*"
Grid.columnDefinitions "*"
Grid.background Brushes.Aqua
Grid.children [
, it somehow got all available space though. Why is this happening and is there another way to stretch these panels all over ListBoxItem?
I've finally learned enough FUNCUI to get working what I wanted.
The text parsing error in my current example does not have to be stored in the global state store. It is local to the textbox view and is reset if the the outer global state changes. So we can think of it as a temporary local program that gets a job done and then is discarded.
Some pre-comments. A Redux<'a>
at it's most basic is an object with Get
and Set
members. It is a merging of state and dispatch. When you call Set a message is dispatched to update the data that this redux points to. With that understanding we can look at the example.
The example demonstrates the capability of a new subclass of HostControl called LocalStateControl. I took the code from LazyView and added the ability for the view to have it's own local state with a lifetime not greater than when a difference is detected in the incoming outer state.
The original code is at
https://github.com/bradphelan/FUNCUI.Samples/blob/master/src/Examples/ValidatingTextBoxApp/Main.fs
namespace BGS
open Avalonia.FuncUI.DSL
open XTargets.Elmish
open System
open Avalonia.Controls
open Avalonia.Layout
open FSharpx
open Avalonia
open Avalonia.FuncUI.Types
module Data =
// Create a simple model with two int fields
type Item = {
value0: int
value1: int
} with
// Provide lenses for focusing on seperate fields
static member value0' = (fun o->o.value0),(fun v o -> {o with value0 = v})
static member value1' = (fun o->o.value1),(fun v o -> {o with value1 = v})
// Provide an initializer
static member init = { value0 = 0; value1 = 1 }
module ItemView =
// the view recieves a `Redux` or a pointer to the data it needs to
// render and update. The `Redux` object has Get and Set methods.
// Calling `Set` fires the dispatcher with a message that knows
// how to do the update on the root data.
let view (item:Redux<Data.Item>) =
// Get a redux for each sub property by using the lens combinators
let value0:Redux<int> = (item >-> Data.Item.value0')
let value1:Redux<int> = (item >-> Data.Item.value1')
// Generate a form field for a specific property
let inline formField label (value:Redux<int>) =
StackPanel.create [
StackPanel.orientation Orientation.Horizontal
StackPanel.children [
TextBlock.create [
TextBlock.text label
TextBlock.width 150.0
]
LocalStateView.create [
// Set state so that the patch algorithm knows to rerender if the value changes
LocalStateView.state value.Get
// Set the view function for rendering. The view function should
// take 1 parameter being a Redux<'a> when 'a : equality. In this
// case we want the errHandler to be our local state and we
// want `string option` though it could be almost anything we want
LocalStateView.viewFunc ( fun (errHandler:Redux<string option>) ->
TextBox.create [
// Render the current value for the text
TextBox.text (string value.Get)
// Convert the Redux<int> to Redux<string> via a two way value converter.
// The setter of a Redux<string option> is passed to collect any parsing
// errors. Notice that `errHandler` is the local state that is passed
// into the view. It doesn't not propagate out of this view. It will
// always be reset to the default value if the state propery is updated
let stringValue:Redux<string> = value.Convert ValueConverters.StringToInt32 errHandler.Set
// Bind the Set and Get methods of the stringValue to the TextBox. See bindText
yield! stringValue |> TextBox.bindText
// Collect the current parse errors and store them in the errors field
// of the textbox
let parseErrors =
errHandler.Get
|> Option.toArray
|> Seq.cast<obj>
TextBox.errors parseErrors
TextBox.width 150.0
] :> IView
)
]
]
]
StackPanel.create [
StackPanel.orientation Orientation.Vertical
StackPanel.children [
formField "Value 0" value0
formField "Value 1" value1
]
]
The Wiki currently contains a few articles that need to be reviewed. Code Examples are for FuncUI 0.1 and therefor don't work anymore (breaking change from 0.1 -> current).
MCVE
module Sample =
open Avalonia.Controls
[<RequireQualifiedAccess>]
type State =
| First
| Second
let update state _ = state
let init() = State.First
let first dispatch =
Grid.create [
Grid.rowDefinitions "Auto"
Grid.columnDefinitions "Auto"
Grid.children [
Button.create [
Grid.column 0
Grid.row 0
Button.content "Second"
Button.onClick (fun _ -> State.Second |> dispatch)
]
]
]
let second dispatch =
Grid.create [
Grid.rowDefinitions "Auto"
Grid.children [
Button.create [
Grid.row 0
Button.content "First"
Button.onClick (fun _ -> State.First |> dispatch)
]
]
]
let view (state: State) (dispatch) =
match state with
| State.First -> first dispatch :> IView
| State.Second -> second dispatch :> IView
Elmish.Program.mkSimple Sample.init Sample.update Sample.view
Expected behaviour:
Click on btn navigates to next page
Actual behaviour:
Click on button changes state but view looks the same.
Without specifyingGrid.columnDefinitions "Auto"
and Grid.column 0
for the first view it works fine.
I don't like title but I wasn't able to come to anything better.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.