0

System.InvalidOperationException:“调用线程无法访问此对象,因为不同的线程拥有它。”

我有一个带有按钮的 WPF GUI,单击该按钮时:

  1. 启动控制动画(在 GUI 上),以及
  2. 启动后台进程以获取本地打印机队列。

我不想阻塞主线程(GUI)。但是,当我尝试使用后台进程的结果更新主线程时,我的代码给出了上述错误。

如何让后台异步进程在不违反上下文且不阻塞主线程的情况下更新主线程?

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 }

这是我正在使用的更新程序:

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 }

编辑#1:以上是完整列表的简短版本。有关使用 Elmish.wpf 的完整源代码,请参阅 https://github.com/awaynemd/AsyncAndElmish。谢谢你。

4

3 回答 3

1

我没有看过你的代码,但我认为你对 WPF 的问题的基本答案是Dispatcher类。您也可以使用 F# 的Async.SwitchToContext. 例如,请参阅这个 SO question。

于 2021-05-27T13:49:30.240 回答
1

我现在有机会在 GitHub 上查看您的源代码,甚至运行它。

问题是打印队列是在异步函数中检索的,这意味着除了 GUI 线程之外的另一个线程。然后队列列表返回到 GUI 线程,并从那里访问。这就是您收到错误消息的原因。当队列返回到 GUI 线程时,它们被映射到打印机类型。这为时已晚。如果您将该映射移到异步中,则为时不晚。返回到 GUI 线程的数据将是打印机列表,这可能没问题。至少没有崩溃。我不能百分百确定它是否可以,因为那里有一个 PrintTicket 类型的字段,问题是把它拉到另一个线程是否安全。如果您需要来自该对象的数据,

在尝试让它运行而没有错误时,这是​​我最终得到的代码。我对异步也不是很了解,而且我不确定在这种情况下使用异步是否有任何意义。但也许你只是在尝试一些东西。

| 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
于 2021-05-27T14:07:29.980 回答
0

@BentTranberg 实际上回答了这个问题的难点。我将其发布为完整的答案,因为编辑问题似乎是多余的。下面的代码是 Bent 的回答,做了一些修改。如 printfn 语句所示,打印机现在正在单独的线程上读取:

| 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
于 2021-05-27T22:16:08.853 回答