ReactiveTabbedPage Data Binding
Asked Answered
L

1

7

I have been using ReactiveUI for a while with Xamarin Forms, but I've hit a brick wall when trying to use a ReactiveTabbedPage. I can't figure out how the ViewModel will get bound to the ReactiveContentPage's that are the children of the ReactiveTabbedPage.

So, as an example, I might have the following XAML:

<ReactiveTabbedPage x:Name="TabbedPage">
    <local:Page1View x:Name="Page1" />
    <local:Page2View x:Name="Page2" />
</ReactiveTabbedPage>

Where Page1View and Page2View are both of type ReactiveContentPage and T is the associated ViewModel.

What I expected to happen was that when the ReactiveTabbedPage was navigated to, Page1View would be displayed, and the ViewModel would be loaded (in the same way it would if I navigated to the Page1View directly). However, the ViewModel never gets called (the constructor is never fired and no data binding occurs).

However, both Page1View and Page2View do render and I can see the initial data that is created in those views (e.g. default text for labels etc.).

I know that the ViewModel stuff is working correctly, because if I navigate to Page1View directly (e.g. not in the ReactiveTabbedPage) everything displays as I expect.

Have I missed something, or am I going about this the wrong way? Or is this just not supported in the current version of RxUI?

Any advice is greatly appreciated!

Littoral answered 10/1, 2017 at 9:35 Comment(0)
G
11

The responsibility for tying the VM to the child pages lies with the host page (i.e. the ReactiveTabbedPage). It alone knows which VM corresponds to which view.

Let's take this one step at a time. First of all, the MainViewModel:

public class MainViewModel : ReactiveObject
{
    public ChildViewModel1 Child1 => new ChildViewModel1();

    public ChildViewModel2 Child2 => new ChildViewModel2();
}

This code obviously isn't realistic because you wouldn't want to recreate the child VMs upon every property access. It's more the API that's pertinent here.

ChildViewModel1 looks like this:

public class ChildViewModel1 : ReactiveObject
{
    public string Test => "Hello";
}

And ChildViewModel2 looks much the same.

Now we can go about setting the views up. Our MainView.xaml looks like this:

<?xml version="1.0" encoding="utf-8" ?>
<rxui:ReactiveTabbedPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:TypeArguments="vms:MainViewModel"
             xmlns:local="clr-namespace:ReactiveTabbedPageTest"
             xmlns:rxui="clr-namespace:ReactiveUI.XamForms;assembly=ReactiveUI.XamForms"
             xmlns:vms="clr-namespace:ReactiveTabbedPageTest.VMs"
             x:Class="ReactiveTabbedPageTest.MainView">

    <local:Child1View x:Name="child1View" Title="Child 1"/>
    <local:Child2View x:Name="child2View" Title="Child 2"/>

</rxui:ReactiveTabbedPage>

Notice it declares each of the child views. We need to hook up the VMs to those views, which we do in the code-behind for MainView:

public partial class MainView : ReactiveTabbedPage<VMs.MainViewModel>
{
    public MainView()
    {
        InitializeComponent();
        this.ViewModel = new VMs.MainViewModel();

        this.WhenActivated(
            disposables =>
            {
                this
                    .OneWayBind(this.ViewModel, x => x.Child1, x => x.child1View.ViewModel)
                    .DisposeWith(disposables);
                this
                    .OneWayBind(this.ViewModel, x => x.Child2, x => x.child2View.ViewModel)
                    .DisposeWith(disposables);
            });
    }
}

I've done this the safest way by using WhenActivated and OneWayBind calls. In reality, it's unlikely your child VMs will change, so directly assigning them rather than binding is totally fine.

Now our child views can be thrown together. Here's ChildView1.xaml:

<?xml version="1.0" encoding="utf-8" ?>
<rxui:ReactiveContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="ReactiveTabbedPageTest.Child1View"
             x:TypeArguments="vms:ChildViewModel1"
             xmlns:rxui="clr-namespace:ReactiveUI.XamForms;assembly=ReactiveUI.XamForms"
             xmlns:vms="clr-namespace:ReactiveTabbedPageTest.VMs">
    <Label x:Name="label" VerticalTextAlignment="Center" HorizontalTextAlignment="Center"/>
</rxui:ReactiveContentPage>

And the code-behind:

public partial class Child1View : ReactiveContentPage<ChildViewModel1>
{
    public Child1View()
    {
        InitializeComponent();

        this.WhenActivated(
            disposables =>
            {
                this
                    .OneWayBind(this.ViewModel, x => x.Test, x => x.label.Text)
                    .DisposeWith(disposables);
            });
    }
}

Once again we're doing the usual RxUI binding goodness to associate properties in the VM with controls in the UI. And once again you could optimize this for properties that don't mutate.

For the purposes of this example, ChildView2 is much the same as ChildView1, but obviously it could be totally different.

The end result is as you'd expect:

animation of reactive tabbed control

What's not evident from the screenshot but is very important is that each tab is deactivating when you switch away from it (as would its associated view model if it implemented ISupportsActivation). This means you can clean up any bindings and subscriptions for that tab when it's not in use, reducing memory pressure and improving performance.

Gonadotropin answered 10/1, 2017 at 10:23 Comment(3)
This wasn't immediately obvious, but solves the problem perfectly! Good news about the deactivation too!Littoral
The above works awesome when the children are contentPages, but how do I hook-up a TabbedPage with a child that is Master/detail (NavigationPage)? My implementation works the first time I click listview item and then click back button. Each additional time I click the listview item I have to click back button more and more to get back to original listview. e.g., (4th time viewing item detail) Click item -> displays detail -> click back once, twice, third, fourth returns to listview. I must not be disposing of the detail page/view correctly :(Horny
SOLVED: "This code obviously isn't realistic because you wouldn't want to recreate the child VMs upon every property access..." this was the problem I was having.Horny

© 2022 - 2024 — McMap. All rights reserved.