问题
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 VB.NET/C# language too, when you develop the UI application.
But I'm rather new to F# and don't really imagine hot to fix such issue exactly in F#.
Source code in F#:
http://pastebin.com/e6WM0Sjw
open System
open System.Net
open Microsoft.FSharp.Control.WebExtensions
open System.Windows.Forms
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) =
async {
try
let uri = new System.Uri(url)
let webClient = new WebClient()
let! html = webClient.AsyncDownloadString(uri)
text.Text <- String.Format("Read %d characters for %s", html.Length, name)
with
| ex -> printfn "%s" (ex.Message);
}
let runAll() =
urlList
|> Seq.map fetchAsync
|> Async.Parallel
|> Async.RunSynchronously
|> ignore
form.Width <- 400
form.Height <- 300
form.Visible <- true
form.Text <- "Test download tool"
text.Width <- 200
text.Height <- 50
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)
Best Regards,
Thanks!
回答1:
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)
回答2:
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 :-)
回答3:
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()
来源:https://stackoverflow.com/questions/20590982/f-cross-thread-ui-exception-in-winforms-app