3

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.

enter image description here

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!

  • In VB/C# you'd have to `Invoke` the thread that created your control in order to perform any work upon it. When you get the error, it should point to a particular control in your code. You'll need to call `Invoke` or `BeginInvoke` with either a recursive target or a delegate to handle your work on the control. I know *nothing* about F# syntax, unfortunately. – Chris Barlow Dec 15 '13 at 04:24

3 Answers3

6

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) 
Gene Belitski
  • 10,270
  • 1
  • 34
  • 54
  • http://s28.postimg.org/oy89dbmtp/image.png I have tried to do as you suggested, but I've got the same error. –  Dec 16 '13 at 06:58
  • I'm positive if you run the app in Release mode everything will be OK. It is a known fact (google `f# async debugging` for use cases) that async code may behave weirdly under debugger. The need to run async under debugger didn't occur to me yet, so I cannot give a sound advice upon how to make the app working under debugger too. – Gene Belitski Dec 16 '13 at 07:34
  • Within my work envinronment VS2012 Ultimate I've tried changing any possible combination of platform, target .NET ver, etc., but can reproduce your problem only when running in Debug mode. – Gene Belitski Dec 16 '13 at 12:38
  • I've tried to use VS2013 Ultimate, here is proof: http://s15.postimg.org/yuzy9d6wr/image.png ( exception occuring in release mode ), hrer is a .net fw version with the F# runtime version: http://s30.postimg.org/y6pfuua1t/image.png –  Dec 16 '13 at 12:53
  • @GeloVolro: You last snapshot gave me some clue...Did you ever try running your app in VS with `CTRL-F5`? Please do, 'cause even in Release mode you ran it with Debugger ATTACHED. – Gene Belitski Dec 16 '13 at 14:52
  • Yes, indeed. With the Ctrl + F5 it has to work. Strange for me... Need to google the difference between just F5 and Ctrl + F5, although, thanks very much. –  Dec 16 '13 at 15:11
  • NP; one good reference is [Start Debugging vs. Start Without Debugging](http://blogs.msdn.com/b/zainnab/archive/2010/11/01/start-debugging-vs-start-without-debugging-vstipdebug0037.aspx). – Gene Belitski Dec 16 '13 at 15:27
4

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

Tomas Petricek
  • 240,744
  • 19
  • 378
  • 553
  • http://s22.postimg.org/73age2rht/image.png Tried to do as you suggested, but the same error. –  Dec 16 '13 at 07:00
2

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()
Ray
  • 31
  • 2