'IActivatableViewModel is activated before the View, so UnhandledInteractionException is thrown in VM activation

I have a ViewModel, that uses an Interaction for each item from a queue that exists independent from both View and ViewModel. Processing is started in this.WhenActivated block, and it relies on Interaction. Handler for Interation is set in IViewFor<MyViewModel>.WhenActivated.

Due to the order of activations, UnhandledInteractionException is thrown.

using System;
using System.Reactive;
using System.Reactive.Disposables;
using System.Windows;
using ReactiveUI;

namespace RxActivationRepro
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window, IViewFor<MainViewModel>
    {
        public MainWindow()
        {
            ViewModel = new MainViewModel();
            InitializeComponent();
            this.WhenActivated(d =>
            {
                ViewModel.Interaction.RegisterHandler(ctx =>
                {
                    MessageBox.Show("Interaction!");
                    ctx.SetOutput(Unit.Default);
                });
            });
        }

        object? IViewFor.ViewModel
        {
            get => ViewModel;
            set => ViewModel = (MainViewModel)value;
        }

        public MainViewModel? ViewModel { get; set; }
    }

    public class MainViewModel : ReactiveObject, IActivatableViewModel
    {
        public MainViewModel()
        {
            this.WhenActivated( d =>
            {
                Interaction.Handle(Unit.Default)
                    .Subscribe(_ => { MessageBox.Show("Nope!"); }, ex => { MessageBox.Show(ex.Message); })
                    .DisposeWith(d);
            });
        }

        public ViewModelActivator Activator { get; } = new();
        public Interaction<Unit, Unit> Interaction { get; } = new();
    }
}

I would expect the below code to work, but it does not. I worked around the issue my removing the interface from the VM to disable the View activating the VM, and handle the activation manually, no big deal.

But is there a way to change the order of activations or defer VM activation for later? Or am I approaching this wrong?

Found in RxUI 7.4.0 (don't ask :P), but the same behaviour is in 17.17 + dotnet 6, so I gues it is by design.

More context:

I'm trying to achieve a feature seen for example in Rider, that when you open new solution having already one instance, you get a dialog asking whether to open new instance, or replace contents of current one. I will have it open a special edit window for each new file, hence Interaction to decouple the edit window from the ViewModel that monitors the request queue. Item in queue before the activation can happen, if you open new file edit before loging in. I also don't want to spam windows, so I want to await the Interaction.Handle call to have just one item being edited at the time.



Solution 1:[1]

Well, thanks to the SO community and especially just the website for being my yellow rubber ducky :)

namespace RxActivationRepro
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window, IViewFor<MainViewModel>
    {
        public MainWindow()
        {
            ViewModel = new MainViewModel();
            InitializeComponent();
            this.WhenActivated(d =>
            {
                ViewModel.Interaction.RegisterHandler(ctx =>
                {
                    MessageBox.Show("Interaction!");
                    ctx.SetOutput(Unit.Default);
                });
                
                ViewModel.InteractUntilDisposed().Subscribe().DisposeWith(d);
            });
        }

        object? IViewFor.ViewModel
        {
            get => ViewModel;
            set => ViewModel = (MainViewModel)value;
        }

        public MainViewModel? ViewModel { get; set; }
    }

    public class MainViewModel : ReactiveObject
    {
        public IObservable<Unit> InteractUntilDisposed()
        {
            return Observable.StartAsync(async ct =>
            {
                while (!ct.IsCancellationRequested)
                {
                    await Interaction.Handle(Unit.Default);
                    await Task.Delay(5000);
                }
            });
        }

        public Interaction<Unit, Unit> Interaction { get; } = new();
    }
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1