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
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)
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 :-)
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()