Search code examples
xamarin.formsreactiveui

How to inherit from base class and specify derived class for ReactiveUI view model in Xamarin Forms?


I have in my application base classes for both Xamarin content pages and view models so that common code can be inherited. I am incorporating ReactiveUI into this framework and want to be able to reference the properties specific to the appropriate view model in the WhenActivated clause, but am having issues trying to do that since the Reactive ViewModel property is based upon what type is given in the ReactiveContentPage<>, but I have that type as the base type.

I have created a minimal reproducible example below that uses the ReactiveUI.XamForms nuget.

View model and associated base view model:

public class BaseViewModel : ReactiveObject
{
}

public class MainPageViewModel : BaseViewModel
{
    private string _result;
    public string Result
    {
        get => _result;
        set => this.RaiseAndSetIfChanged(ref _result, value);
    }
    private string _username;
    public string UserName
    {
        get => _username;
        set => this.RaiseAndSetIfChanged(ref _username, value);
    }
    
    public ReactiveCommand<Unit, Unit> RegisterCommand { get; }

    public MainPageViewModel()
    {
        RegisterCommand = ReactiveCommand
            .CreateFromObservable(ExecuteRegisterCommand);
    }

    private IObservable<Unit> ExecuteRegisterCommand()
    {
        Result = "Hello, " + UserName;
        return Observable.Return(Unit.Default);
    }
}

View code behind:

public partial class MainPage : BasePage
{
    public MainPage()
    {
        InitializeComponent();

        BindingContext = new MainPageViewModel(); 

        this.WhenActivated(disposable =>
        {
            this.Bind(ViewModel, x => x.UserName, x => x.Username.Text)
                .DisposeWith(disposable);

            this.BindCommand(ViewModel, x => x.RegisterCommand, x => x.Register)
                .DisposeWith(disposable);

            this.Bind(ViewModel, x => x.Result, x => x.Result.Text)
               .DisposeWith(disposable);
        });
    }        
}

View XAML:

<local:BasePage
x:Class="ReactiveUIXamarin.MainPage"         
xmlns:local="clr-namespace:ReactiveUIXamarin;assembly=ReactiveUIXamarin"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:ios="clr-namespace:Xamarin.Forms.PlatformConfiguration.iOSSpecific;assembly=Xamarin.Forms.Core" 
xmlns="http://xamarin.com/schemas/2014/forms"
  ios:Page.UseSafeArea="true">
  <StackLayout>
      <Entry x:Name="Username" Placeholder="Username"/>
      <Button x:Name="Register" Text="Go" TextColor="White" BackgroundColor="Gray" />
      <Label x:Name="Result" />
  </StackLayout>
</local:BasePage>

Associated Base Page for view:

//if you replace below with ReactiveContentPage<MainPageViewModel> then it compiles, but want to keep this
//with BaseViewModel so that all pages can inherit from this
public class BasePage : ReactiveContentPage<BaseViewModel>
{
}

As noted in the above code comment, replacing ReactiveContentPage<BaseViewModel> with ReactiveContentPage<MainPageViewModel> allows it to compile so that you can see how the program runs in a compiled state, but I want to stay with BaseViewModel as the type so that all pages can inherit from the class.

So the problem comes in at the views code behind, because even though at runtime the ReactiveUI ViewModel property is actually a MainPageViewModel object, it doesn't know that at compile time. So I have tried casting the ViewModel as the derived type:

this.WhenActivated(disposable =>
        {
            this.Bind((MainPageViewModel)ViewModel, x => x.UserName, x => x.Username.Text)
                .DisposeWith(disposable);

            this.BindCommand((MainPageViewModel)ViewModel, x => x.RegisterCommand, x => x.Register)
                .DisposeWith(disposable);

            this.Bind((MainPageViewModel)ViewModel, x => x.Result, x => x.Result.Text)
               .DisposeWith(disposable);
        });

The compiler is OK with the Bind methods, but it is the BindCommand method that I cannot figure out. It gives the compile error of:

The type 'ReactiveUIXamarin.MainPage' cannot be used as type parameter 'TView' in the generic type or method 'CommandBinder.BindCommand<TView, TViewModel, TProp, TControl>(TView, TViewModel?, Expression<Func<TViewModel, TProp>>, Expression<Func<TView, TControl>>, string?)'. There is no implicit reference conversion from 'ReactiveUIXamarin.MainPage' to 'ReactiveUI.IViewFor<ReactiveUIXamarin.ViewModel.MainPageViewModel>'.

It is trying to convert from MainPage to MainPageViewModel and I only want the conversion of the BaseViewModel to MainPageViewModel.

How do I change the BindCommand statement to get this to work?


Solution

  • OK, I finally figured it out. I kept inspecting the ReactiveUI's CommandBinder and what the error I was getting was really saying it wanted. The error stated there was no implicit conversion from RectiveUIXamarin.MainPage to ReactiveUI.IViewFor<ReactiveUIXamarin.ViewModel.MainPageViewModel>.

    The ReactiveUIXamarin.MainPage was the TView parameter in this case and in looking at the CommandBinder, the expectation was that the TView would implement the interface IViewFor<TViewModel>.

    So I added that interface to the MainPage Class in the form of IViewFor<MainPageViewModel>. and then added the implementation of the interface at the end.

    After making those changes, everything compiles and works correctly.

    public partial class MainPage : BasePage, IViewFor<MainPageViewModel>
    {
        public MainPage()
        {
            InitializeComponent();
    
            BindingContext = new MainPageViewModel(); 
    
            this.WhenActivated(disposable =>
            {
                this.Bind((MainPageViewModel)ViewModel, x => x.UserName, x => x.Username.Text)
                    .DisposeWith(disposable);
    
                this.BindCommand((MainPageViewModel)ViewModel, x => x.RegisterCommand, x => x.Register)
                    .DisposeWith(disposable);
    
                this.Bind((MainPageViewModel)ViewModel, x => x.Result, x => x.Result.Text)
                   .DisposeWith(disposable);
            });
        }
    
        MainPageViewModel IViewFor<MainPageViewModel>.ViewModel 
        { 
            get => (MainPageViewModel)ViewModel; 
            set => ViewModel = value; 
        }
    }