Search code examples
multithreadingasynchronousf#window

F# Making the result of an async workflow available to be displayed by the UI thread


I'm trying to launch a window which will require waiting for a login to an external system, so I would like to handle this login asynchronously. I've achieved this in the F# interactive window and everything behaves as expected, however when I run the code in my program I get the error:

System.InvalidOperationException: The calling thread cannot access this object because a different thread owns it.

F# Interactive:

#r "WindowsBase"
#r "PresentationCore"
#r "PresentationFramework"

open System
open System.Threading
open System.Windows

let loginTask = async {
    Console.WriteLine "Logging in"
    Thread.Sleep(5000)
    let user = "MyUser"
    Console.WriteLine ("Logged in as " + user)
    return user
}

let createWindow () =
    Console.WriteLine "Creating window"
    let window = Window()
    window.Title <- "MyWindow"
    window.Show()
    window

let runWindowWithUser (window:Window) user =
    window.Title <- (user + "'s Window")
    Console.WriteLine ("Running " + window.Title + " as " + user)

let mainAsync = async {
    let window = createWindow()

    let! userToken = loginTask |> Async.StartChild
    let! user = userToken 

    runWindowWithUser window user
    }

do mainAsync |> Async.StartImmediate

Program.fs

open System.Threading
open System.Windows

[<EntryPoint; STAThread>]
let main argv =

    let loginTask = async {
        Console.WriteLine "Logging in"
        Thread.Sleep(5000)
        let user = "MyUser"
        Console.WriteLine ("Logged in as " + user)
        return user
    }
    
    let createWindow () =
        Console.WriteLine "Creating window"
        let window = Window()
        window.Title <- "MyWindow"
        window.Show()
        window
    
    let runWindowWithUser (window:Window) user =
        window.Title <- (user + "'s Window")
        Console.WriteLine ("Running " + window.Title + " as " + user)
    
    let mainAsync = async {
        let window = createWindow()
    
        let! userToken = loginTask |> Async.StartChild
        let! user = userToken 
    
        runWindowWithUser window user
        }
    
    do mainAsync |> Async.StartImmediate

    Console.ReadKey()

    1

I understand that let! can cause the rest of the workflow to continue on the background thread so I have tried swapping threads:

    let mainAsync = async {
        let context = SynchronizationContext.Current
        let window = createWindow()
    
        do! Async.SwitchToThreadPool()
        let! user = loginTask
        
        do! Async.SwitchToContext context
        runWindowWithUser window user
    }

but this doesn't seem to change back to the original thread as I'm expecting.

I also tried to keep all of my UI code out of the async workflow to avoid dealing with threads, but then I'm unsure of how to get my user information back from work which is done on a background thread

let loginTask = async {
    Console.WriteLine "Logging in"
    Thread.Sleep(5000)
    let user = "MyUser"
    Console.WriteLine ("Logged in as " + user)
    return user
}

Console.WriteLine "Creating window"
let window = Window()
window.Title <- "MyWindow"
window.Show()

let user = loginTask|> Async.StartImmediate   // How do I get user information from loginTask without using let! that must be called from an async workflow?

window.Title <- (user + "'s Window")
Console.WriteLine ("Running " + window.Title + " as " + user)

I'm very new to F# and functional programming in general. How am I able to get the user information from the login code on a background thread into the UI thread, and also why is the threading behavior different in the F# interactive window?


Solution

  • Here's my code, I've removed the Console.Writeline() calls, you would just replace those with logger calls (or Debug.WriteLine(), or whatever).

    open System
    open System.Threading
    open System.Windows
    
    let createWindow() =
        let window = Window()
        window.Title <- "MyWindow"
        window.Show()
        window
    
    let runWindowWithUser (window: Window) user =
        window.Title <- (user + "'s Window")
    
    let loginTask = 
        async {
            Thread.Sleep(1000)
            let user = "MyUser"
            return user
        }
        
    [<EntryPoint; STAThread>]
    let main _ =
        let app = Application()
        app.MainWindow <- createWindow()
    
        app.Startup.Add (fun _ ->
            async {
                let context = SynchronizationContext.Current
                do! Async.SwitchToThreadPool()
                let! user = loginTask
                do! Async.SwitchToContext context
                runWindowWithUser app.MainWindow user
            } |> Async.StartImmediate
        )
    
        app.Run()