Browse Source

Merge pull request #2294 from worldbeater/master

Add RoutedViewHost control implementation
pull/2313/head
Steven Kirk 8 years ago
committed by GitHub
parent
commit
0f66c591ee
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 224
      src/Avalonia.ReactiveUI/RoutedViewHost.cs
  2. 104
      tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs

224
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
{
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// <para>
/// ReactiveUI routing consists of an IScreen that contains current
/// RoutingState, several IRoutableViewModels, and a platform-specific
/// XAML control called RoutedViewHost.
/// </para>
/// <para>
/// 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.
/// </para>
/// <para>
/// Place this control to a view containing your ViewModel that implements
/// IScreen, and bind IScreen.Router property to RoutedViewHost.Router property.
/// <code>
/// <![CDATA[
/// <rxui:RoutedViewHost
/// HorizontalAlignment="Stretch"
/// VerticalAlignment="Stretch"
/// Router="{Binding Router}">
/// <rxui:RoutedViewHost.DefaultContent>
/// <TextBlock Text="Default Content"/>
/// </rxui:RoutedViewHost.DefaultContent>
/// </rxui:RoutedViewHost>
/// ]]>
/// </code>
/// </para>
/// <para>
/// See <see href="https://reactiveui.net/docs/handbook/routing/">
/// ReactiveUI routing documentation website</see> for more info.
/// </para>
/// </remarks>
public class RoutedViewHost : UserControl, IActivatable, IEnableLogger
{
/// <summary>
/// The router dependency property.
/// </summary>
public static readonly AvaloniaProperty<RoutingState> RouterProperty =
AvaloniaProperty.Register<RoutedViewHost, RoutingState>(nameof(Router));
/// <summary>
/// The default content property.
/// </summary>
public static readonly AvaloniaProperty<object> DefaultContentProperty =
AvaloniaProperty.Register<RoutedViewHost, object>(nameof(DefaultContent));
/// <summary>
/// Fade in animation property.
/// </summary>
public static readonly AvaloniaProperty<IAnimation> FadeInAnimationProperty =
AvaloniaProperty.Register<RoutedViewHost, IAnimation>(nameof(DefaultContent),
CreateOpacityAnimation(0d, 1d, TimeSpan.FromSeconds(0.25)));
/// <summary>
/// Fade out animation property.
/// </summary>
public static readonly AvaloniaProperty<IAnimation> FadeOutAnimationProperty =
AvaloniaProperty.Register<RoutedViewHost, IAnimation>(nameof(DefaultContent),
CreateOpacityAnimation(1d, 0d, TimeSpan.FromSeconds(0.25)));
/// <summary>
/// Initializes a new instance of the <see cref="RoutedViewHost"/> class.
/// </summary>
public RoutedViewHost()
{
this.WhenActivated(disposables =>
{
this.WhenAnyObservable(x => x.Router.CurrentViewModel)
.DistinctUntilChanged()
.Subscribe(NavigateToViewModel)
.DisposeWith(disposables);
});
}
/// <summary>
/// Gets or sets the <see cref="RoutingState"/> of the view model stack.
/// </summary>
public RoutingState Router
{
get => GetValue(RouterProperty);
set => SetValue(RouterProperty, value);
}
/// <summary>
/// Gets or sets the content displayed whenever there is no page currently routed.
/// </summary>
public object DefaultContent
{
get => GetValue(DefaultContentProperty);
set => SetValue(DefaultContentProperty, value);
}
/// <summary>
/// Gets or sets the animation played when page appears.
/// </summary>
public IAnimation FadeInAnimation
{
get => GetValue(FadeInAnimationProperty);
set => SetValue(FadeInAnimationProperty, value);
}
/// <summary>
/// Gets or sets the animation played when page disappears.
/// </summary>
public IAnimation FadeOutAnimation
{
get => GetValue(FadeOutAnimationProperty);
set => SetValue(FadeOutAnimationProperty, value);
}
/// <summary>
/// Duplicates the Content property with a private setter.
/// </summary>
public new object Content
{
get => base.Content;
private set => base.Content = value;
}
/// <summary>
/// Gets or sets the ReactiveUI view locator used by this router.
/// </summary>
public IViewLocator ViewLocator { get; set; }
/// <summary>
/// Invoked when ReactiveUI router navigates to a view model.
/// </summary>
/// <param name="viewModel">ViewModel to which the user navigates.</param>
/// <exception cref="Exception">
/// Thrown when ViewLocator is unable to find the appropriate view.
/// </exception>
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);
}
/// <summary>
/// Updates the content with transitions.
/// </summary>
/// <param name="newContent">New content to set.</param>
private async void UpdateContent(object newContent)
{
if (FadeOutAnimation != null)
await FadeOutAnimation.RunAsync(this, Clock);
Content = newContent;
if (FadeInAnimation != null)
await FadeInAnimation.RunAsync(this, Clock);
}
/// <summary>
/// Creates opacity animation for this routed view host.
/// </summary>
/// <param name="from">Opacity to start from.</param>
/// <param name="to">Opacity to finish with.</param>
/// <param name="duration">Duration of the animation.</param>
/// <returns>Animation object instance.</returns>
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)
}
}
};
}
}
}

104
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<FirstRoutableViewModel> { }
public class SecondRoutableViewModel : ReactiveObject, IRoutableViewModel
{
public string UrlPathSegment => "second";
public IScreen HostScreen { get; set; }
}
public class SecondRoutableView : ReactiveUserControl<SecondRoutableViewModel> { }
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<FirstRoutableViewModel>));
Locator.CurrentMutable.Register(() => new SecondRoutableView(), typeof(IViewFor<SecondRoutableViewModel>));
}
[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);
}
}
}
Loading…
Cancel
Save