Search code examples
asynchronousf#avalonia

How to pick a file using F# Avalonia FuncUI, a problem with async


I've got a working FuncUI app, but adding a file picker to get some text from the file system is proving tricky. I've stripped my app down to the following app based on a modified version of the counter app on the Avalonia FuncUI home page. It runs fine, but hangs at line 34 in the Async.RunSynchronously after you press the "Load File" button:

namespace CounterApp

open Avalonia
open Avalonia.Controls.ApplicationLifetimes
open Avalonia.Themes.Fluent
open Avalonia.FuncUI.Hosts
open Avalonia.Controls
open Avalonia.FuncUI
open Avalonia.FuncUI.DSL
open Avalonia.Layout
open Avalonia.Platform.Storage
open System.IO


module Main =

    let view () =
        Component(fun ctx ->
            let state = ctx.useState 0

            let top = TopLevel.GetTopLevel ctx.control

            let useFiles() =
              async {
                let options = FilePickerOpenOptions(Title = "Open Text File", AllowMultiple = false)
         
                let! result = top.StorageProvider.OpenFilePickerAsync(options) |> Async.AwaitTask
          
                return result
              }
            
            let callback _ =
              useFiles()
              |> Async.RunSynchronously |> ignore

            DockPanel.create [
                DockPanel.children [
                    Button.create [
                        Button.dock Dock.Bottom
                        Button.onClick callback
                        Button.content "Load file"
                        Button.horizontalAlignment HorizontalAlignment.Stretch
                        Button.horizontalContentAlignment HorizontalAlignment.Center
                    ]                   
                ]
            ]
        )

type MainWindow() =
    inherit HostWindow()
    do
        base.Title <- "Counter Example"
        base.Content <- Main.view ()

type App() =
    inherit Application()

    override this.Initialize() =
        this.Styles.Add (FluentTheme())
        this.RequestedThemeVariant <- Styling.ThemeVariant.Dark

    override this.OnFrameworkInitializationCompleted() =
        match this.ApplicationLifetime with
        | :? IClassicDesktopStyleApplicationLifetime as desktopLifetime ->
            desktopLifetime.MainWindow <- MainWindow()
        | _ -> ()

module Program =

    [<EntryPoint>]
    let main(args: string[]) =
        AppBuilder
            .Configure<App>()
            .UsePlatformDetect()
            .UseSkia()
            .StartWithClassicDesktopLifetime(args)

This is my best approximation at what might be a way to go, but I am now stuck. Any ideas on what would make this working code?


Solution

  • While looking to see if anyone had answered my question, Google reported an Avalonia FuncUI link on github to me, which in turn, led me to reading an article on Elmish, the wider context for my app.

    Turns out all I needed to proceed was to make the callback a void function, and to start the task immediately:

    let useFiles() =
      async {
        let options = FilePickerOpenOptions(Title = "Open Text File",
                                            AllowMultiple = false)
             
        let! result = 
          top.StorageProvider.OpenFilePickerAsync(options)
          |> Async.AwaitTask
                    
        System.Console.WriteLine "result now available"
      }
                
    let callback _ =
      useFiles()
      |> Async.StartImmediate
    

    This successfully calls the file picker, thus solving my stated problem. I can now continue and make use of the file picker's choice returned in result.