0
votes

I am currently working in a Xamarin Forms project and am attempting to use ReactiveUI with a Master Detail Page navigation standard Xamarin Forms project.

After reading through the documentation on the ReactiveUI website as well as the book 'You, I and ReactiveUI' by Kent Boogaart I am still lost on how to make a Master Detail setup work with ReactiveUI and Xamarin Forms for app navigation.

I have the app being bootstrapped, which loads the root view (Master Detail Page):

internal sealed class AppBootstrap: ReactiveObject, IScreen
{
    public AppBootstrap()
    {
        RegisterDependencies();

        this
            .Router
            .NavigateAndReset
            .Execute(GetRootViewModel())
            .Subscribe();
    }

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

    public Page GetMainPage() => new RoutedViewHost();

    private void RegisterDependencies()
    {
        Locator.CurrentMutable.RegisterConstant(this, typeof(IScreen));

        /*View Model Registrations*/
        Locator.CurrentMutable.Register(() => new RootView(), typeof(IViewFor<RootViewModel>));
        Locator.CurrentMutable.Register(()=> new MenuView(), typeof(IViewFor<MenuViewModel>));
        Locator.CurrentMutable.Register(()=> new DetailView(), typeof(IViewFor<DetailViewModel>));

        /*Service Registrations*/
    }

    private IRoutableViewModel GetRootViewModel()
    {
        //Add check for login here
        return new RootViewModel(this);
    }
}

And then in my RootViewModel:

public sealed class RootViewModel: ViewModelBase
{
    private Page _masterPage;
    private Page _detailPage;

    public RootViewModel(IScreen hostScreen) : base(hostScreen)
    { }

    public Page MasterPage
    {
        get => _masterPage;
        set => this.RaiseAndSetIfChanged(ref _masterPage, value);
    }

    public Page DetailPage
    {
        get => _detailPage;
        set => this.RaiseAndSetIfChanged(ref _detailPage, value);
    }

    public override Action<CompositeDisposable> OnActivated()
    {
        //What do I do?
    }
}

After googling and searching stack overflow there appears to be no answer here and the 'documentation' isn't too helpful. Both the samples listed on the ReactiveUI site as well as all the blog posts online only deal with single page navigation; vice the more standard drawer based navigation that most real world phone applications implement.

Any help on what to do or links to actual working Master Detail Page based navigation would be extremely helpful as I have just failed on my attempts to find anything of substance.

1
Do you mind opening an issue on GitHub.com/ReactiveUI/website as well. Seems like a important gap in our documentation we need to fix.Glenn Watson
@GlennWatson no problem, I will open up an issue.AndrewHunter
Hello Andrew. I'm curious about how did you finally solve this problem? I'm going with the same thing here as I'm trying to route a view model linked against a master detail page but I have no clue on how to do that.Ahmed Elashker

1 Answers

1
votes

You're right about lack of documentation for MasterDetailPage. Thanks for creating that issue on the site. Until we get that updated, here's one way to do it.

In this example, I assume the detail page has the exact same layout for every item. Even if that doesn't fit your use case, it's pretty easy to customize it to make it work for various layouts.

Instead of creating a new DetailViewModel and DetailPage every time an item is selected, we simply swap out the model. The view listens to this change and rebinds.

public class MyMasterDetailViewModel : ReactiveObject, IRoutableViewModel
{
    private IScreen _hostScreen;

    public MyMasterDetailViewModel(IScreen hostScreen = null)
    {
        _hostScreen = hostScreen ?? Locator.Current.GetService<IScreen>();

        var cellVms = GetData().Select(model => new CustomCellViewModel(model));
        MyList = new ObservableCollection<CustomCellViewModel>(cellVms);

        // Set the first list item as the default detail view content.
        Detail = new DetailViewModel();
        Detail.Model = cellVms.First().Model;

        // Swap out the detail's model property every time the user selects an item.
        this.WhenAnyValue(x => x.Selected)
            .Where(x => x != null)
            .Subscribe(cellVm => Detail.Model = cellVm.Model);
    }

    private CustomCellViewModel _selected;
    public CustomCellViewModel Selected
    {
        get => _selected;
        set => this.RaiseAndSetIfChanged(ref _selected, value);
    }

    public DetailViewModel Detail { get; }

    public ObservableCollection<CustomCellViewModel> MyList { get; }
}

...

public partial class MyMasterDetailPage : ReactiveMasterDetailPage<MyMasterDetailViewModel>
{
    public MyMasterDetailPage()
    {
        InitializeComponent();

        ViewModel = new MasterDetailViewModel();
        Detail = new NavigationPage(new DetailPage(ViewModel.Detail));

        this.WhenActivated(
            disposables =>
            {
                this
                    .OneWayBind(ViewModel, vm => vm.MyList, v => v.MyListView.ItemsSource)
                    .DisposeWith(disposables);
                this
                    .Bind(ViewModel, vm => vm.Selected, v => v.MyListView.SelectedItem)
                    .DisposeWith(disposables);
                this
                    .WhenAnyValue(x => x.ViewModel.Selected)
                    .Where(x => x != null)
                    .Subscribe(
                        model =>
                        {
                            // Hide the master list every time the user selects an item
                            // and reset the SelectedItem "trigger."
                            MyListView.SelectedItem = null;
                            IsPresented = false;
                        })
                    .DisposeWith(disposables);
            });
    }
}

...

public class DetailViewModel : ReactiveObject
{
    private CustomData _model;

    public CustomData Model
    {
        get => _model;
        set => this.RaiseAndSetIfChanged(ref _model, value);
    }

    public string Title => Model.Title;
}

...

public partial class DetailPage : ReactiveContentPage<DetailViewModel>
{
    public DetailPage(DetailViewModel viewModel)
    {
        InitializeComponent();

        ViewModel = viewModel;

        this.WhenActivated(
            disposables =>
            {
                this
                    .WhenAnyValue(x => x.ViewModel.Model)
                    .Where(x => x != null)
                    .Subscribe(model => PopulateFromModel(model))
                    .DisposeWith(disposables);
            });
    }

    private void PopulateFromModel(MyModel model)
    {
        Title = model.Title;
        TitleLabel.Text = model.Title;
    }
}

...

<?xml version="1.0" encoding="utf-8" ?>
<rxui:ReactiveMasterDetailPage
         xmlns="http://xamarin.com/schemas/2014/forms"
         xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
         xmlns:rxui="clr-namespace:ReactiveUI.XamForms;assembly=ReactiveUI.XamForms"
         xmlns:local="clr-namespace:XamFormsSandbox"
         x:Class="XamFormsSandbox.MyMasterDetailPage"
         x:TypeArguments="local:MyMasterDetailViewModel"
         NavigationPage.HasNavigationBar="False">
    <MasterDetailPage.Master>
        <ContentPage Title="Master">
            <StackLayout>
                <ListView x:Name="MyListView">
                    <ListView.ItemTemplate>
                        <DataTemplate>
                            <local:CustomCell />
                        </DataTemplate>
                    </ListView.ItemTemplate>
                </ListView>
            </StackLayout>
        </ContentPage>
    </MasterDetailPage.Master>
</rxui:ReactiveMasterDetailPage>

Note that I'm hiding the RoutedViewHost navigation bar with NavigationPage.HasNavigationBar="False" in the XAML above. Otherwise, we would have two navigation bars on top of each other.

UPDATE

Here's a link to the sample project (currently a PR): https://github.com/reactiveui/ReactiveUI/pull/1741