committed by
GitHub
27 changed files with 505 additions and 81 deletions
@ -1,22 +0,0 @@ |
|||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
namespace Avalonia |
|||
{ |
|||
/// <summary>
|
|||
/// Specifies that this object supports a simple, transacted notification for batch
|
|||
/// initialization.
|
|||
/// </summary>
|
|||
public interface ISupportInitialize |
|||
{ |
|||
/// <summary>
|
|||
/// Signals the object that initialization is starting.
|
|||
/// </summary>
|
|||
void BeginInit(); |
|||
|
|||
/// <summary>
|
|||
/// Signals the object that initialization is complete.
|
|||
/// </summary>
|
|||
void EndInit(); |
|||
} |
|||
} |
|||
@ -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) |
|||
} |
|||
} |
|||
}; |
|||
} |
|||
} |
|||
} |
|||
@ -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…
Reference in new issue