Search code examples
xamarinuwpf#xamarin.essentialsfabulous

Using Xamarin Essentials File Picker in Fabulous (F#) UWP app


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?


Solution

  • 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.