Browse Source

Add ViewContract to RoutedViewHost

pull/7266/head
Sergey Volkov 5 years ago
parent
commit
af902ae969
  1. 57
      src/Avalonia.ReactiveUI/RoutedViewHost.cs
  2. 73
      tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs

57
src/Avalonia.ReactiveUI/RoutedViewHost.cs

@ -57,7 +57,13 @@ namespace Avalonia.ReactiveUI
/// </summary>
public static readonly StyledProperty<RoutingState?> RouterProperty =
AvaloniaProperty.Register<RoutedViewHost, RoutingState?>(nameof(Router));
/// <summary>
/// <see cref="AvaloniaProperty"/> for the <see cref="ViewContract"/> property.
/// </summary>
public static readonly StyledProperty<string?> ViewContractProperty =
AvaloniaProperty.Register<ViewModelViewHost, string?>(nameof(ViewContract));
/// <summary>
/// Initializes a new instance of the <see cref="RoutedViewHost"/> class.
/// </summary>
@ -70,15 +76,18 @@ namespace Avalonia.ReactiveUI
.Where(router => router == null)!
.Cast<object?>();
var viewContract = this.WhenAnyValue(x => x.ViewContract);
this.WhenAnyValue(x => x.Router)
.Where(router => router != null)
.SelectMany(router => router!.CurrentViewModel)
.Merge(routerRemoved)
.Subscribe(NavigateToViewModel)
.CombineLatest(viewContract)
.Subscribe(tuple => NavigateToViewModel(tuple.First, tuple.Second))
.DisposeWith(disposables);
});
}
/// <summary>
/// Gets or sets the <see cref="RoutingState"/> of the view model stack.
/// </summary>
@ -87,17 +96,27 @@ namespace Avalonia.ReactiveUI
get => GetValue(RouterProperty);
set => SetValue(RouterProperty, value);
}
/// <summary>
/// Gets or sets the view contract.
/// </summary>
public string? ViewContract
{
get => GetValue(ViewContractProperty);
set => SetValue(ViewContractProperty, 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>
private void NavigateToViewModel(object? viewModel)
/// <param name="contract">The contract for view resolution.</param>
private void NavigateToViewModel(object? viewModel, string? contract)
{
if (Router == null)
{
@ -112,17 +131,33 @@ namespace Avalonia.ReactiveUI
Content = DefaultContent;
return;
}
var viewLocator = ViewLocator ?? global::ReactiveUI.ViewLocator.Current;
var viewInstance = viewLocator.ResolveView(viewModel);
var viewInstance = viewLocator.ResolveView(viewModel, contract);
if (viewInstance == null)
{
this.Log().Warn($"Couldn't find view for '{viewModel}'. Is it registered? Falling back to default content.");
if (contract == null)
{
this.Log().Warn($"Couldn't find view for '{viewModel}'. Is it registered? Falling back to default content.");
}
else
{
this.Log().Warn($"Couldn't find view with contract '{contract}' for '{viewModel}'. Is it registered? Falling back to default content.");
}
Content = DefaultContent;
return;
}
this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel}.");
if (contract == null)
{
this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel}.");
}
else
{
this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel} and contract '{contract}'.");
}
viewInstance.ViewModel = viewModel;
if (viewInstance is IDataContextProvider provider)
provider.DataContext = viewModel;

73
tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs

@ -29,6 +29,8 @@ namespace Avalonia.ReactiveUI.UnitTests
public class FirstRoutableView : ReactiveUserControl<FirstRoutableViewModel> { }
public class AlternativeFirstRoutableView : ReactiveUserControl<FirstRoutableViewModel> { }
public class SecondRoutableViewModel : ReactiveObject, IRoutableViewModel
{
public string UrlPathSegment => "second";
@ -38,16 +40,22 @@ namespace Avalonia.ReactiveUI.UnitTests
public class SecondRoutableView : ReactiveUserControl<SecondRoutableViewModel> { }
public class AlternativeSecondRoutableView : ReactiveUserControl<SecondRoutableViewModel> { }
public class ScreenViewModel : ReactiveObject, IScreen
{
public RoutingState Router { get; } = new RoutingState();
}
public static string AlternativeViewContract => "AlternativeView";
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>));
Locator.CurrentMutable.Register(() => new AlternativeFirstRoutableView(), typeof(IViewFor<FirstRoutableViewModel>), AlternativeViewContract);
Locator.CurrentMutable.Register(() => new AlternativeSecondRoutableView(), typeof(IViewFor<SecondRoutableViewModel>), AlternativeViewContract);
}
[Fact]
@ -101,6 +109,71 @@ namespace Avalonia.ReactiveUI.UnitTests
Assert.Equal(defaultContent, host.Content);
}
[Fact]
public void RoutedViewHost_Should_Stay_In_Sync_With_RoutingState_And_Contract()
{
var screen = new ScreenViewModel();
var defaultContent = new TextBlock();
var host = new RoutedViewHost
{
Router = screen.Router,
DefaultContent = defaultContent,
PageTransition = null
};
var root = new TestRoot
{
Child = host
};
Assert.NotNull(host.Content);
Assert.IsType<TextBlock>(host.Content);
Assert.Equal(defaultContent, host.Content);
var first = new FirstRoutableViewModel();
screen.Router.Navigate.Execute(first).Subscribe();
host.ViewContract = null;
Assert.NotNull(host.Content);
Assert.IsType<FirstRoutableView>(host.Content);
Assert.Equal(first, ((FirstRoutableView)host.Content).DataContext);
Assert.Equal(first, ((FirstRoutableView)host.Content).ViewModel);
host.ViewContract = AlternativeViewContract;
Assert.NotNull(host.Content);
Assert.IsType<AlternativeFirstRoutableView>(host.Content);
Assert.Equal(first, ((AlternativeFirstRoutableView)host.Content).DataContext);
Assert.Equal(first, ((AlternativeFirstRoutableView)host.Content).ViewModel);
var second = new SecondRoutableViewModel();
screen.Router.Navigate.Execute(second).Subscribe();
host.ViewContract = null;
Assert.NotNull(host.Content);
Assert.IsType<SecondRoutableView>(host.Content);
Assert.Equal(second, ((SecondRoutableView)host.Content).DataContext);
Assert.Equal(second, ((SecondRoutableView)host.Content).ViewModel);
host.ViewContract = AlternativeViewContract;
Assert.NotNull(host.Content);
Assert.IsType<AlternativeSecondRoutableView>(host.Content);
Assert.Equal(second, ((AlternativeSecondRoutableView)host.Content).DataContext);
Assert.Equal(second, ((AlternativeSecondRoutableView)host.Content).ViewModel);
screen.Router.NavigateBack.Execute(Unit.Default).Subscribe();
Assert.NotNull(host.Content);
Assert.IsType<AlternativeFirstRoutableView>(host.Content);
Assert.Equal(first, ((AlternativeFirstRoutableView)host.Content).DataContext);
Assert.Equal(first, ((AlternativeFirstRoutableView)host.Content).ViewModel);
screen.Router.NavigateBack.Execute(Unit.Default).Subscribe();
Assert.NotNull(host.Content);
Assert.IsType<TextBlock>(host.Content);
Assert.Equal(defaultContent, host.Content);
}
[Fact]
public void RoutedViewHost_Should_Show_Default_Content_When_Router_Is_Null()
{

Loading…
Cancel
Save