fsprojects / Avalonia.FuncUI

Develop cross-plattform GUI Applications using F# and Avalonia!
https://funcui.avaloniaui.net/
MIT License
889 stars 72 forks source link

Question: How to make funcui use DataTemplates #370

Closed fwaris closed 7 months ago

fwaris commented 7 months ago

I am trying to integrate the AvaloniaGraphControl (https://github.com/Oaz/AvaloniaGraphControl) into funcui.

The basic graph shows up fine. The issue is with the formatting of graph elements (e.g. nodes).

In the AvaloniaGraphControl xaml examples, node formatting is handled by DataTemplates that are defined under the GraphPanel control in xaml.

I have tried adding an IDataTemplate to the GraphPanel.DataTemplates variable but its is not being pickup when running under funcui.

Please see attached .fsx file that comprise an example graph with formatting. I would appreciate any quick suggestions to try to fix this issue.

fwaris commented 7 months ago

Note for some reason I cannot attach files so emdedding the .fsx file inline:

event_loop.fsx

#r "nuget: AVLoop"
#r "nuget: AvaloniaGraphControl"
#r "nuget: Avalonia.FuncUI.Elmish"

open FSharp.Compiler.Interactive
open AVLoop

let install(theme) =
        fsi.EventLoop <- {new IEventLoop with 
                                member x.Run() = 
                                    createApp(theme, [||])
                                    false //dummy
                                member x.Invoke(f) = disp f
                                member x.ScheduleRestart() = () //dummy
                        }

install(Default,Dark) //wait till initialization message before submitting more code

Graph.fsx

#load "event_loop.fsx" //run by itself first till you see "avalonia initialized" and then submit other code
open Elmish
open Avalonia.Controls
open Avalonia.Media
open Avalonia.FuncUI
open Avalonia.FuncUI.DSL
open Avalonia.FuncUI.Hosts
open Avalonia.FuncUI.Elmish

type [<CLIMutable>] Node = {Label:string}
let order (a,b) = if a >= b then a,b else b,a

let deviceRels =
  [
    "gnodeb_id", ["cell_id"; "rmod"; "bbmod"]
    "enodeb_id", ["cell_id"; "rmod"; "bbomd"]
    "cell_id", ["sector_id"]
    "rmod",["sfp"]
    "bbmod",["sfp"; "smod"; "cabinet"]
    "smod", ["cabinet"]
  ]

let dg = new AvaloniaGraphControl.Graph()
dg.Edges.Clear()
deviceRels |> List.collect (fun (r,xs) -> xs |> List.map(fun b -> order(r,b))) |> List.distinct
|> List.iter (fun (a,b) -> 
  dg.Edges.Add(AvaloniaGraphControl.Edge(
    {Label=a},
    {Label=b},
    tailSymbol = AvaloniaGraphControl.Edge.Symbol.None,
    headSymbol = AvaloniaGraphControl.Edge.Symbol.None)))

[<AutoOpen>]
module GraphPanel =
    open Avalonia.FuncUI.Types
    open Avalonia.FuncUI.Builder
    open AvaloniaGraphControl
    open Avalonia.Controls.Templates

    let create (attrs: IAttr<GraphPanel> list): IView<GraphPanel> =
        ViewBuilder.Create<GraphPanel>(attrs)

    type GraphPanel with
        static member graph<'t when 't :> GraphPanel>(value:Graph) : IAttr<'t> = 
          AttrBuilder<'t>.CreateProperty<Graph>(GraphPanel.GraphProperty, value, ValueNone)          

        static member itemTemplate<'t when 't :> GraphPanel>(value: IDataTemplate) : IAttr<'t> =
          let getter : ('t -> IDataTemplate) = (fun t -> if t.DataTemplates.Count > 0 then  t.DataTemplates.[0] else null)
          let setter : ('t * IDataTemplate -> unit) = (fun (t,v) -> t.DataTemplates.Clear(); t.DataTemplates.Add(v))
          AttrBuilder<'t>.CreateProperty<IDataTemplate>("itemTemplate", value, ValueSome getter, ValueSome setter, ValueNone)

        static member dataTemplates<'t when 't :> GraphPanel>(value: DataTemplates) : IAttr<'t> =
          let getter : ('t -> DataTemplates) = (fun t -> if t.DataTemplates = null then new DataTemplates() else t.DataTemplates)
          let setter : ('t * DataTemplates -> unit) = (fun (t,v) -> t.DataTemplates.Clear(); t.DataTemplates.AddRange(v))
          AttrBuilder<'t>.CreateProperty<DataTemplates>("DataTemplates", value, ValueSome getter, ValueSome setter, ValueNone)

[<AutoOpen>]
module TextSticker =
    open Avalonia.FuncUI.Types
    open Avalonia.FuncUI.Builder
    open AvaloniaGraphControl

    let create (attrs: IAttr<TextSticker> list): IView<TextSticker> =
        ViewBuilder.Create<TextSticker>(attrs)

    type TextSticker with
        static member shape<'t when 't :> TextSticker>(value:TextSticker.Shapes) : IAttr<'t> = 
          AttrBuilder<'t>.CreateProperty<TextSticker.Shapes>(TextSticker.ShapeProperty, value, ValueNone)          

        static member text<'t when 't :> TextSticker>(value:string) : IAttr<'t> = 
          AttrBuilder<'t>.CreateProperty<string>(TextSticker.TextProperty, value, ValueNone)          

module Counter =
    open AvaloniaGraphControl
    open Avalonia.Controls.Templates

    type State = { count : int ; graph:AvaloniaGraphControl.Graph}
    let init = { count = 0; graph=dg}

    type Msg = Increment | Decrement | Reset

    let update (msg: Msg) (state: State) : State =
        match msg with
        | Increment -> { state with count = state.count + 1 }
        | Decrement -> { state with count = state.count - 1 }
        | Reset -> init

    let view (state: State) (dispatch) =
      Viewbox.create [
        Viewbox.child (
          GraphPanel.create [
            GraphPanel.graph state.graph
            GraphPanel.itemTemplate (
                  DataTemplateView<Node>.create (fun (data:Node) ->
                    TextSticker.create [
                      TextSticker.shape TextSticker.Shapes.Ellipse
                      TextSticker.text data.Label
                    ])                        
            )

            // GraphPanel.dataTemplates (
            //   let ds = DataTemplates()
            //   ds.AddRange(
            //     [
            //       DataTemplateView<Node>.create (fun (data:Node) ->
            //         TextSticker.create [
            //           TextSticker.shape TextSticker.Shapes.Ellipse
            //           TextSticker.text data.Label
            //         ])          
            //     ])
            //   ds
            //   )

            ]
            )            
          ]

type MainWindow() as this =
    inherit HostWindow()
    do
        base.Title <- "BasicTemplate"
        base.Background <- Brushes.White
        base.Width <- 400.0
        base.Height <- 400.0
        Program.mkSimple (fun () -> Counter.init) Counter.update Counter.view
        |> Program.withHost this
        |> Program.run

let win = MainWindow()
win.Show()
JaggerJo commented 7 months ago

@fwaris could you provide a runnable sample project? (just a single fsproj)

fwaris commented 7 months ago

sure ... let me put that together thanks

fwaris commented 7 months ago

please see a minimal sample in this repo https://github.com/fwaris/GraphControlSample.

The IDataTemplate is not being applied for node formatting. If it were, the nodes would have an elliptical shape (instead of rounded rectangle)

fwaris commented 7 months ago

@JaggerJo for some reason I am not able to attach files to github comments / issues. Instead I have created a minimal standalone sample in my personal github the link to the repo is : https://github.com/fwaris/GraphControlSample.

I would appreciate if you take a look and give me some suggestions or a workaround to fix this issue.

Thanks!

Faisal

Numpsy commented 7 months ago

Try setting the graph property after the templates property? e.g.

GraphPanel.create [

  GraphPanel.dataTemplates (
     let ds = DataTemplates()
     ds.AddRange(
       [
         DataTemplateView<Node>.create (fun (data:Node) ->
           TextSticker.create [
             TextSticker.shape TextSticker.Shapes.Ellipse
             TextSticker.text data.Label
           ])          
       ])
     ds
     )
  GraphPanel.graph state.graph
  ]
Numpsy commented 7 months ago

I don't see a itemTemplate property on GraphPanel at all?

fwaris commented 7 months ago

that works. thanks!

(the itemTemplate was an experiment to see if a single template could be added to the DataTemplates instance variable using just a funcui function - but that does not seem to work)