F# cross-thread UI exception in WinForms App

前端 未结 3 422
盖世英雄少女心
盖世英雄少女心 2021-01-21 09:55

I have a problem with developing a simple application in F#, which just reads the length of the requested HTML page.

Seems to be that such an error would be similar for

相关标签:
3条回答
  • 2021-01-21 10:10

    You must switch thread context to UI thread from Async ThreadPool prior to updating text.Text property. See MSDN link for the F# Async-specific explanation.

    After modifying your snippet by capturing UI context with

    let uiContext = System.Threading.SynchronizationContext()
    

    placed right after your let form = new Form() statement and changing fetchAsync definition to

    let fetchAsync(name, url:string) =
        async { 
            try 
                let uri = new System.Uri(url)
                let webClient = new WebClient()
                let! html = webClient.AsyncDownloadString(uri)
                do! Async.SwitchToContext(uiContext)
                text.Text <- text.Text + String.Format("Read {0} characters for {1}\n", html.Length, name)
            with
                | ex -> printfn "%s" (ex.Message);
        }
    

    it works without any problems.

    UPDATE: After discussing the debugger idiosyncrasy with a colleague, who emphasized the need of cleanly manipulating UI context, the following modification is agnostic now to the manner of run:

    open System
    open System.Net
    open Microsoft.FSharp.Control.WebExtensions
    open System.Windows.Forms
    open System.Threading
    
    let form = new Form()
    let text = new Label()
    let button = new Button()
    
    let urlList = [ "Microsoft.com", "http://www.microsoft.com/"
                    "MSDN", "http://msdn.microsoft.com/"
                    "Bing", "http://www.bing.com"
                  ]
    
    let fetchAsync(name, url:string, ctx) =
        async {
            try
                let uri = new System.Uri(url)
                let webClient = new WebClient()
                let! html = webClient.AsyncDownloadString(uri)
                do! Async.SwitchToContext ctx
                text.Text <- text.Text + sprintf "Read %d characters for %s\n" html.Length name
            with
                | ex -> printfn "%s" (ex.Message);
        }
    
    let runAll() =
        let ctx = SynchronizationContext.Current
        text.Text <- String.Format("{0}\n", System.DateTime.Now)
        urlList
        |> Seq.map (fun(site, url) -> fetchAsync(site, url, ctx))
        |> Async.Parallel
        |> Async.Ignore
        |> Async.Start
    
    form.Width  <- 400
    form.Height <- 300
    form.Visible <- true
    form.Text <- "Test download tool"
    
    text.Width <- 200
    text.Height <- 100
    text.Top <- 0
    text.Left <- 0
    form.Controls.Add(text)
    
    button.Text <- "click me"
    button.Top <- text.Height
    button.Left <- 0
    button.Click |> Event.add(fun sender -> runAll() |> ignore)
    form.Controls.Add(button)
    
    [<STAThread>]
    do Application.Run(form) 
    
    0 讨论(0)
  • 2021-01-21 10:23

    As an alternative to using the Invoke operation (or context switching) explicitly, you can also start the computation using Async.StartImmediate.

    This primitive starts the asynchronous workflow on a current thread (synchronization context) and then it ensures that all continuations are called on the same synchronization context - so it essentially handles Invoke automatically for you.

    To do that, you do not need to change anything in fetchAsync. You just need to change how the computation is started in runAll:

    let runAll() =
        urlList
        |> Seq.map fetchAsync
        |> Async.Parallel 
        |> Async.Ignore
        |> Async.StartImmediate
    

    Just like before, this composes all of them in parallel, then it ignores the result to get a computation of type Async<unit> and then it starts it on a current thread. This is one of the nice features in F# async :-)

    0 讨论(0)
  • 2021-01-21 10:23

    I had the same problem in that the Async.SwitchToContext did not switch to the Main Gui thread. It was in fact switching to some other thread.

    In the end I found that the problem was how I got the uiContext in the first place. Using the following worked:

    let uiContext = System.Threading.SynchronizationContext.Current
    

    But it didnt work with:

    let uiContext = System.Threading.SynchronizationContext()
    
    0 讨论(0)
提交回复
热议问题