Attempting to access ViewModel property on ReactiveUserControl<MyViewModel> - raises Operation is not valid due to the current state of the object

Viewed 286

I am attempting to bind a command between a View and associated ViewModel via the code-behind file of the view:

public partial class MyView : ReactiveUserControl<MyViewModel>
{
    public MyView()
    {
        this.WhenActivated(disposables =>
        {
            this.BindCommand(
                this.ViewModel,
                vm => vm.MyCommand,
                v => v.MyButton);
        });
        InitializeComponent();
    }

    private void InitializeComponent()
    {
        AvaloniaXamlLoader.Load(this);
    }
}

This is the corresponding view xaml:

<UserControl 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"
             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
             x:Class="MyProject.Views.MyView"
             xmlns:vm="using:MyProject.ViewModels"
             x:DataType="vm:MyViewModel"
             x:CompileBindings="True"
             >
    <StackPanel>
            <Button Name="MyButton" Content="Next" HorizontalAlignment="Center" />
    </StackPanel>
    
</UserControl>

And this is the View Model which has the command which should be binded to a Button control on the View:

public class MyViewModel : ViewModelBase, IRoutableViewModel, IActivatableViewModel
{
    public ViewModelActivator Activator { get; }
    
    public string UrlPathSegment { get; } = "my-url";
    public IScreen HostScreen { get; }

    public ReactiveCommand<Unit, IRoutableViewModel> MyCommand { get; }
    
    public MyViewModel(IScreen screen)
    {
        Activator = new ViewModelActivator();
        HostScreen = screen;
        
        MyCommand =
            ReactiveCommand.CreateFromObservable( () => 
                HostScreen.Router.Navigate.Execute(new MyViewModel(HostScreen)));
        
        this.WhenActivated((CompositeDisposable disposables) =>
        {
            /* handle activation */
            Disposable
                .Create(() => { /* handle deactivation */ })
                .DisposeWith(disposables);
        });
    } 
    
}

However, this results in the following error being raised:

Unhandled exception. System.InvalidOperationException: Operation is not valid due to the current state of the object.
   at ReactiveUI.Reflection.<>c__DisplayClass4_0.<GetValueFetcherForProperty>b__0(Object obj, Object[] _) in D:\a\ReactiveUI\ReactiveUI\src\ReactiveUI\Expression\Reflection.cs:line 123
   at ReactiveUI.Reflection.TryGetValueForPropertyChain[TValue](TValue& changeValue, Object current, IEnumerable`1 expressionChain) in D:\a\ReactiveUI\ReactiveUI\src\ReactiveUI\Expression\Reflection.cs:line 226
   at ReactiveUI.ObservedChangedMixin.TryGetValue[TSender,TValue](IObservedChange`2 item, TValue& changeValue) in D:\a\ReactiveUI\ReactiveUI\src\ReactiveUI\Mixins\ObservedChangedMixin.cs:line 105
   at ReactiveUI.ObservedChangedMixin.GetValueOrDefault[TSender,TValue](IObservedChange`2 item) in D:\a\ReactiveUI\ReactiveUI\src\ReactiveUI\Mixins\ObservedChangedMixin.cs:line 63
   at ReactiveUI.ReactiveNotifyPropertyChangedMixin.<>c.<NestedObservedChanges>b__5_0(IObservedChange`2 x) in D:\a\ReactiveUI\ReactiveUI\src\ReactiveUI\Mixins\ReactiveNotifyPropertyChangedMixin.cs:line 181
   at System.Reactive.Linq.ObservableImpl.Select`2.Selector._.OnNext(TSource value) in /_/Rx.NET/Source/src/System.Reactive/Linq/Observable/Select.cs:line 39
--- End of stack trace from previous location ---
... (the stacktrace is quite lengthy so I will omit the rest, please let me know if posting the full trace would be helpful)

When I bind the command directly in the Xaml file using the Command={Binding MyCommand} syntax, everything works as expected.

After playing around a bit, I determined the problem seems to be related to referencing the ViewModel property on MyView.

I have ReactiveUI routing configured on my MainWindowViewModel, and I cannot tell whether this is related:

public class MainWindowViewModel : ViewModelBase, IScreen, IActivatableViewModel
    {
        public ViewModelActivator Activator { get; }

        public RoutingState Router { get; } = new RoutingState();

        readonly ObservableAsPropertyHelper<IRoutableViewModel> _currentViewModel;
        private IRoutableViewModel? CurrentViewModel => _currentViewModel.Value;
        public string? CurrentViewUrl() => CurrentViewModel?.UrlPathSegment;

        public MainWindowViewModel()
        {
            Activator = new ViewModelActivator();

            _currentViewModel = Router.CurrentViewModel.ToProperty(this, x => x.CurrentViewModel);

            this.WhenActivated((CompositeDisposable disposables) =>
            {
                Router.Navigate.Execute(new MyViewModel(this));
                /* handle activation */
                Disposable
                    .Create(() => { /* handle deactivation */ })
                    .DisposeWith(disposables);
            });
        }
        
    }

And the corresponding MainWindow.xaml for the view (I have not modified the code-behind file for this view)

<Window xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:vm="using:MyProject.ViewModels"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:reactiveUi="http://reactiveui.net"
        mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
        x:Class="MyProject.Views.MainWindow"
        x:DataType="vm:MainWindowViewModel"
        x:CompileBindings="True"
        Icon="/Assets/avalonia-logo.ico"
        Title="MyProject"
        Background="{DynamicResource Zinc800}">
    
    <reactiveUi:RoutedViewHost Router="{Binding Router}" PageTransition="{x:Null}">
        <reactiveUi:RoutedViewHost.DefaultContent>
            <TextBlock Text="Requested view not found"
                       HorizontalAlignment="Center"
                       VerticalAlignment="Center" />
        </reactiveUi:RoutedViewHost.DefaultContent>
    </reactiveUi:RoutedViewHost>
    
</Window>

I am going in circles trying to figure out why this is happening, when I pan through the various examples implementing bindings in code-behind, nothing stands out to me as different than what I have done here, unless it has something to do with the way I have implemented routing.

Please forgive me if the cause is obvious, I am new to ReactiveUI, MVVM and desktop development.

1 Answers

From the View activation documentation It looks like you can't directly bind to control name like WPF/UWP in Avalonia UI. You need to use FindControl method.

public class View : ReactiveWindow<ViewModel>
{
    // WITH OUT THIS LINE, it will throw "Operation is not valid due to the current state of the object"
    public Button ExampleButton2 => this.FindControl<Button>("ExampleButton");

    public View()
    {
        this.WhenActivated(disposables => 
        {
            // Bind the 'ExampleCommand' to 'ExampleButton' defined above.
            this.BindCommand(ViewModel, x => x.ExampleCommand, x => x.ExampleButton2)
                .DisposeWith(disposables);
        });
        AvaloniaXamlLoader.Load(this);
    }
}
Related