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