reactiveui/splat

[BUG?] ReactiveCommands instantiated after setting up Splat.AutoFac trigger "Call from invalid thread" excetpion.

chris-stones opened this issue · 8 comments

Description
I am using Splat.Autofac with AvaloniaUI.
ReactiveCommands that are instantiated after Splat.Autofac is setup cuase an exception to be thrown after the commands action is invoked.
ReactiveCommands that are instantiated before Splat.Autofac function just fine.

Steps To Reproduce

  1. Clone and run https://github.com/chris-stones/AvaloniaAutofacBug
  2. Click "I Dont Crash".
  3. Observe "Dont Crash Clicked" in Debugger Output window.
  4. Click "I Do Crash".
  5. Observe "Do Crash Clicked" in Debugger Output window.
  6. Observe Exception.
The thread 0x6bd0 has exited with code 0 (0x0).
An unhandled exception of type 'System.InvalidOperationException' occurred in System.Private.CoreLib.dll
Call from invalid thread

Expected behavior
Neither button clicks should throw an exception.

Environment(please complete the following information):

  • OS: Windows 10 AND Arch Linux

Additional context
.NET Core 3.1

<ItemGroup>
    <PackageReference Include="Autofac" Version="4.9.4" />
    <PackageReference Include="Avalonia" Version="0.9.11" />
    <PackageReference Include="Avalonia.Desktop" Version="0.9.11" />
    <PackageReference Include="Avalonia.ReactiveUI" Version="0.9.11" />
    <PackageReference Include="Splat.Autofac" Version="9.0.5" />
  </ItemGroup>

Full stack trace here

   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._.SecondObserver.OnNext(TSecond value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Linq\Observable\CombineLatest.cs:line 177
   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.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.Select`2.Selector._.OnNext(TSource value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Linq\Observable\Select.cs:line 48
   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.Scan`2._.OnNext(TSource value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Linq\Observable\Scan.cs:line 49
   at System.Reactive.SafeObserver`1.WrappingSafeObserver.OnNext(TSource value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Internal\SafeObserver.cs:line 31
   at System.Reactive.ObserveOnObserverLongRunning`1.Drain() in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Internal\ScheduledObserver.cs:line 740
   at System.Reactive.ObserveOnObserverLongRunning`1.<>c.<.cctor>b__17_0(ObserveOnObserverLongRunning`1 self, ICancelable cancelable) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Internal\ScheduledObserver.cs:line 685
   at System.Reactive.Concurrency.DefaultScheduler.LongRunning.LongScheduledWorkItem`1.<>c.<.ctor>b__3_0(Object thisObject) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Concurrency\DefaultScheduler.cs:line 182
   at System.Reactive.Concurrency.ConcurrencyAbstractionLayerImpl.<>c.<StartThread>b__8_0(Object itemObject) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Concurrency\ConcurrencyAbstractionLayerImpl.cs:line 76
   at System.Threading.ThreadHelper.ThreadStart_Context(Object state)
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Threading.ThreadHelper.ThreadStart(Object obj)

It's due to the fact that Avalonia doesn't register a Splat container registry, it does all its work in UseReactiveUI()

I got the AutoFac to work by doing

        public static AppBuilder BuildAvaloniaApp()
        {
            var builder = new ContainerBuilder();
            builder.UseAutofacDependencyResolver();

            return AppBuilder.Configure<App>()
                            .UsePlatformDetect()
                            .LogToDebug()
                            .UseReactiveUI();
        }

Doing the container registration before UseReactiveUI. You could also do what's inside that method and do

                RxApp.MainThreadScheduler = AvaloniaScheduler.Instance;
                Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher));
                Locator.CurrentMutable.RegisterConstant(new AutoDataTemplateBindingHook(), typeof(IPropertyBindingHook));

I cant thank you enough!
This was driving me crazy.
THANKS!

@glennawatson Please note that there is warning in Avalonia project template:

        // WARNING! Do not put anything here, or stuff WILL break!
        // Use App FrameworkInitialized class to do so.
        // Initialization code. Don't use any Avalonia, third-party APIs or any
        // SynchronizationContext-reliant code before AppMain is called: things aren't initialized
        // yet and stuff might break.

And it is there for reason. Getting AvaloniaScheduler before FrameworkInitialized may break things in avalonia, I've had serious performance problems because of this.

Registering the splat container won't call that scheduler.

A real solution is if you can get the avalonka guys to hook in like this. https://github.com/reactiveui/ReactiveUI/blob/b1f4f879f68312c7d8f9d0df01f336a0c5b4fefe/src/ReactiveUI/RxApp.cs#L80

This will cause the new registrations to be passed onto new containers

GitHub
An advanced, composable, functional reactive model-view-viewmodel framework for all .NET platforms that is inspired by functional reactive programming. ReactiveUI allows you to abstract mutable st...

I must say that the Avalonia template comment was the whole reason I got myselt into this mess.

// WARNING! Do not put anything here, or stuff WILL break!
// Use App FrameworkInitialized class to do so.
// Initialization code. Don't use any Avalonia, third-party APIs or any
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
// yet and stuff might break.

My instinct was to initialise Autofac FIRST, as soon as possible...
When I saw that comment, I decided to instead initialise Autofac in OnFrameworkInitializationCompleted, which caused the issues raised here.

Looks like i just mis-interprted the meaning of third-party API in this context..

Thanks Everyone!

This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.