How to update the WPF GUI from non-blocking async methods in F#

247 views Asked by At

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

I have a WPF GUI with a button that when clicked does:

  1. starts a control animation (on the GUI), and
  2. starts a background process to obtain the local printer queues.

I do not want to block the main thread (GUI). However, the code I have gives the above error when I try to update the main thread with the results of the background process.

How do I have a background async process update the main thread without a context violation and not blocking the main thread?

open System.Printing

let GetPrinters = 
      let LocalPrintServer = new PrintServer()
      let printQueues = LocalPrintServer.GetPrintQueues [|EnumeratedPrintQueueTypes.Local; EnumeratedPrintQueueTypes.Connections|]
      let printerList =
          printQueues
              |> Seq.cast<PrintQueue>
              |> Seq.toList
      printerList


let GetPrintersAsync() = 
       async { 
               let! token = Async.StartChild(GetPrinters)
               let! p = token
               return p }

This is the update procedure I'm using:

let asyncUpper  =
        async {
               let! printerQues = GetPrintersAsync ()
               return printerQues
            }


// This is where the error is being displayed.
let getprinters (printers:PrintQueue list) =  
       printers 
          |> List.map (fun pq ->  {fullname = pq.FullName; comment = pq.Comment; defaultPrintTicket= Some pq.DefaultPrintTicket; 
                                                                         description= pq.Description; isInError=pq.IsInError; isOffline=pq.IsOffline; Id= Guid.NewGuid()  } ) 
                                                     
{ m with Printers = getprinters; IsRefreshing = false }

Edit #1: The above is a short version of the complete listing. Please see https://github.com/awaynemd/AsyncAndElmish for the complete source code using Elmish.wpf. Thank you.

3

There are 3 answers

2
Bent Tranberg On BEST ANSWER

I've had a chance to look at your source on GitHub now, and even run it.

The problem is that the print queues are retrieved in an async function, which means another thread than the GUI thread. Then the list of queues are returned to the GUI thread, and accessed from there. That's why you get the error message. When the queues are returned to the GUI thread, then they are mapped to the Printer type. This is too late. If you move that mapping into the async instead, then it won't be too late. The data returned to the GUI thread will be Printer list, which is perhaps fine. At least there's no crash. I am not one hundred percent sure if it's ok, because there's a field of type PrintTicket in there, and the question is whether it's safe to pull this across to another thread. If you need data from that object, maybe this too should be mapped to a record in the async before being returned to the GUI thread.

While trying to get it running without the error, this is the code I ended up with. I am not that knowledgeable about async either, and I'm not sure whether there's any point using async for this case. But maybe you're just trying out stuff.

| GetPrintersMsg ->
    let getPrinters () = async {
        use ps = new PrintServer()
        return
            ps.GetPrintQueues [| EnumeratedPrintQueueTypes.Local; EnumeratedPrintQueueTypes.Connections |]
            |> Seq.cast<PrintQueue>
            |> Seq.toList
            |> List.map (fun pq ->
                {
                    Id = Guid.NewGuid()
                    fullname = pq.FullName
                    comment = pq.Comment
                    defaultPrintTicket = Some pq.DefaultPrintTicket
                    description = pq.Description
                    isInError = pq.IsInError
                    isOffline = pq.IsOffline
                })
        }
    m, Cmd.OfAsync.either getPrinters () OnPrintersResult OnPrintersError
| OnPrintersResult printers ->
    { m with Printers = printers; IsRefreshing = false }, Cmd.none
6
Alan Wayne On

@BentTranberg actually answered the hard part of this question. I post this as the completed answer since editing the question seems redundant. The below code is Bent's answer with a few modifications. The printers are now being read on a separate thread as seen with the printfn statements:

| GetPrintersMsg ->
    let getPrinters () = async {
        printfn "1: %i" Thread.CurrentThread.ManagedThreadId  

        let getprinters = async {
            printfn "11: %i" Thread.CurrentThread.ManagedThreadId  
            use ps = new PrintServer()
            return
                ps.GetPrintQueues [| EnumeratedPrintQueueTypes.Local; EnumeratedPrintQueueTypes.Connections |]
                |> Seq.cast<PrintQueue>
                |> Seq.toList
                |> List.map (fun pq ->
                    {
                        Id = Guid.NewGuid()
                        fullname = pq.FullName
                        comment = pq.Comment
                        defaultPrintTicket = Some pq.DefaultPrintTicket
                        description = pq.Description
                        isInError = pq.IsInError
                        isOffline = pq.IsOffline
                    }) }
        let! d = getprinters |> Async.StartChild
        return! d
        }
    m, Cmd.OfAsync.either getPrinters () OnPrintersResult OnPrintersError
1
Brian Berns On

I haven't looked at your code, but I think the basic answer to your question for WPF is the Dispatcher class. You can also use F#'s Async.SwitchToContext. See this SO question, for example.