Hunting around on the web, and SO (including a question that I asked about 11 months ago), I've cobbled together an answer, which I post below. I'd love it if someone who knows this stuff can tell me if this is right.
First, there is documentation on this: https://github.com/reactiveui/ReactiveUI/blob/master/docs/basics/routing.md. However, to me, at least, it is too high level (I've not used RxUI in the WPF world, for example). But now that I've worked through the issues, it does make sense.
In the end my goal was to see if I could have common code for a universal app. All the files below are created in the shared project. When implementing the back button, etc., for Windows Store app this might diverge, but I've not tackled that yet.
First, you need a container page setup. The XAML of my MainPage.xaml looked like the following:
<Page
x:Class="IWalker.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:IWalker"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:rxui="using:ReactiveUI"
mc:Ignorable="d">
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<rxui:RoutedViewHost Router="{Binding Router}" />
</Grid>
And the main page cs file:
public sealed partial class MainPage : Page
{
public MainPage()
{
this.InitializeComponent();
DataContext = Locator.Current.GetService(typeof(IScreen));
}
}
There are a number of ways to setup the data context, I'm not sure what the best one is... But this works as long as you modify your App.xaml.cs file to look something like this in the ctor:
public App()
{
this.InitializeComponent();
this.Suspending += this.OnSuspending;
autoSuspendHelper = new AutoSuspendHelper(this);
RxApp.SuspensionHost.CreateNewAppState = () => new MainPageViewModel();
RxApp.SuspensionHost.SetupDefaultSuspendResume();
// Register everything... becasue....
Locator.CurrentMutable.Register(() => new StartPage(), typeof(IViewFor<StartPageViewModel>));
Locator.CurrentMutable.Register(() => new MeetingPage(), typeof(IViewFor<MeetingPageViewModel>));
// Create the main view model, and register that.
var r = new RoutingState();
Locator.CurrentMutable.RegisterConstant(r, typeof(RoutingState));
Locator.CurrentMutable.RegisterConstant(new MainPageViewModel(r), typeof(IScreen));
}
The key here are the lines below the comment "// Create the main view model". You have to create the RoutingState, and there is where I register IScreen so I can pick out a single version of it later. This is all the DI code that is used by RxUI. I'm not 100% sure I will need RoutingState registered like this as I always ended up referencing it off IScreen. So it could be that line is unnecessary is a full blown app.
Finally, we need the main page view model:
class MainPageViewModel : ReactiveObject, IScreen
{
/// <summary>
/// Return the routing state.
/// </summary>
public RoutingState Router { get; private set; }
public MainPageViewModel(RoutingState state = null)
{
Router = state;
// Go to the first page and get this show ion the road.
Router.Navigate.Execute(new StartPageViewModel(this));
}
}
There you see the router getting stored, and the IScreen dependency. And, most important, you see the switch to the StartPage, which is the first "interesting page" of my app.
Start page is pretty simple. First, note the StartPage registration in the App.xaml.cs ctor shown above. Next, here is the xaml for reference:
<Page ...
<Grid>
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<TextBox Name="IndicoUrl" MinWidth="200"/>
<Button Name="FindIndicoUrl" Content="Load It!"/>
</StackPanel>
</Grid>
</Page>
Next is the code in the xaml code behind. Note this is more than some other frameworks (like CaliburnMicro). It could well be there is a better way to do this - I know RxUI supports design time authoring of bindings, etc., but I've not tackled that yet. And I do not think RxUI implements wiring up by convention.
public sealed partial class StartPage : Page, IViewFor<StartPageViewModel>
{
public StartPage()
{
this.InitializeComponent();
this.BindCommand(ViewModel, x => x.SwitchPages, x => x.FindIndicoUrl);
this.Bind(ViewModel, x => x.MeetingAddress, y => y.IndicoUrl.Text);
}
public StartPageViewModel ViewModel
{
get { return (StartPageViewModel)GetValue(ViewModelProperty); }
set { SetValue(ViewModelProperty, value); }
}
public static readonly DependencyProperty ViewModelProperty =
DependencyProperty.Register("ViewModel", typeof(StartPageViewModel), typeof(StartPage), new PropertyMetadata(null));
object IViewFor.ViewModel
{
get { return ViewModel; }
set { ViewModel = (StartPageViewModel)value; }
}
}
Note the IVeiwFor dependency at the top. This requires implementing the ViewModel dependency property a little further down. Note that the ctor setups up bindings on the view model, which is actually null at the time the ctor is called. This is dealt with by the plumbing. As usual in a MVVM, the View knows all about the ViewModel and not vice versa.
Finally, the StartPage view model:
public class StartPageViewModel : ReactiveObject, IRoutableViewModel
{
/// <summary>
/// When clicked, it will cause the page to switch and the text to be saved.
/// </summary>
public ReactiveCommand<object> SwitchPages { get; set; }
/// <summary>
/// The meeting address (bindable).
/// </summary>
public string MeetingAddress
{
get { return _meetingAddress; }
set { this.RaiseAndSetIfChanged(ref _meetingAddress, value); }
}
private string _meetingAddress;
/// <summary>
/// Setup the page
/// </summary>
public StartPageViewModel(IScreen screen)
{
HostScreen = screen;
// We can switch pages only when the user has written something into the meeting address text.
var canNavagateAway = this.WhenAny(x => x.MeetingAddress, x => !string.IsNullOrWhiteSpace(x.Value));
SwitchPages = ReactiveCommand.Create(canNavagateAway);
// When we navigate away, we should save the text and go
SwitchPages
.Select(x => MeetingAddress)
.Subscribe(addr =>
{
Settings.LastViewedMeeting = addr;
HostScreen.Router.Navigate.Execute(new MeetingPageViewModel(HostScreen, addr));
});
// Setup the first value for the last time we ran.
MeetingAddress = Settings.LastViewedMeeting;
}
/// <summary>
/// Track the home screen.
/// </summary>
public IScreen HostScreen {get; private set;}
/// <summary>
/// Where we will be located.
/// </summary>
public string UrlPathSegment
{
get { return "/home"; }
}
}
Note the last two properties, which are demanded by the routable view interface dependency. HostScreen needs to be set by the ctor, and, as you'd expect in a RxUI app, you set everything up in the ctor of the VM for behavior.
I'm far from an expert here. Please let me know if there are easier ways and I'll try to update this. Or ways to remove some of the boilerplate code! :-)