Avalonia.Threading.Dispatcher: Call from invalid thread when using Cmd.OfAsync
There seems to be an issue with async workloads that are not scheduled on the UI thread.
module Program =
[<STAThread>] // <-- used for drag & drop.
[<EntryPoint>]
let main (args: string[]) =
AppBuilder
.Configure<App>()
.UsePlatformDetect()
.UseSkia()
.StartWithClassicDesktopLifetime(args)
...
let update (msg: AppMsg) (appModel: AppModel) =
match msg with
| ParseFile uri ->
let processing =
fun _ ->
async {
let! e = ThisWillThrowAsync()
...
}
{ appModel with
State = InputFileSelected uri}
, (Elmish.Cmd.OfAsync.attempt processing () (fun ex -> FailParse ex.Message))
The async workload is executed on the ThreadPool
All follow-up view & update code is then run from the TP Thread which ends up throwing:
Unhandled exception. System.InvalidOperationException: Call from invalid thread
at Avalonia.Threading.Dispatcher.<VerifyAccess>g__ThrowVerifyAccess|16_0()
at Avalonia.Threading.Dispatcher.VerifyAccess()
at Avalonia.AvaloniaObject.VerifyAccess()
at Avalonia.AvaloniaObject.GetValue[T](StyledProperty`1 property)
at Avalonia.Controls.ContentControl.get_Content()
at Avalonia.FuncUI.VirtualDom.VirtualDom.updateRoot(ContentControl host, FSharpOption`1 last, FSharpOption`1 next)
at Avalonia.FuncUI.Hosts.HostWindow.update(FSharpOption`1 nextViewElement)
at Avalonia.FuncUI.Hosts.HostWindow.Avalonia.FuncUI.Hosts.IViewHost.Update(FSharpOption`1 next)
at [email protected](model state, FSharpFunc`2 dispatch)
at [email protected](Unit unitVar0)
at [email protected](msg msg)
at [email protected](FSharpChoice`2 r)
at Microsoft.FSharp.Control.AsyncPrimitives.CallThenInvokeNoHijackCheck[a,b](AsyncActivation`1 ctxt, b result1, FSharpFunc`2 userCode) in D:\a\_work\1\s\src\FSharp.Core\async.fs:line 528
at Microsoft.FSharp.Control.Trampoline.Execute(FSharpFunc`2 firstAction) in D:\a\_work\1\s\src\FSharp.Core\async.fs:line 112
--- End of stack trace from previous location ---
at [email protected](ExceptionDispatchInfo edi) in D:\a\_work\1\s\src\FSharp.Core\async.fs:line 1174
at Microsoft.FSharp.Control.Trampoline.Execute(FSharpFunc`2 firstAction) in D:\a\_work\1\s\src\FSharp.Core\async.fs:line 112
at <StartupCode$FSharp-Core>[email protected](Object o) in D:\a\_work\1\s\src\FSharp.Core\async.fs:line 195
at System.Threading.ThreadPoolWorkQueue.Dispatch()
at System.Threading.PortableThreadPool.WorkerThread.WorkerThreadStart()
Running with Elmish.Cmd.OfAsyncImmediate.attempt fixes this, but puts the async code on the main thread.
Try Program.runWithAvaloniaSyncDispatch
Thanks, @JordanMarr. It works now.
Maybe it makes sense to set up an exception handler somewhere in the FuncUI stack, as a temporary measure? Make it reraise with more detailed instructions on setting up dispatcher synchronization.
I've just started using this library, so my understanding of its design is practically nil :). From a user perspective this looks like a bug though, because the ofError function only returns the message without calling into dispatch.
Not sure if this is connected, but the happy path messages don't get processed.
Full code:
let update (msg: AppMsg) (appModel: AppModel) =
match msg with
| ParseFile uri ->
let processing =
fun _ ->
async {
let! rows = Parser.AsyncLoad(uri.LocalPath) // the parser throws for invalid input
if (rows.Rows |> Seq.isEmpty |> not) then
return ItemsParsed(rows.Rows |> Seq.toArray)
else
return FailParse "No rows found in file"
}
{ appModel with
State = InputFileSelected uri}
, (Elmish.Cmd.OfAsyncImmediate.attempt processing () (fun ex -> FailParse ex.Message)
type AppMsg =
| ParseFile of Uri
| FailParse of string
| ItemsParsed of Parser.Row array
| FileDragDropMsg of FileDragDrop.FileDragDropMsg
I have tracing enabled with Program.withTrace (fun msg model subs -> printfn $"msg: {msg} {model} {subs}") and the messages don't get logged or otherwise processed.
Ushering async data back to the main UI thread has been a part of UI development for a long time. But I get your point.
I think Program.runWithAvaloniaSyncDispatch () should be used by default as there really is no drawback.
And maybe we should consider making that call obsolete and instead introducing:
Program.runFuncUI ()
Not sure if this is connected, but the happy path messages don't get processed.
You should be able to just use Cmd.OfAsync instead of Cmd.OfAsyncImmediate.
Stylistically, it would look better if you:
-
open Elmishinstead ofElmish.Cmdeverywhere - Make
FailParsetake anExceptioninstead of astring, and then could you just have:, Cmd.OfAsync.attempt processing () FailParse
For anyone reading this, the happy path issue was unrelated. either is what I should have used instead of attempt. attempt accepts functions with returns, but does not dispatch the successful, non-exception result.
If I get more time to spend on UI work, I could look into patching attempt to accept 'a -> unit functions only and submit a PR. @JordanMarr let me know if this sits well with the intended architecture.
Glad you got it working. 😎 I don’t personally think there is a need to change any of the built-in Elmish handlers in this library. It would be better to propose any changes in the Elmish repository itself.