diff --git a/src/Avalonia.ReactiveUI/RoutedViewHost.cs b/src/Avalonia.ReactiveUI/RoutedViewHost.cs
new file mode 100644
index 0000000000..726e086d9c
--- /dev/null
+++ b/src/Avalonia.ReactiveUI/RoutedViewHost.cs
@@ -0,0 +1,224 @@
+using System;
+using System.Reactive.Disposables;
+using System.Reactive.Linq;
+using Avalonia.Animation;
+using Avalonia.Controls;
+using Avalonia.Styling;
+using ReactiveUI;
+using Splat;
+
+namespace Avalonia
+{
+ ///
+ /// This control hosts the View associated with ReactiveUI RoutingState,
+ /// and will display the View and wire up the ViewModel whenever a new
+ /// ViewModel is navigated to. Nested routing is also supported.
+ ///
+ ///
+ ///
+ /// ReactiveUI routing consists of an IScreen that contains current
+ /// RoutingState, several IRoutableViewModels, and a platform-specific
+ /// XAML control called RoutedViewHost.
+ ///
+ ///
+ /// RoutingState manages the ViewModel navigation stack and allows
+ /// ViewModels to navigate to other ViewModels. IScreen is the root of
+ /// a navigation stack; despite the name, its views don't have to occupy
+ /// the whole screen. RoutedViewHost monitors an instance of RoutingState,
+ /// responding to any changes in the navigation stack by creating and
+ /// embedding the appropriate view.
+ ///
+ ///
+ /// Place this control to a view containing your ViewModel that implements
+ /// IScreen, and bind IScreen.Router property to RoutedViewHost.Router property.
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// ]]>
+ ///
+ ///
+ ///
+ /// See
+ /// ReactiveUI routing documentation website for more info.
+ ///
+ ///
+ public class RoutedViewHost : UserControl, IActivatable, IEnableLogger
+ {
+ ///
+ /// The router dependency property.
+ ///
+ public static readonly AvaloniaProperty RouterProperty =
+ AvaloniaProperty.Register(nameof(Router));
+
+ ///
+ /// The default content property.
+ ///
+ public static readonly AvaloniaProperty DefaultContentProperty =
+ AvaloniaProperty.Register(nameof(DefaultContent));
+
+ ///
+ /// Fade in animation property.
+ ///
+ public static readonly AvaloniaProperty FadeInAnimationProperty =
+ AvaloniaProperty.Register(nameof(DefaultContent),
+ CreateOpacityAnimation(0d, 1d, TimeSpan.FromSeconds(0.25)));
+
+ ///
+ /// Fade out animation property.
+ ///
+ public static readonly AvaloniaProperty FadeOutAnimationProperty =
+ AvaloniaProperty.Register(nameof(DefaultContent),
+ CreateOpacityAnimation(1d, 0d, TimeSpan.FromSeconds(0.25)));
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public RoutedViewHost()
+ {
+ this.WhenActivated(disposables =>
+ {
+ this.WhenAnyObservable(x => x.Router.CurrentViewModel)
+ .DistinctUntilChanged()
+ .Subscribe(NavigateToViewModel)
+ .DisposeWith(disposables);
+ });
+ }
+
+ ///
+ /// Gets or sets the of the view model stack.
+ ///
+ public RoutingState Router
+ {
+ get => GetValue(RouterProperty);
+ set => SetValue(RouterProperty, value);
+ }
+
+ ///
+ /// Gets or sets the content displayed whenever there is no page currently routed.
+ ///
+ public object DefaultContent
+ {
+ get => GetValue(DefaultContentProperty);
+ set => SetValue(DefaultContentProperty, value);
+ }
+
+ ///
+ /// Gets or sets the animation played when page appears.
+ ///
+ public IAnimation FadeInAnimation
+ {
+ get => GetValue(FadeInAnimationProperty);
+ set => SetValue(FadeInAnimationProperty, value);
+ }
+
+ ///
+ /// Gets or sets the animation played when page disappears.
+ ///
+ public IAnimation FadeOutAnimation
+ {
+ get => GetValue(FadeOutAnimationProperty);
+ set => SetValue(FadeOutAnimationProperty, value);
+ }
+
+ ///
+ /// Duplicates the Content property with a private setter.
+ ///
+ public new object Content
+ {
+ get => base.Content;
+ private set => base.Content = value;
+ }
+
+ ///
+ /// Gets or sets the ReactiveUI view locator used by this router.
+ ///
+ public IViewLocator ViewLocator { get; set; }
+
+ ///
+ /// Invoked when ReactiveUI router navigates to a view model.
+ ///
+ /// ViewModel to which the user navigates.
+ ///
+ /// Thrown when ViewLocator is unable to find the appropriate view.
+ ///
+ private void NavigateToViewModel(IRoutableViewModel viewModel)
+ {
+ if (viewModel == null)
+ {
+ this.Log().Info("ViewModel is null, falling back to default content.");
+ UpdateContent(DefaultContent);
+ return;
+ }
+
+ var viewLocator = ViewLocator ?? ReactiveUI.ViewLocator.Current;
+ var view = viewLocator.ResolveView(viewModel);
+ if (view == null) throw new Exception($"Couldn't find view for '{viewModel}'. Is it registered?");
+
+ this.Log().Info($"Ready to show {view} with autowired {viewModel}.");
+ view.ViewModel = viewModel;
+ UpdateContent(view);
+ }
+
+ ///
+ /// Updates the content with transitions.
+ ///
+ /// New content to set.
+ private async void UpdateContent(object newContent)
+ {
+ if (FadeOutAnimation != null)
+ await FadeOutAnimation.RunAsync(this, Clock);
+ Content = newContent;
+ if (FadeInAnimation != null)
+ await FadeInAnimation.RunAsync(this, Clock);
+ }
+
+ ///
+ /// Creates opacity animation for this routed view host.
+ ///
+ /// Opacity to start from.
+ /// Opacity to finish with.
+ /// Duration of the animation.
+ /// Animation object instance.
+ private static IAnimation CreateOpacityAnimation(double from, double to, TimeSpan duration)
+ {
+ return new Avalonia.Animation.Animation
+ {
+ Duration = duration,
+ Children =
+ {
+ new KeyFrame
+ {
+ Setters =
+ {
+ new Setter
+ {
+ Property = OpacityProperty,
+ Value = from
+ }
+ },
+ Cue = new Cue(0d)
+ },
+ new KeyFrame
+ {
+ Setters =
+ {
+ new Setter
+ {
+ Property = OpacityProperty,
+ Value = to
+ }
+ },
+ Cue = new Cue(1d)
+ }
+ }
+ };
+ }
+ }
+}
diff --git a/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs
new file mode 100644
index 0000000000..c85b999af2
--- /dev/null
+++ b/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs
@@ -0,0 +1,104 @@
+using System;
+using System.Reactive.Concurrency;
+using System.Reactive.Disposables;
+using Avalonia.Controls;
+using Avalonia.Rendering;
+using Avalonia.Platform;
+using Avalonia.UnitTests;
+using Avalonia;
+using ReactiveUI;
+using DynamicData;
+using Xunit;
+using Splat;
+using Avalonia.Markup.Xaml;
+using System.ComponentModel;
+using System.Threading.Tasks;
+using System.Reactive;
+
+namespace Avalonia
+{
+ public class RoutedViewHostTest
+ {
+ public class FirstRoutableViewModel : ReactiveObject, IRoutableViewModel
+ {
+ public string UrlPathSegment => "first";
+
+ public IScreen HostScreen { get; set; }
+ }
+
+ public class FirstRoutableView : ReactiveUserControl { }
+
+ public class SecondRoutableViewModel : ReactiveObject, IRoutableViewModel
+ {
+ public string UrlPathSegment => "second";
+
+ public IScreen HostScreen { get; set; }
+ }
+
+ public class SecondRoutableView : ReactiveUserControl { }
+
+ public class ScreenViewModel : ReactiveObject, IScreen
+ {
+ public RoutingState Router { get; } = new RoutingState();
+ }
+
+ public RoutedViewHostTest()
+ {
+ Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher));
+ Locator.CurrentMutable.Register(() => new FirstRoutableView(), typeof(IViewFor));
+ Locator.CurrentMutable.Register(() => new SecondRoutableView(), typeof(IViewFor));
+ }
+
+ [Fact]
+ public void RoutedViewHostShouldStayInSyncWithRoutingState()
+ {
+ var screen = new ScreenViewModel();
+ var defaultContent = new TextBlock();
+ var host = new RoutedViewHost
+ {
+ Router = screen.Router,
+ DefaultContent = defaultContent,
+ FadeOutAnimation = null,
+ FadeInAnimation = null
+ };
+
+ var root = new TestRoot
+ {
+ Child = host
+ };
+
+ Assert.NotNull(host.Content);
+ Assert.Equal(typeof(TextBlock), host.Content.GetType());
+ Assert.Equal(defaultContent, host.Content);
+
+ screen.Router.Navigate
+ .Execute(new FirstRoutableViewModel())
+ .Subscribe();
+
+ Assert.NotNull(host.Content);
+ Assert.Equal(typeof(FirstRoutableView), host.Content.GetType());
+
+ screen.Router.Navigate
+ .Execute(new SecondRoutableViewModel())
+ .Subscribe();
+
+ Assert.NotNull(host.Content);
+ Assert.Equal(typeof(SecondRoutableView), host.Content.GetType());
+
+ screen.Router.NavigateBack
+ .Execute(Unit.Default)
+ .Subscribe();
+
+ Assert.NotNull(host.Content);
+ Assert.Equal(typeof(FirstRoutableView), host.Content.GetType());
+
+ screen.Router.NavigateBack
+ .Execute(Unit.Default)
+ .Subscribe();
+
+ Assert.NotNull(host.Content);
+ Assert.Equal(typeof(TextBlock), host.Content.GetType());
+ Assert.Equal(defaultContent, host.Content);
+ }
+ }
+}
\ No newline at end of file