AvaloniaUI / Avalonia

Develop Desktop, Embedded, Mobile and WebAssembly apps with C# and XAML. The most popular .NET UI client technology
https://avaloniaui.net
MIT License
25.53k stars 2.21k forks source link

ReactiveCommand.CanExecute causes "Call from invalid thread" exception #3598

Closed mishun closed 1 year ago

mishun commented 4 years ago

Hi! Here's simple example program that has button that executes ReactiveCommand and CheckBox that should enable and disable button via ReactiveCommand.CanExecute:

// Program.fs
namespace Test

open System
open System.Threading
open ReactiveUI
open Avalonia
open Avalonia.Controls
open Avalonia.Controls.ApplicationLifetimes
open Avalonia.Markup.Xaml
open Avalonia.ReactiveUI

type MainWindow () as this =
    inherit Window ()

    let canExecute = Event<bool> ()
    let command =
        ReactiveCommand.Create(
            Action (fun () -> printfn "Hi!"),
            canExecute = canExecute.Publish
        )

    do
        this.InitializeComponent ()
        this.DataContext <- this

        let checkBox = this.FindControl<CheckBox> "checkBox"

        // Another thread variant:
        checkBox.Checked.Add(fun _ -> Thread(fun () -> canExecute.Trigger true).Start())
        checkBox.Unchecked.Add(fun _ -> Thread(fun () -> canExecute.Trigger false).Start())

        // UI thread variant:
        //checkBox.Checked.Add(fun _ -> canExecute.Trigger true)
        //checkBox.Unchecked.Add(fun _ -> canExecute.Trigger false)

    member private this.InitializeComponent () =
        AvaloniaXamlLoader.Load this

    member __.PushMeCommand = command

type App () =
    inherit Application()

    override this.Initialize () =
        AvaloniaXamlLoader.Load this

    override __.OnFrameworkInitializationCompleted () =
        base.OnFrameworkInitializationCompleted ()
        match base.ApplicationLifetime with
            | :? IClassicDesktopStyleApplicationLifetime as desktop ->
                desktop.MainWindow <- MainWindow ()
            | _ -> ()

module Test =
    let BuildAvaloniaApp() =
        AppBuilder.Configure<App>()
            .UseReactiveUI()
            .UsePlatformDetect()

    [<EntryPoint>]
    let main (args: string[]) =
        BuildAvaloniaApp().StartWithClassicDesktopLifetime(args)
<Window x:Class="Test.MainWindow"
        xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"

        Title="Test">

  <Grid RowDefinitions="*,*" ColumnDefinitions="120">
    <CheckBox Content="Button enabled" Name="checkBox" IsChecked="false" Grid.Row="0" />
    <Button Content="Push me!" Command="{Binding PushMeCommand}" Grid.Row="1" />
  </Grid>
</Window>

When toggling CheckBox it fails with following message:

Unhandled exception. System.InvalidOperationException: Call from invalid thread
   at Avalonia.Threading.Dispatcher.VerifyAccess()
   at Avalonia.AvaloniaObject.GetValue(AvaloniaProperty property)
   at Avalonia.AvaloniaObject.GetValue[T](AvaloniaProperty`1 property)
   at Avalonia.Controls.Button.get_CommandParameter()
   at Avalonia.Controls.Button.CanExecuteChanged(Object sender, EventArgs e)
   at ReactiveUI.ReactiveCommandBase`2.OnCanExecuteChanged(Boolean newValue) in d:\a\1\s\src\ReactiveUI\ReactiveCommand\ReactiveCommandBase.cs:line 198
   at System.Reactive.AnonymousSafeObserver`1.OnNext(T value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\AnonymousSafeObserver.cs:line 44
   at System.Reactive.Sink`1.ForwardOnNext(TTarget value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Internal\Sink.cs:line 50
   at System.Reactive.IdentitySink`1.OnNext(T value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Internal\IdentitySink.cs:line 16
   at System.Reactive.Subjects.FastImmediateObserver`1.EnsureActive(Int32 count) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Subjects\ReplaySubject.cs:line 858
   at System.Reactive.Subjects.FastImmediateObserver`1.EnsureActive() in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Subjects\ReplaySubject.cs:line 761
   at System.Reactive.Subjects.ReplaySubject`1.ReplayBase.OnNext(T value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Subjects\ReplaySubject.cs:line 277
   at System.Reactive.Subjects.ReplaySubject`1.OnNext(T value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Subjects\ReplaySubject.cs:line 167
   at System.Reactive.Sink`1.ForwardOnNext(TTarget value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Internal\Sink.cs:line 50
   at System.Reactive.IdentitySink`1.OnNext(T value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Internal\IdentitySink.cs:line 16
   at System.Reactive.Sink`1.ForwardOnNext(TTarget value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Internal\Sink.cs:line 50
   at System.Reactive.Linq.ObservableImpl.DistinctUntilChanged`2._.OnNext(TSource value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Linq\Observable\DistinctUntilChanged.cs:line 74
   at System.Reactive.Sink`1.ForwardOnNext(TTarget value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Internal\Sink.cs:line 50
   at System.Reactive.Linq.ObservableImpl.CombineLatest`3._.FirstObserver.OnNext(TFirst value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Linq\Observable\CombineLatest.cs:line 106
   at System.Reactive.Sink`1.ForwardOnNext(TTarget value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Internal\Sink.cs:line 50
   at System.Reactive.IdentitySink`1.OnNext(T value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Internal\IdentitySink.cs:line 16
   at System.Reactive.Sink`1.ForwardOnNext(TTarget value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Internal\Sink.cs:line 50
   at System.Reactive.IdentitySink`1.OnNext(T value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Internal\IdentitySink.cs:line 16
   at Microsoft.FSharp.Control.FSharpEvent`1.Trigger(T arg) in E:\A\_work\130\s\src\fsharp\FSharp.Core\event.fs:line 127
   at <StartupCode$test>.$Program.-ctor@31-6.Invoke() in C:\Users\User\source\repos\avalonia-test\Program.fs:line 31
   at System.Threading.ThreadHelper.ThreadStart_Context(Object state)
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
--- End of stack trace from previous location where exception was thrown ---
   at System.Threading.ThreadHelper.ThreadStart()

If canExecute.Trigger is called on same thread (commented variant) then it works as expected. Well, "Call from invalid thread" is actually true statement here, but from what I understand, one of the main points of using ReactiveUI is that it is supposed to handle such things automatically.

MarkusKgit commented 4 years ago

This is actually working as intended. ReactiveUI doesn't automatically change back to UIThread. See here

mishun commented 4 years ago

https://github.com/reactiveui/ReactiveUI/blob/8951172879d944a983e22cbe640752f09091bbfc/src/ReactiveUI/ReactiveCommand/ReactiveCommand.cs#L653 Hmm, yes, it appears to do only CanExecute updates caused by started/stopped execution on scheduler.

I apologize for my stupid question, but what to do with already existing 3rd party ReactiveCommands? Can I specify in command binding that CanExecute needs to be brought on UI thread, or is there any existing adapter for ReactiveCommand?