2

I am testing out making a UWP app using the Fabulous framework for writing functional cross-platform apps, and I want to use a FilePicker on a button press and use the selected file for some data processing.

Executing let fileResult = FilePicker.PickAsync() |> Async.AwaitTask opens the file picker and returns a Async<FileResult> after a file is picked (This to say that the button and subsequent function call executes), but the rest of the code following it will execute before the result can be used. If I append |> Async.RunSynchronously it (as expected) blocks the thread and no file can be chosen in the window that appears, although the return value would be the FileResult.

After looking into how this should be done, I realise that the file picker should be opened on the main thread, which leads me to a solution on the following form

let getFileResultAsync = 
                async {
                    let tcs = new TaskCompletionSource<FileResult>()
                    Device.BeginInvokeOnMainThread(fun () ->
                        async {
                            let! fileResult = FilePicker.PickAsync() |> Async.AwaitTask
                            tcs.SetResult(fileResult)
                        } 
                        |> Async.StartImmediate
                    )

                    return! tcs.Task |> Async.AwaitTask
                }

which will return Async<FileResult>, but it appears that the Device.BeginInvokeOnMainThread block is never accessed. How would I go about opening the FilePicker, selecting a file and then subsequently process the file in such an app?

GregC
  • 7,737
  • 2
  • 53
  • 67
MechMayhem
  • 75
  • 7
  • Is it possible that the internal async{...} block is not required? The documentation does not mention anything about asynchronous body of the lambda in the BeginInvokeOnMainThread. Device.BeginInvokeOnMainThread(fun () -> let fileResult = FilePicker.PickAsync() |> Async.AwaitTask tcs.SetResult(fileResult) ) – Piotr Rodak Jan 04 '21 at 19:46
  • That doesn't seem to do the trick, unfortunately. Same issue with the lambda in the BeginInvokeOnMainThread seemingly not executing at all – MechMayhem Jan 05 '21 at 08:59

1 Answers1

2

I figured out a way to do what I wanted by further looking into the test example and the Fabulous documentation for Updates and messages https://fsprojects.github.io/Fabulous/Fabulous.XamarinForms/update.html.

Basing it off of the standard app that is generated when creating a new Fabulous project, just have a given string in the model for e.g. the file path (I've called it FilePath) for now, and add three additional messages to type Msg as follows

type Msg =
...
| SelectFile
| PresentFile of FileResult 
| ErrorFileNotSelected

where the first is sent whenever the button for selecting a file is pressed, the second is sent with the file upon selected and the third is for if a user exits out of the file dialogue without selecting a file.

You need a function to select the file asynchronously

let selectFileAsync = 
        async {
            let! result = FilePicker.PickAsync() |> Async.AwaitTask
            return result
        } 

and a Fabulous.Cmd which calls the above function and sends the message further in the program (probably a better way of explaining that)

let selectFileCmd = async {
            let! file = selectFileAsync

            match file with 
            | null ->  return Some(ErrorFileNotSelected)
            | _  -> return Some(PresentFile file)
        }

and finally, add the three following patterns to the update, where selectFileCmd is called under SelectFile

let update msg model = 
...
| SelectFile ->
            {model with FilePath = "Selecting file"}, (Cmd.ofAsyncMsgOption selectFileCmd)
| PresentFile file -> 
            {model with FilePath = file.FullPath}, Cmd.none 
| ErrorFileNotSelected -> 
            {model with FilePath = "Error. Must select a file"}, Cmd.none

I am not sure whether this is considered a good approach, but it seems better than utilising let mutable at least.

MechMayhem
  • 75
  • 7