F#中如何通过非阻塞异步方法更新WPF GUI且避免跨线程访问错误
错误原因
System.InvalidOperationException: 'The calling thread cannot access this object because a different thread owns it.'是WPF开发中典型的跨线程访问错误:WPF要求所有和UI绑定的数据、控件的修改操作必须在UI线程执行,现有代码中异步获取打印机完成后的model更新逻辑是在后台线程触发的,因此触发了校验报错。
Elmish.WPF 标准解决方案
Elmish框架已经内置了异步操作的线程上下文封送逻辑,只需按照Elmish的规范改写异步调用即可,不需要手动处理线程调度,全程不会阻塞UI线程:
- 首先修正异步方法的定义,把同步获取打印机的逻辑包装为标准Async类型:
open System.Printing open Elmish // 同步获取打印机列表的逻辑保持不变 let GetPrinters () = let LocalPrintServer = new PrintServer() let printQueues = LocalPrintServer.GetPrintQueues [|EnumeratedPrintQueueTypes.Local; EnumeratedPrintQueueTypes.Connections|] printQueues |> Seq.cast<PrintQueue> |> Seq.toList // 包装为异步方法,也可以直接在Cmd中使用无需单独定义 let GetPrintersAsync () = async { return GetPrinters() }
- 新增异步完成的消息类型,用于接收异步返回的结果:
type Msg = | RefreshPrinters // 按钮点击时发送的触发消息 | RefreshPrintersCompleted of PrintQueue list // 异步获取完成后接收结果的消息
- 在Update函数中用Elmish内置的
Cmd.OfAsync处理异步逻辑,框架会自动把异步结果封送回UI线程执行更新:
let update (msg: Msg) (m: Model) = match msg with | RefreshPrinters -> // 直接把IsRefreshing设为true,UI绑定后会自动触发控件动画,不会阻塞 let asyncCmd = Cmd.OfAsync.perform GetPrinters () RefreshPrintersCompleted { m with IsRefreshing = true }, asyncCmd | RefreshPrintersCompleted printers -> // 此处逻辑默认在UI线程执行,不会触发跨线程错误 let printerList = 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 = printerList; IsRefreshing = false }, Cmd.none
通用原生WPF解决方案(非Elmish场景)
如果不使用Elmish框架,可以手动用WPF的Dispatcher把更新逻辑封送到UI线程执行:
open System.Windows.Threading // 异步获取完成后调用Dispatcher执行更新 Dispatcher.CurrentDispatcher.Invoke(fun () -> // 此处放置更新model、修改UI控件的逻辑 let printerList = getprinters printers { m with Printers = printerList; IsRefreshing = false } )
原有代码问题说明
之前定义的GetPrintersAsync用法有误,Async.StartChild是用于启动嵌套子异步计算的API,不适合用来包装同步方法,直接用async { return 同步方法() }的形式包装即可。
内容的提问来源于stack exchange,提问作者Alan Wayne
相关产品推荐
相关产品推荐

